Project Files
src / helpers / globalConfigReader.ts
/**
* Direct reader for ~/.lmstudio/.internal/global-plugin-configs.json
*
* SDK's getGlobalPluginConfig() may return stale values because per-chat config changes trigger plugin restart, but global config changes do not.
* This helper reads directly from the JSON persistence file which is written immediately on every change.
*
* Plugin-agnostic: Uses getSelfPluginIdentifier() from core-bundle to resolve our own "owner/name" identifier.
*
* Priority rules:
* - Tool-call parameters: SDK cache is fresh (tool invocation triggers refresh). Use SDK directly.
* - main() / early generate(): SDK may be stale or unavailable. Prefer direct JSON read as ground truth.
* - If both sources have a value → JSON wins (most current).
* - If only one source has a value → use that one.
* - If both are empty/undefined → intentional default, return undefined to let schematics defaults apply.
*/
import fs from "fs";
import os from "os";
import path from "path";
import { getSelfPluginIdentifier } from "../core-bundle.mjs";
const GLOBAL_CONFIGS_PATH = path.join(
os.homedir(),
".lmstudio",
".internal",
"global-plugin-configs.json"
);
interface GlobalConfigField {
key: string;
value: any;
}
interface PluginEntry {
fields?: GlobalConfigField[];
}
/**
* Reads the raw global plugin configs JSON file.
*/
function readGlobalConfigsJson(): Record<string, any> | null {
try {
const content = fs.readFileSync(GLOBAL_CONFIGS_PATH, "utf-8");
return JSON.parse(content);
} catch (e) {
console.warn("[globalConfigReader] Failed to read global-plugin-configs.json:", e instanceof Error ? e.message : String(e));
return null;
}
}
/**
* Finds the plugin entry for a given owner/name identifier in the raw JSON data.
*/
function findPluginEntry(
rawData: Record<string, any>,
pluginId: string
): PluginEntry | undefined {
const plugins = Array.isArray(rawData?.json?.plugins) ? rawData.json.plugins : [];
for (const entry of plugins) {
if (!Array.isArray(entry) || entry.length < 2) continue;
const id = String(entry[0] ?? "");
if (id === pluginId) {
return entry[1];
}
}
return undefined;
}
/**
* Reads a single global config field value directly from the JSON persistence file.
*/
export function getGlobalConfigFieldDirect(fieldKey: string): any {
const pluginId = getSelfPluginIdentifier();
if (!pluginId) {
console.warn("[globalConfigReader] Cannot read global config: getSelfPluginIdentifier() returned null");
return undefined;
}
const rawData = readGlobalConfigsJson();
if (!rawData) return undefined;
const pluginEntry = findPluginEntry(rawData, pluginId);
if (!pluginEntry || !Array.isArray(pluginEntry.fields)) {
return undefined;
}
for (const field of pluginEntry.fields) {
if (field.key === fieldKey) {
return field.value;
}
}
return undefined;
}
/**
* Reads ALL global config fields directly from the JSON persistence file.
*/
export function getAllGlobalConfigFieldsDirect(): Map<string, any> {
const pluginId = getSelfPluginIdentifier();
if (!pluginId) {
console.warn("[globalConfigReader] Cannot read global configs: getSelfPluginIdentifier() returned null");
return new Map();
}
const rawData = readGlobalConfigsJson();
if (!rawData) return new Map();
const pluginEntry = findPluginEntry(rawData, pluginId);
if (!pluginEntry || !Array.isArray(pluginEntry.fields)) {
return new Map();
}
const map = new Map<string, any>();
for (const field of pluginEntry.fields) {
map.set(field.key, field.value);
}
return map;
}
/**
* Resolves a global config value with proper priority: JSON file wins over SDK cache.
*
* Use this in main() or early generate() where SDK values may be stale.
* For tool-call parameters, prefer direct SDK access instead (tool invocation refreshes the cache).
*
* @param fieldKey - Field key matching schematics definition
* @param sdkValue - Optional: value already read via SDK getGlobalPluginConfig().get(fieldKey)
* @returns Resolved value respecting priority rules, or undefined if both sources empty
*/
export function resolveGlobalConfigField<T = any>(
fieldKey: string,
sdkValue?: T
): T | undefined {
const jsonValue = getGlobalConfigFieldDirect(fieldKey);
// Debug logging — temporary, for development only
console.debug("[globalConfigReader] resolve:", {
field: fieldKey,
jsonHas: jsonValue !== undefined ? "yes" : "no",
sdkHas: sdkValue !== undefined ? "yes" : "no",
jsonVal: JSON.stringify(jsonValue),
sdkVal: JSON.stringify(sdkValue),
});
// Both have values → JSON wins (ground truth)
if (jsonValue !== undefined && sdkValue !== undefined) {
console.debug(`[globalConfigReader] "${fieldKey}" → JSON wins over SDK`);
return jsonValue as T;
}
// Only JSON has value
if (jsonValue !== undefined) {
console.debug(`[globalConfigReader] "${fieldKey}" → from JSON only`);
return jsonValue as T;
}
// Only SDK has value → use it
if (sdkValue !== undefined) {
console.debug(`[globalConfigReader] "${fieldKey}" → from SDK only`);
return sdkValue;
}
// Both empty → intentional default
console.debug(`[globalConfigReader] "${fieldKey}" → both empty, using schematics default`);
return undefined;
}
/**
* Batch-resolve multiple global config fields with proper priority.
* More efficient than calling resolveGlobalConfigField repeatedly since JSON is read once.
*/
export function resolveGlobalConfigFields<T extends Record<string, any>>(
sdkConfig?: { get: (key: string) => any },
fieldKeys: string[] = []
): T | null {
const directMap = getAllGlobalConfigFieldsDirect();
if (!directMap.size && !sdkConfig) {
return null;
}
const result: Record<string, any> = {};
for (const key of fieldKeys) {
const jsonValue = directMap.get(key);
const sdkValue = sdkConfig?.get(key);
if (jsonValue !== undefined && sdkValue !== undefined) {
result[key] = jsonValue; // JSON wins
} else if (jsonValue !== undefined) {
result[key] = jsonValue;
} else if (sdkValue !== undefined) {
result[key] = sdkValue;
}
}
return result as T;
}