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),

181 lines • 8.88 kB
import { existsSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve, sep } from "node:path"; /** Desktop's own file is a few kilobytes; anything far larger is not it. */ const MAX_INSTALLATIONS_BYTES = 4 * 1024 * 1024; /** The Desktop config directories, in the same order `config.ts` reads them. */ function desktopConfigDirs() { const home = homedir(); return [ join(home, "AppData", "Roaming", "Comfy Desktop"), // Windows join(home, "Library", "Application Support", "Comfy Desktop"), // macOS join(home, ".config", "Comfy Desktop"), // Linux ]; } /** * Path identity, tolerant of trailing separators — and of CASE only where the filesystem * is (codex). * * Folding case everywhere is wrong on Linux, where `/home/u/ComfyUI` and `/home/u/comfyui` * are two different installs: the drift sentence would be attributed to the wrong one and * name a flag the user never put there. Windows is case-insensitive, and it is the * platform whose config layout this file is mostly about, so the fold is scoped to it. * * macOS is usually — not always — case-insensitive, and guessing is not worth it: a * mismatch there DECLINES, which costs a sentence rather than printing a wrong one. */ function samePath(a, b) { const fold = process.platform === "win32"; const norm = (p) => { const resolved = resolve(p.trim()).replace(new RegExp(`\\${sep}+$`), ""); return fold ? resolved.toLowerCase() : resolved; }; try { return norm(a) === norm(b); } catch { return false; } } /** * Split the saved string into tokens the way a shell would, honouring quotes. * * `--extra-model-paths-config "C:\my path\x.yaml"` splits on whitespace into three * fragments, none of which appears in argv — so a naive split reports a change that IS in * force as missing, which is this issue's own defect pointed at the user. */ function splitLaunchArgs(raw) { const out = []; const re = /"([^"]*)"|'([^']*)'|(\S+)/g; let m; while ((m = re.exec(raw)) !== null) { const tok = m[1] ?? m[2] ?? m[3]; if (tok) out.push(tok); } return out; } /** * Does this Desktop record describe the install we are running? * * Desktop's `installPath` is the install ROOT, and for its own installer layout `main.py` * sits one level down at `<installPath>/ComfyUI` — the same wrapper shape `config.ts` * heals when it detects installs. Our resolved path is the directory that HOLDS main.py, * so the two legitimately differ by that one segment. Comparing them naively finds nothing * on exactly the Desktop installs this feature exists for, and a silent no-match is * indistinguishable here from "no drift" — the sentence would simply never appear. */ function identifiesInstall(recorded, ours) { return samePath(recorded, ours) || samePath(join(recorded, "ComfyUI"), ours); } /** * The saved launch arguments for the install at `installPath`, or undefined when the * question could not be answered. * * UNDEFINED IS "NOT ESTABLISHED", NOT "NONE" (#796). No file, unreadable file, no entry * for this path, two entries claiming it, or an entry with no `launchArgs` key are all * cases where the caller must stay quiet rather than report an empty set — "Desktop saves * no arguments for this install" is a claim, and a missing file does not support it. An * entry that explicitly saves an EMPTY string is different: that is a real answer, and it * comes back as an empty array. */ export function desktopSavedLaunchArgs(installPath) { if (!installPath?.trim()) return undefined; const matches = []; for (const dir of desktopConfigDirs()) { let parsed; try { const file = join(dir, "installations.json"); if (!existsSync(file)) continue; // A SIZE BOUND (codex P3). This runs while composing the restart REPORT, and the // answer is worth one small local JSON read — not an unbounded slurp of whatever // happens to sit at that name. It does not make the read non-blocking, which is the // honest residual: a redirected roaming profile on a slow share can still cost this // report a moment. The restart itself has already happened by then, and this path // already makes synchronous process-table and network probes. if (statSync(file).size > MAX_INSTALLATIONS_BYTES) continue; parsed = JSON.parse(readFileSync(file, "utf-8")); } catch { // Unreadable or malformed: nothing established. continue; } if (!Array.isArray(parsed)) continue; for (const inst of parsed) { if (!inst || typeof inst !== "object") continue; const rec = inst; // A remote entry describes a URL, not a local process — its saved arguments say // nothing about the server running here. if (rec.sourceId === "remote" || rec.sourceId === "cloud") continue; if (typeof rec.installPath !== "string" || !identifiesInstall(rec.installPath, installPath)) continue; // Desktop writes ONE STRING. An array is accepted too rather than assumed away: // the shape is theirs to change, and a silent mis-read here would produce a // confident sentence about arguments nobody saved. const raw = rec.launchArgs; const args = typeof raw === "string" ? splitLaunchArgs(raw) : Array.isArray(raw) && raw.every((t) => typeof t === "string") ? raw.filter(Boolean) : undefined; if (args === undefined) continue; matches.push({ name: typeof rec.name === "string" && rec.name.trim() ? rec.name : installPath, args, }); } } // AMBIGUITY DECLINES. Two entries claiming the same path cannot both be the install // whose settings the user edited, and picking one would put a specific, checkable // sentence in front of them that may be about the other. return matches.length === 1 ? matches[0] : undefined; } /** The flag NAME of a token: `--port=8000` and `--port` are the same instruction, and a * value token has no flag name at all. */ function flagName(token) { return token.startsWith("-") ? token.split("=")[0] : ""; } /** * The sentence naming saved arguments the running server does not have (#848). * * Returns "" when there is nothing established to say — no saved settings, no running * argv, or nothing missing. Silence is the correct output for all three: the caller * already prints the general "if you were expecting different arguments" remedy, and this * only ever REPLACES a conditional with an observation when one is available. * * ONLY ABSENCE IS REPORTED, never the reverse. A running server carrying arguments the * saved settings do not mention is ordinary — Desktop adds its own — so that direction is * not a finding. And the sentence says the tokens are absent, not that the restart * dropped them or that Desktop ignored them: neither is observed here. */ export function describeSavedLaunchArgDrift(saved, runningArgv) { if (!saved || !saved.args.length || !runningArgv?.length) return ""; // COMPARE FLAGS, NOT TOKENS. The saved settings are one string and argv is already // split, so the same instruction is spelled several ways: `--port 8000` against // `--port=8000`, a quoted path against its unquoted argv form. Token equality called all // of those "not in force" — a confident, checkable, WRONG sentence sending the user to // restart Desktop for nothing, which is this issue's defect pointed back at them. // // Only FLAGS are reported, and only by name. A bare value is an argument to a flag: its // formatting varies, and it says nothing on its own. The reported case is a whole flag // that is absent, which is exactly what #848 was about. const runningFlags = new Set(runningArgv.map(flagName).filter(Boolean)); const missing = saved.args .filter((tok) => tok.startsWith("-")) .filter((tok) => !runningFlags.has(flagName(tok))); if (!missing.length) return ""; return (` ComfyUI Desktop's saved launch settings for "${saved.name}" include ` + `${missing.join(" ")}, which the running server's arguments do NOT contain — so that ` + `change is not in effect. It is applied when Desktop itself spawns the server: fully ` + `quit the ComfyUI Desktop app and relaunch it.`); } //# sourceMappingURL=desktop-launch-args.js.map