UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

175 lines • 7.16 kB
import { readFile, writeFile, mkdir, rename } from "node:fs/promises"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; import { logger } from "../utils/logger.js"; const ENV_PREFIX = "COMFYUI_DEFAULT_"; function defaultConfigPath() { const xdg = process.env.XDG_CONFIG_HOME; const root = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"); return join(root, "comfyui-mcp", "config.json"); } function defaultEnv() { return process.env; } const state = { cfg: { configPath: defaultConfigPath(), env: defaultEnv() }, configValues: {}, runtimeValues: {}, /** Why the on-disk config is not being applied, when it EXISTS and failed to * load. Undefined = loaded fine, or there is no file. See configLoadError(). */ configError: undefined, }; function parseEnvValue(raw) { try { return JSON.parse(raw); } catch { return raw; } } function envDefaults(env) { const out = {}; for (const [key, val] of Object.entries(env)) { if (!key.startsWith(ENV_PREFIX) || val === undefined) continue; const name = key.slice(ENV_PREFIX.length).toLowerCase(); if (name.length === 0) continue; out[name] = parseEnvValue(val); } return out; } export const DefaultsManager = { configure(opts) { if (opts.configPath !== undefined) state.cfg.configPath = opts.configPath; if (opts.env !== undefined) state.cfg.env = opts.env; }, reset() { state.cfg = { configPath: defaultConfigPath(), env: defaultEnv() }; state.configValues = {}; state.runtimeValues = {}; state.configError = undefined; }, getConfigPath() { return state.cfg.configPath; }, async load() { state.runtimeValues = {}; state.configError = undefined; if (!existsSync(state.cfg.configPath)) { state.configValues = {}; return; } try { const raw = await readFile(state.cfg.configPath, "utf-8"); const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { state.configValues = parsed; } else { logger.warn("Defaults config file is not a JSON object, ignoring", { path: state.cfg.configPath, }); state.configValues = {}; state.configError = "it is valid JSON but not an object (an array or a bare value)"; } } catch (err) { logger.warn("Failed to parse defaults config file, ignoring", { path: state.cfg.configPath, error: err instanceof Error ? err.message : err, }); state.configValues = {}; state.configError = err instanceof Error ? err.message : String(err); } }, /** * Why the config file on disk is NOT being applied, if it is not (#796). * * A file that EXISTS and could not be loaded left `configValues` empty with * only a `logger.warn` to show for it. The user wrote that file to change what * gets generated, and the only signal that it stopped applying went to a log * they may have no access to — so their renders quietly used different * settings and `get_defaults action:"get"` reported the built-ins as though * nothing were wrong. * * Undefined means the file was loaded, or there is no file — both fine. */ configLoadError() { return state.configError; }, /** * Return the resolved view of all defaults, with source attribution. * Precedence (lowest → highest): config → env → runtime. */ getAll() { const merged = {}; for (const [k, v] of Object.entries(state.configValues)) { merged[k] = { value: v, source: "config" }; } for (const [k, v] of Object.entries(envDefaults(state.cfg.env))) { merged[k] = { value: v, source: "env" }; } for (const [k, v] of Object.entries(state.runtimeValues)) { merged[k] = { value: v, source: "runtime" }; } return merged; }, get(key) { return this.getAll()[key]; }, async set(updates, opts) { for (const [k, v] of Object.entries(updates)) { state.runtimeValues[k] = v; } if (opts?.persist) { // The file exists and we could not load it, so `state.configValues` is // EMPTY — not because the user has no defaults, but because we could not // read the ones they wrote. Merging into `{}` and writing would replace // their file with just this update, destroying hand-written content whose // only problem may be a trailing comma (#796). // // Unlike the session store, a PARSE failure is preserved here too: that // file is machine-written, this one is typed by a person, so unparseable // means "has a typo in content they care about", not "worthless bytes". if (state.configError !== undefined) { const aside = `${state.cfg.configPath}.unreadable-${Date.now()}`; try { await rename(state.cfg.configPath, aside); logger.warn(`Preserved the unloadable defaults config as ${aside} before writing a fresh one ` + `(it could not be loaded: ${state.configError}). Your previous settings are in that file.`); state.configError = undefined; // preserved — later writes are ordinary } catch (err) { throw new Error(`Refusing to overwrite ${state.cfg.configPath}: it could not be loaded ` + `(${state.configError}) and could not be moved aside either ` + `(${err instanceof Error ? err.message : String(err)}), so writing would destroy ` + `settings that are probably still there. The values you just set are active for ` + `this session; fix or move that file to persist them.`); } } const merged = { ...state.configValues, ...updates }; state.configValues = merged; const dir = dirname(state.cfg.configPath); await mkdir(dir, { recursive: true }); await writeFile(state.cfg.configPath, JSON.stringify(merged, null, 2), "utf-8"); } }, /** * Fill missing or undefined fields in `args` from resolved defaults. * Per-call args win — only undefined-or-missing keys get backfilled. */ apply(args) { const result = { ...args }; const resolved = this.getAll(); for (const [k, { value }] of Object.entries(resolved)) { if (result[k] === undefined) result[k] = value; } return result; }, }; //# sourceMappingURL=defaults-manager.js.map