src / mcp / config.ts
src / mcp / config.ts
import { optionalEnv, resolveMadeForEnv, type McpResultPreset } from "./core-bundle.mjs";
import { getLogsDir, getPluginLogFilename } from "../core-bundle.mjs";
import { type VisionApiName } from "../services/VisionAnalyzer.js";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Matches toolsProvider.ts's localTimestamp() exactly, so entries from both
// entrypoints read consistently in the shared plugin log file.
function localTimestamp(): string {
try {
return new Date().toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
} catch {
return new Date().toString();
}
}
/**
* Mirrors analyse-image/src/config.ts's globalConfigSchematics fields.
*/
export type McpConfig = {
chatWorkingDirectories: string;
/** The preset resolved from MCP_MADE_FOR_BIONIC (primary, documented) or MCP_MADE_FOR (documented alternate selector for the unsloth/generic presets) — see made-for-bionic-core's presets/index.ts. */
preset: McpResultPreset;
visionApiBaseUrl: string;
visionApiKey: string;
qwen3VlModel: string;
embedPngMetadata: boolean;
includeGenerationMetadata: boolean;
/** Path to Unsloth Studio's local chat database (only read by the unsloth preset's beforeToolCall hook). */
clientDbLocation: string;
/** Which adapter's model-loading methods to use (see VisionApiName) — defaults per PRESET_DEFAULTS.visionApi (NOT always the preset's own name), overridable via VISION_API for a server that doesn't match the usual API shape. */
visionApi: VisionApiName;
/** Path to the llama-server binary used by the VISION_API="llama-server" strand (only read by that adapter). */
llamaServerBinary: string;
/** Local port the VISION_API="llama-server" strand's own standalone router listens on -- independent of visionApiBaseUrl (mirrors src/config.ts's llamaServerPort default). */
llamaServerPort: number;
/** --ctx-size applied to every model the VISION_API="llama-server" strand's router auto-loads (mirrors src/config.ts's llamaServerCtxSize default). */
llamaServerCtxSize: number;
/** --models-max passed to the VISION_API="llama-server" strand's router server (mirrors src/config.ts's llamaServerModelsMax default). */
llamaServerModelsMax: number;
/** Idle-shutdown TTL in minutes for the VISION_API="llama-server" strand's router server, 0 disables it (mirrors src/config.ts's llamaServerIdleTtlMinutes default). */
llamaServerIdleTtlMinutes: number;
/** Port of the local HTTP file server used to build http:// preview links. Same unified per-preset scheme as every other bridge (generate-image, process-image): bionic/unsloth/generic each get their own fixed default so they can run side by side without colliding. */
httpServerPort: number;
};
/**
* One self-contained defaults record per adapter (bionic/generic/unsloth) — no per-field
* if/else-on-preset.name branching scattered across separate functions. Each adapter owns its
* own scratchpad root, Vision API server default, model key default, and default VISION_API
* strand (NOT always its own name — generic/unsloth default to "llama-server" since their native
* load-then-verify strategies proved unreliable in practice, see
* made-for-bionic/planning/made-for-preset-rollout-plan.md). Independently overridable per call
* via VISION_API — see resolveVisionApi() below.
*/
const PRESET_DEFAULTS: Record<
McpResultPreset["name"],
{ chatWorkingDirectories: string; visionApiBaseUrl: string; qwen3VlModel: string; llamaServerBinary: string; visionApi: VisionApiName }
> = {
bionic: {
chatWorkingDirectories: path.join(os.homedir(), ".lmstudio", "scratchpads"),
visionApiBaseUrl: "http://127.0.0.1:1234/v1",
qwen3VlModel: "qwen/qwen3-vl-8b",
llamaServerBinary: path.join(os.homedir(), ".lmstudio", "extensions", "backends", "llama.cpp-mac-arm64-apple-metal-advsimd-2.48.0", "llama-server"),
visionApi: "bionic",
},
generic: {
chatWorkingDirectories: path.join(os.homedir(), "Pictures"),
visionApiBaseUrl: "http://127.0.0.1:1234/v1",
qwen3VlModel: "qwen/qwen3-vl-8b",
llamaServerBinary: path.join(os.homedir(), ".lmstudio", "extensions", "backends", "qwen3-vl-embedding", "llama-server"),
visionApi: "llama-server",
},
unsloth: {
chatWorkingDirectories: path.join(os.homedir(), ".unsloth", "studio", "sandbox"),
visionApiBaseUrl: "http://127.0.0.1:8888/v1",
qwen3VlModel: "unsloth/Qwen3-VL-8B-Instruct-GGUF",
llamaServerBinary: path.join(os.homedir(), ".unsloth", "llama.cpp", "llama-server"),
visionApi: "llama-server",
},
};
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
if (value === undefined) return fallback;
return /^(1|true|yes)$/i.test(value.trim());
}
function parseNumber(value: string | undefined, fallback: number): number {
if (value === undefined) return fallback;
const parsed = Number(value.trim());
return Number.isFinite(parsed) ? parsed : fallback;
}
function parsePort(name: string, value: string | undefined, fallback: number): number {
if (value === undefined) return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive integer, got "${value}".`);
}
return parsed;
}
/** Same unified scheme as every other bridge (generate-image, process-image): each preset gets its own fixed port so bionic/unsloth/generic MCP servers can run side by side without colliding. */
function defaultHttpServerPort(preset: McpResultPreset): number {
if (preset.name === "unsloth") return 54780;
if (preset.name === "generic") return 54790;
return 54770;
}
/** Not preset-dependent -- same default for bionic/generic/unsloth, see src/config.ts's matching llamaServer* defaults. */
const LLAMA_SERVER_PORT_DEFAULT = 8099;
const LLAMA_SERVER_CTX_SIZE_DEFAULT = 8192;
/** analyse-image only ever requests 1 model through the router at a time -- kept adjustable (not hardcoded to 1) for reuse elsewhere, should the need for >1 concurrent model ever arise. */
const LLAMA_SERVER_MODELS_MAX_DEFAULT = 1;
const LLAMA_SERVER_IDLE_TTL_MINUTES_DEFAULT = 10;
/** Every env var readMcpConfig() reads — used to log a complete, unfiltered snapshot. */
const MCP_ENV_VAR_NAMES = [
"CHAT_WORKING_DIRECTORIES",
"MCP_MADE_FOR_BIONIC", // legacy, documented selector (see README_MCP.md)
"MCP_MADE_FOR", // documented alternate selector for the unsloth/generic presets (see README_MCP.md)
"VISION_API", // overrides which adapter's model-loading methods to use; defaults to MCP_MADE_FOR's preset name
"VISION_API_BASE_URL",
"VISION_API_KEY",
"QWEN3_VL_MODEL",
"LLAMA_SERVER_BINARY", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_PORT", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_CTX_SIZE", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_MODELS_MAX", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_IDLE_TTL_MINUTES", // only read by the VISION_API="llama-server" adapter
"EMBED_PNG_METADATA",
"INCLUDE_GENERATION_METADATA",
"CLIENT_DB_LOCATION", // only read by the unsloth preset's beforeToolCall hook
"HTTP_SERVER_PORT",
] as const;
/**
* Raw process.env values for every var readMcpConfig() reads, exactly as received —
* unset stays `null` (never silently coalesced to a default), set-but-empty stays `""`.
* Callers should log this BEFORE readMcpConfig() so a required-env failure still leaves
* a full record of what was actually passed in.
*/
export function getRawEnvSnapshot(): Record<(typeof MCP_ENV_VAR_NAMES)[number], string | null> {
const snapshot = {} as Record<(typeof MCP_ENV_VAR_NAMES)[number], string | null>;
for (const name of MCP_ENV_VAR_NAMES) {
snapshot[name] = process.env[name] ?? null;
}
return snapshot;
}
const VISION_API_NAMES: readonly VisionApiName[] = ["bionic", "generic", "unsloth", "llama-server"];
/** VISION_API overrides which adapter's model-loading methods to use; defaults to each preset's own PRESET_DEFAULTS.visionApi (NOT always preset.name — generic/unsloth now default to "llama-server", see PRESET_DEFAULTS). Falls back to the same default on an unset or unrecognized value — never throws. */
function resolveVisionApi(preset: McpResultPreset, raw: string | undefined): VisionApiName {
const trimmed = (raw ?? "").trim().toLowerCase();
return (VISION_API_NAMES as readonly string[]).includes(trimmed) ? (trimmed as VisionApiName) : PRESET_DEFAULTS[preset.name].visionApi;
}
export function readMcpConfig(): McpConfig {
const preset = resolveMadeForEnv(optionalEnv("MCP_MADE_FOR"), optionalEnv("MCP_MADE_FOR_BIONIC"));
const defaults = PRESET_DEFAULTS[preset.name];
return {
chatWorkingDirectories: optionalEnv("CHAT_WORKING_DIRECTORIES") ?? defaults.chatWorkingDirectories,
preset,
visionApiBaseUrl: optionalEnv("VISION_API_BASE_URL") ?? defaults.visionApiBaseUrl,
visionApiKey: optionalEnv("VISION_API_KEY") ?? "",
qwen3VlModel: optionalEnv("QWEN3_VL_MODEL") ?? defaults.qwen3VlModel,
embedPngMetadata: parseBoolean(optionalEnv("EMBED_PNG_METADATA"), true),
includeGenerationMetadata: parseBoolean(optionalEnv("INCLUDE_GENERATION_METADATA"), true),
clientDbLocation: optionalEnv("CLIENT_DB_LOCATION") ?? path.join(os.homedir(), ".unsloth", "studio", "studio.db"),
visionApi: resolveVisionApi(preset, optionalEnv("VISION_API")),
llamaServerBinary: optionalEnv("LLAMA_SERVER_BINARY") ?? defaults.llamaServerBinary,
llamaServerPort: parseNumber(optionalEnv("LLAMA_SERVER_PORT"), LLAMA_SERVER_PORT_DEFAULT),
llamaServerCtxSize: parseNumber(optionalEnv("LLAMA_SERVER_CTX_SIZE"), LLAMA_SERVER_CTX_SIZE_DEFAULT),
llamaServerModelsMax: parseNumber(optionalEnv("LLAMA_SERVER_MODELS_MAX"), LLAMA_SERVER_MODELS_MAX_DEFAULT),
llamaServerIdleTtlMinutes: parseNumber(optionalEnv("LLAMA_SERVER_IDLE_TTL_MINUTES"), LLAMA_SERVER_IDLE_TTL_MINUTES_DEFAULT),
httpServerPort: parsePort("HTTP_SERVER_PORT", optionalEnv("HTTP_SERVER_PORT"), defaultHttpServerPort(preset)),
};
}
/** Sets the process.env values core/tools.ts reads directly (see handleAnalyseImage/handleDetectObject/handleAnnotateImage). */
export function applyMcpConfig(config: McpConfig): void {
process.env.LMSTUDIO_VISION_API_BASE_URL = config.visionApiBaseUrl;
process.env.LMSTUDIO_VISION_API_KEY = config.visionApiKey;
process.env.LMSTUDIO_VISION_MODEL_KEY = config.qwen3VlModel;
process.env.EMBED_PNG_METADATA = config.embedPngMetadata ? "true" : "false";
process.env.INCLUDE_GENERATION_METADATA = config.includeGenerationMetadata ? "true" : "false";
process.env.LMSTUDIO_VISION_API = config.visionApi;
process.env.LMSTUDIO_LLAMA_SERVER_BINARY = config.llamaServerBinary;
process.env.LMSTUDIO_LLAMA_SERVER_PORT = String(config.llamaServerPort);
process.env.LMSTUDIO_LLAMA_SERVER_CTX_SIZE = String(config.llamaServerCtxSize);
process.env.LMSTUDIO_LLAMA_SERVER_MODELS_MAX = String(config.llamaServerModelsMax);
process.env.LMSTUDIO_LLAMA_SERVER_IDLE_TTL_MINUTES = String(config.llamaServerIdleTtlMinutes);
process.env.HTTP_SERVER_PORT = String(config.httpServerPort);
}
export function logPreflight(line: string): void {
try {
const logsDir = getLogsDir();
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
fs.appendFileSync(path.join(logsDir, getPluginLogFilename()), `${localTimestamp()} - ${line}\n`);
} catch {
// Never let logging itself break startup.
}
}
import { optionalEnv, resolveMadeForEnv, type McpResultPreset } from "./core-bundle.mjs";
import { getLogsDir, getPluginLogFilename } from "../core-bundle.mjs";
import { type VisionApiName } from "../services/VisionAnalyzer.js";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Matches toolsProvider.ts's localTimestamp() exactly, so entries from both
// entrypoints read consistently in the shared plugin log file.
function localTimestamp(): string {
try {
return new Date().toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
} catch {
return new Date().toString();
}
}
/**
* Mirrors analyse-image/src/config.ts's globalConfigSchematics fields.
*/
export type McpConfig = {
chatWorkingDirectories: string;
/** The preset resolved from MCP_MADE_FOR_BIONIC (primary, documented) or MCP_MADE_FOR (documented alternate selector for the unsloth/generic presets) — see made-for-bionic-core's presets/index.ts. */
preset: McpResultPreset;
visionApiBaseUrl: string;
visionApiKey: string;
qwen3VlModel: string;
embedPngMetadata: boolean;
includeGenerationMetadata: boolean;
/** Path to Unsloth Studio's local chat database (only read by the unsloth preset's beforeToolCall hook). */
clientDbLocation: string;
/** Which adapter's model-loading methods to use (see VisionApiName) — defaults per PRESET_DEFAULTS.visionApi (NOT always the preset's own name), overridable via VISION_API for a server that doesn't match the usual API shape. */
visionApi: VisionApiName;
/** Path to the llama-server binary used by the VISION_API="llama-server" strand (only read by that adapter). */
llamaServerBinary: string;
/** Local port the VISION_API="llama-server" strand's own standalone router listens on -- independent of visionApiBaseUrl (mirrors src/config.ts's llamaServerPort default). */
llamaServerPort: number;
/** --ctx-size applied to every model the VISION_API="llama-server" strand's router auto-loads (mirrors src/config.ts's llamaServerCtxSize default). */
llamaServerCtxSize: number;
/** --models-max passed to the VISION_API="llama-server" strand's router server (mirrors src/config.ts's llamaServerModelsMax default). */
llamaServerModelsMax: number;
/** Idle-shutdown TTL in minutes for the VISION_API="llama-server" strand's router server, 0 disables it (mirrors src/config.ts's llamaServerIdleTtlMinutes default). */
llamaServerIdleTtlMinutes: number;
/** Port of the local HTTP file server used to build http:// preview links. Same unified per-preset scheme as every other bridge (generate-image, process-image): bionic/unsloth/generic each get their own fixed default so they can run side by side without colliding. */
httpServerPort: number;
};
/**
* One self-contained defaults record per adapter (bionic/generic/unsloth) — no per-field
* if/else-on-preset.name branching scattered across separate functions. Each adapter owns its
* own scratchpad root, Vision API server default, model key default, and default VISION_API
* strand (NOT always its own name — generic/unsloth default to "llama-server" since their native
* load-then-verify strategies proved unreliable in practice, see
* made-for-bionic/planning/made-for-preset-rollout-plan.md). Independently overridable per call
* via VISION_API — see resolveVisionApi() below.
*/
const PRESET_DEFAULTS: Record<
McpResultPreset["name"],
{ chatWorkingDirectories: string; visionApiBaseUrl: string; qwen3VlModel: string; llamaServerBinary: string; visionApi: VisionApiName }
> = {
bionic: {
chatWorkingDirectories: path.join(os.homedir(), ".lmstudio", "scratchpads"),
visionApiBaseUrl: "http://127.0.0.1:1234/v1",
qwen3VlModel: "qwen/qwen3-vl-8b",
llamaServerBinary: path.join(os.homedir(), ".lmstudio", "extensions", "backends", "llama.cpp-mac-arm64-apple-metal-advsimd-2.48.0", "llama-server"),
visionApi: "bionic",
},
generic: {
chatWorkingDirectories: path.join(os.homedir(), "Pictures"),
visionApiBaseUrl: "http://127.0.0.1:1234/v1",
qwen3VlModel: "qwen/qwen3-vl-8b",
llamaServerBinary: path.join(os.homedir(), ".lmstudio", "extensions", "backends", "qwen3-vl-embedding", "llama-server"),
visionApi: "llama-server",
},
unsloth: {
chatWorkingDirectories: path.join(os.homedir(), ".unsloth", "studio", "sandbox"),
visionApiBaseUrl: "http://127.0.0.1:8888/v1",
qwen3VlModel: "unsloth/Qwen3-VL-8B-Instruct-GGUF",
llamaServerBinary: path.join(os.homedir(), ".unsloth", "llama.cpp", "llama-server"),
visionApi: "llama-server",
},
};
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
if (value === undefined) return fallback;
return /^(1|true|yes)$/i.test(value.trim());
}
function parseNumber(value: string | undefined, fallback: number): number {
if (value === undefined) return fallback;
const parsed = Number(value.trim());
return Number.isFinite(parsed) ? parsed : fallback;
}
function parsePort(name: string, value: string | undefined, fallback: number): number {
if (value === undefined) return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive integer, got "${value}".`);
}
return parsed;
}
/** Same unified scheme as every other bridge (generate-image, process-image): each preset gets its own fixed port so bionic/unsloth/generic MCP servers can run side by side without colliding. */
function defaultHttpServerPort(preset: McpResultPreset): number {
if (preset.name === "unsloth") return 54780;
if (preset.name === "generic") return 54790;
return 54770;
}
/** Not preset-dependent -- same default for bionic/generic/unsloth, see src/config.ts's matching llamaServer* defaults. */
const LLAMA_SERVER_PORT_DEFAULT = 8099;
const LLAMA_SERVER_CTX_SIZE_DEFAULT = 8192;
/** analyse-image only ever requests 1 model through the router at a time -- kept adjustable (not hardcoded to 1) for reuse elsewhere, should the need for >1 concurrent model ever arise. */
const LLAMA_SERVER_MODELS_MAX_DEFAULT = 1;
const LLAMA_SERVER_IDLE_TTL_MINUTES_DEFAULT = 10;
/** Every env var readMcpConfig() reads — used to log a complete, unfiltered snapshot. */
const MCP_ENV_VAR_NAMES = [
"CHAT_WORKING_DIRECTORIES",
"MCP_MADE_FOR_BIONIC", // legacy, documented selector (see README_MCP.md)
"MCP_MADE_FOR", // documented alternate selector for the unsloth/generic presets (see README_MCP.md)
"VISION_API", // overrides which adapter's model-loading methods to use; defaults to MCP_MADE_FOR's preset name
"VISION_API_BASE_URL",
"VISION_API_KEY",
"QWEN3_VL_MODEL",
"LLAMA_SERVER_BINARY", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_PORT", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_CTX_SIZE", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_MODELS_MAX", // only read by the VISION_API="llama-server" adapter
"LLAMA_SERVER_IDLE_TTL_MINUTES", // only read by the VISION_API="llama-server" adapter
"EMBED_PNG_METADATA",
"INCLUDE_GENERATION_METADATA",
"CLIENT_DB_LOCATION", // only read by the unsloth preset's beforeToolCall hook
"HTTP_SERVER_PORT",
] as const;
/**
* Raw process.env values for every var readMcpConfig() reads, exactly as received —
* unset stays `null` (never silently coalesced to a default), set-but-empty stays `""`.
* Callers should log this BEFORE readMcpConfig() so a required-env failure still leaves
* a full record of what was actually passed in.
*/
export function getRawEnvSnapshot(): Record<(typeof MCP_ENV_VAR_NAMES)[number], string | null> {
const snapshot = {} as Record<(typeof MCP_ENV_VAR_NAMES)[number], string | null>;
for (const name of MCP_ENV_VAR_NAMES) {
snapshot[name] = process.env[name] ?? null;
}
return snapshot;
}
const VISION_API_NAMES: readonly VisionApiName[] = ["bionic", "generic", "unsloth", "llama-server"];
/** VISION_API overrides which adapter's model-loading methods to use; defaults to each preset's own PRESET_DEFAULTS.visionApi (NOT always preset.name — generic/unsloth now default to "llama-server", see PRESET_DEFAULTS). Falls back to the same default on an unset or unrecognized value — never throws. */
function resolveVisionApi(preset: McpResultPreset, raw: string | undefined): VisionApiName {
const trimmed = (raw ?? "").trim().toLowerCase();
return (VISION_API_NAMES as readonly string[]).includes(trimmed) ? (trimmed as VisionApiName) : PRESET_DEFAULTS[preset.name].visionApi;
}
export function readMcpConfig(): McpConfig {
const preset = resolveMadeForEnv(optionalEnv("MCP_MADE_FOR"), optionalEnv("MCP_MADE_FOR_BIONIC"));
const defaults = PRESET_DEFAULTS[preset.name];
return {
chatWorkingDirectories: optionalEnv("CHAT_WORKING_DIRECTORIES") ?? defaults.chatWorkingDirectories,
preset,
visionApiBaseUrl: optionalEnv("VISION_API_BASE_URL") ?? defaults.visionApiBaseUrl,
visionApiKey: optionalEnv("VISION_API_KEY") ?? "",
qwen3VlModel: optionalEnv("QWEN3_VL_MODEL") ?? defaults.qwen3VlModel,
embedPngMetadata: parseBoolean(optionalEnv("EMBED_PNG_METADATA"), true),
includeGenerationMetadata: parseBoolean(optionalEnv("INCLUDE_GENERATION_METADATA"), true),
clientDbLocation: optionalEnv("CLIENT_DB_LOCATION") ?? path.join(os.homedir(), ".unsloth", "studio", "studio.db"),
visionApi: resolveVisionApi(preset, optionalEnv("VISION_API")),
llamaServerBinary: optionalEnv("LLAMA_SERVER_BINARY") ?? defaults.llamaServerBinary,
llamaServerPort: parseNumber(optionalEnv("LLAMA_SERVER_PORT"), LLAMA_SERVER_PORT_DEFAULT),
llamaServerCtxSize: parseNumber(optionalEnv("LLAMA_SERVER_CTX_SIZE"), LLAMA_SERVER_CTX_SIZE_DEFAULT),
llamaServerModelsMax: parseNumber(optionalEnv("LLAMA_SERVER_MODELS_MAX"), LLAMA_SERVER_MODELS_MAX_DEFAULT),
llamaServerIdleTtlMinutes: parseNumber(optionalEnv("LLAMA_SERVER_IDLE_TTL_MINUTES"), LLAMA_SERVER_IDLE_TTL_MINUTES_DEFAULT),
httpServerPort: parsePort("HTTP_SERVER_PORT", optionalEnv("HTTP_SERVER_PORT"), defaultHttpServerPort(preset)),
};
}
/** Sets the process.env values core/tools.ts reads directly (see handleAnalyseImage/handleDetectObject/handleAnnotateImage). */
export function applyMcpConfig(config: McpConfig): void {
process.env.LMSTUDIO_VISION_API_BASE_URL = config.visionApiBaseUrl;
process.env.LMSTUDIO_VISION_API_KEY = config.visionApiKey;
process.env.LMSTUDIO_VISION_MODEL_KEY = config.qwen3VlModel;
process.env.EMBED_PNG_METADATA = config.embedPngMetadata ? "true" : "false";
process.env.INCLUDE_GENERATION_METADATA = config.includeGenerationMetadata ? "true" : "false";
process.env.LMSTUDIO_VISION_API = config.visionApi;
process.env.LMSTUDIO_LLAMA_SERVER_BINARY = config.llamaServerBinary;
process.env.LMSTUDIO_LLAMA_SERVER_PORT = String(config.llamaServerPort);
process.env.LMSTUDIO_LLAMA_SERVER_CTX_SIZE = String(config.llamaServerCtxSize);
process.env.LMSTUDIO_LLAMA_SERVER_MODELS_MAX = String(config.llamaServerModelsMax);
process.env.LMSTUDIO_LLAMA_SERVER_IDLE_TTL_MINUTES = String(config.llamaServerIdleTtlMinutes);
process.env.HTTP_SERVER_PORT = String(config.httpServerPort);
}
export function logPreflight(line: string): void {
try {
const logsDir = getLogsDir();
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
fs.appendFileSync(path.join(logsDir, getPluginLogFilename()), `${localTimestamp()} - ${line}\n`);
} catch {
// Never let logging itself break startup.
}
}