Project Files
src / tools / read_config.ts
/**
* read_config — return the current plugin settings so an agent can assist with configuration.
*
* Introspects both ConfigSchematics at runtime to discover all fields, their types,
* display names, subtitles, and real default values. No manual synchronization —
* changes to config.ts are automatically reflected here. Fields hidden by
* `engineDoesNotSupport: true` are excluded. No writes occur — this tool is read-only.
*/
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { formatToolMetaBlock } from "../core-bundle.mjs";
import { configSchematics, globalConfigSchematics } from "../config.js";
// ── Helpers ───────────────────────────────────────────────────────────────
/** Compare current value to default (handles arrays via JSON) */
function isDefault(current: unknown, def: unknown): boolean {
if (current === def) return true;
return JSON.stringify(current) === JSON.stringify(def);
}
/** Sentinel values that indicate we couldn't read the real value */
const SENTINELS = new Set(["(masked)", "(error reading)", "(config unavailable)"]);
/**
* Iterate over a ConfigSchematics' fields Map and build output entries.
* The SDK stores field metadata in a Map (insertion order = definition order).
*/
function buildEntries(cfg: any, schematics: any): Array<any> {
const entries: Array<any> = [];
for (const [key, field] of schematics.fields.entries()) {
// Skip fields hidden from the UI
if (field.valueTypeParams?.engineDoesNotSupport) continue;
let current: unknown;
if (cfg) {
try {
const raw = cfg.get(key);
// SDK returns null for masked protected fields (isProtected: true)
if (raw === null && field.defaultValue !== "") {
current = "(masked)";
} else {
current = raw;
}
} catch {
current = "(error reading)";
}
} else {
current = "(config unavailable)";
}
const isSentinel = SENTINELS.has(current as string);
const isModified: boolean | null = isSentinel ? null : !isDefault(current, field.defaultValue);
const entry: any = {
key,
type: field.valueTypeKey === "numeric" ? "number" : field.valueTypeKey,
displayName: field.valueTypeParams?.displayName ?? key,
subtitle: field.valueTypeParams?.subtitle,
current,
};
if (isModified === true) {
entry.default = field.defaultValue;
}
entry.isModified = isModified;
entries.push(entry);
}
return entries;
}
// ── Tool definition ───────────────────────────────────────────────
export function createReadConfigTool(ctl: ToolsProviderController): Tool {
return tool({
name: "read_config",
description: `Return the current plugin settings as JSON so an agent can assist with configuration.
Outputs a JSON object containing all visible config fields (in definition order). Each field is an object with:
- key: the config field name
- type: the value type (string, boolean, number, stringArray)
- displayName: human-readable label shown in the UI
- subtitle: help text shown beneath the field
- current: the live current value (full, never truncated)
- default: the actual default value (resolved at runtime — e.g., real home paths, not placeholders)
- isModified: true when current differs from default; null when the value cannot be read (masked/error)
This is a read-only tool — it does not modify any settings. All fields are introspected from the
plugin's ConfigSchematics at runtime — no manual synchronization is needed.
${formatToolMetaBlock()}`,
parameters: {},
implementation: async () => {
const ctlAny = ctl as any;
const pluginCfg = (ctlAny.getPluginConfig ?? ctlAny.getConfig)?.call(ctl, configSchematics) ?? null;
const globalCfg = (ctlAny.getGlobalPluginConfig ?? ctlAny.getGlobalConfig)?.call(ctl, globalConfigSchematics) ?? null;
const pluginEntries = buildEntries(pluginCfg, configSchematics);
const globalEntries = buildEntries(globalCfg, globalConfigSchematics);
return { settings: [...pluginEntries, ...globalEntries] };
},
});
}