src / config.ts
import { createConfigSchematics, type ToolsProviderController } from "@lmstudio/sdk";
export type PowerShellPathMode = "auto" | "manual";
export type PowerShell7Config = {
powershellPathMode: PowerShellPathMode;
manualPowerShellPath?: string;
configSource: "lm-studio-ui";
defaultTimeoutSeconds: number;
maxTimeoutSeconds: number;
stdoutMaxBytes: number;
stderrMaxBytes: number;
scriptsDirectory: string;
auditLogPath: string;
stripAnsiOutput: boolean;
prependNormalErrorView: boolean;
};
const DEFAULT_CONFIG = {
powershellPathMode: "auto",
manualPowerShellPath: undefined,
configSource: "lm-studio-ui",
defaultTimeoutSeconds: 30,
maxTimeoutSeconds: 300,
stdoutMaxBytes: 64 * 1024,
stderrMaxBytes: 64 * 1024,
scriptsDirectory: ".powershell7-scripts",
auditLogPath: ".powershell7-audit.log",
stripAnsiOutput: true,
prependNormalErrorView: true,
} satisfies PowerShell7Config;
export const configSchematics = createConfigSchematics()
.field(
"defaultTimeoutSeconds",
"numeric",
{
displayName: "Default Timeout Seconds",
subtitle: "Default timeout for PowerShell commands and scripts.",
int: true,
min: 1,
max: 300,
slider: { min: 1, max: 300, step: 1 },
},
DEFAULT_CONFIG.defaultTimeoutSeconds,
)
.field(
"maxTimeoutSeconds",
"numeric",
{
displayName: "Maximum Timeout Seconds",
subtitle: "Upper limit for tool-requested timeoutSeconds.",
int: true,
min: 1,
max: 600,
slider: { min: 1, max: 600, step: 1 },
},
DEFAULT_CONFIG.maxTimeoutSeconds,
)
.field(
"stdoutMaxBytes",
"numeric",
{
displayName: "Stdout Max Bytes",
subtitle: "Maximum stdout bytes returned to the model.",
int: true,
min: 1024,
max: 1048576,
},
DEFAULT_CONFIG.stdoutMaxBytes,
)
.field(
"stderrMaxBytes",
"numeric",
{
displayName: "Stderr Max Bytes",
subtitle: "Maximum stderr bytes returned to the model.",
int: true,
min: 1024,
max: 1048576,
},
DEFAULT_CONFIG.stderrMaxBytes,
)
.field(
"scriptsDirectory",
"string",
{
displayName: "Scripts Directory",
subtitle: "Directory for managed .ps1 files. Relative paths are inside the LM Studio working directory.",
},
DEFAULT_CONFIG.scriptsDirectory,
)
.field(
"auditLogPath",
"string",
{
displayName: "Audit Log Path",
subtitle: "Path for JSONL audit logs. Relative paths are inside the LM Studio working directory.",
},
DEFAULT_CONFIG.auditLogPath,
)
.field(
"stripAnsiOutput",
"boolean",
{
displayName: "Strip ANSI Output",
subtitle: "Remove terminal color escape sequences from stdout/stderr before returning results.",
},
DEFAULT_CONFIG.stripAnsiOutput,
)
.field(
"prependNormalErrorView",
"boolean",
{
displayName: "Use Normal PowerShell Error View",
subtitle: "Sets $ErrorView='NormalView' for cleaner errors.",
},
DEFAULT_CONFIG.prependNormalErrorView,
)
.build();
export const globalConfigSchematics = createConfigSchematics()
.field(
"powershellPathMode",
"select",
{
displayName: "PowerShell 7 Path Mode",
subtitle: "Automatic finds pwsh.exe/pwsh. Manual uses the path below.",
options: [
{ value: "auto", displayName: "Automatic" },
{ value: "manual", displayName: "Manual" },
],
},
DEFAULT_CONFIG.powershellPathMode,
)
.field(
"manualPowerShellPath",
"string",
{
displayName: "Manual PowerShell 7 Path",
subtitle: "Only used when path mode is Manual. Example: C:\\Program Files\\PowerShell\\7\\pwsh.exe",
placeholder: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
},
"",
)
.build();
export function loadRuntimeConfig(ctl: ToolsProviderController): PowerShell7Config {
const pluginConfig = ctl.getPluginConfig(configSchematics);
const globalConfig = ctl.getGlobalPluginConfig(globalConfigSchematics);
const envConfig = loadConfigFromEnv();
const maxTimeoutSeconds = clampInteger(
pluginConfig.get("maxTimeoutSeconds") ?? envConfig.maxTimeoutSeconds,
1,
600,
DEFAULT_CONFIG.maxTimeoutSeconds,
);
const defaultTimeoutSeconds = Math.min(
clampInteger(
pluginConfig.get("defaultTimeoutSeconds") ?? envConfig.defaultTimeoutSeconds,
1,
300,
DEFAULT_CONFIG.defaultTimeoutSeconds,
),
maxTimeoutSeconds,
);
return {
powershellPathMode: readPathMode(globalConfig.get("powershellPathMode") || envConfig.powershellPathMode),
manualPowerShellPath: normalizeOptionalText(globalConfig.get("manualPowerShellPath")),
configSource: "lm-studio-ui",
defaultTimeoutSeconds,
maxTimeoutSeconds,
stdoutMaxBytes: clampInteger(
pluginConfig.get("stdoutMaxBytes") ?? envConfig.stdoutMaxBytes,
1024,
1048576,
DEFAULT_CONFIG.stdoutMaxBytes,
),
stderrMaxBytes: clampInteger(
pluginConfig.get("stderrMaxBytes") ?? envConfig.stderrMaxBytes,
1024,
1048576,
DEFAULT_CONFIG.stderrMaxBytes,
),
scriptsDirectory:
normalizeOptionalText(pluginConfig.get("scriptsDirectory")) ??
envConfig.scriptsDirectory,
auditLogPath:
normalizeOptionalText(pluginConfig.get("auditLogPath")) ??
envConfig.auditLogPath,
stripAnsiOutput: pluginConfig.get("stripAnsiOutput") ?? envConfig.stripAnsiOutput,
prependNormalErrorView: pluginConfig.get("prependNormalErrorView") ?? envConfig.prependNormalErrorView,
};
}
export function loadConfigFromEnv(env: NodeJS.ProcessEnv = process.env): PowerShell7Config {
const maxTimeoutSeconds = readPositiveInteger(
env.POWERSHELL7_MAX_TIMEOUT_SECONDS,
DEFAULT_CONFIG.maxTimeoutSeconds,
);
return {
powershellPathMode: readPathMode(env.POWERSHELL7_PATH_MODE),
manualPowerShellPath: normalizeOptionalText(env.POWERSHELL7_MANUAL_PATH),
configSource: "lm-studio-ui",
defaultTimeoutSeconds: Math.min(
readPositiveInteger(
env.POWERSHELL7_DEFAULT_TIMEOUT_SECONDS,
DEFAULT_CONFIG.defaultTimeoutSeconds,
),
maxTimeoutSeconds,
),
maxTimeoutSeconds,
stdoutMaxBytes: readPositiveInteger(
env.POWERSHELL7_STDOUT_MAX_BYTES,
DEFAULT_CONFIG.stdoutMaxBytes,
),
stderrMaxBytes: readPositiveInteger(
env.POWERSHELL7_STDERR_MAX_BYTES,
DEFAULT_CONFIG.stderrMaxBytes,
),
scriptsDirectory: normalizeOptionalText(env.POWERSHELL7_SCRIPTS_DIR) ?? DEFAULT_CONFIG.scriptsDirectory,
auditLogPath: normalizeOptionalText(env.POWERSHELL7_AUDIT_LOG) ?? DEFAULT_CONFIG.auditLogPath,
stripAnsiOutput: readBoolean(env.POWERSHELL7_STRIP_ANSI_OUTPUT, DEFAULT_CONFIG.stripAnsiOutput),
prependNormalErrorView: readBoolean(
env.POWERSHELL7_PREPEND_NORMAL_ERROR_VIEW,
DEFAULT_CONFIG.prependNormalErrorView,
),
};
}
function readPathMode(value: string | undefined): PowerShellPathMode {
const normalized = value?.trim().toLowerCase();
return normalized === "manual" ? "manual" : "auto";
}
function readPositiveInteger(value: string | undefined, fallback: number): number {
if (value === undefined || value.trim() === "") {
return fallback;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 1) {
return fallback;
}
return parsed;
}
function clampInteger(value: number, min: number, max: number, fallback: number): number {
if (!Number.isFinite(value)) {
return fallback;
}
return Math.min(Math.max(Math.trunc(value), min), max);
}
function normalizeOptionalText(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function readBoolean(value: string | undefined, fallback: boolean): boolean {
const normalized = value?.trim().toLowerCase();
if (!normalized) {
return fallback;
}
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
}
return fallback;
}