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),
151 lines • 7.02 kB
JavaScript
// User-editable overrides for EVERY agent prompt the orchestrator controls.
//
// Design: each prompt registers its built-in DEFAULT (id + label + default text).
// The user's override is stored SEPARATELY (~/.comfyui-mcp/panel-prompts.json), so
// a default is never destroyed — an empty override or an explicit Reset always falls
// back to the built-in. This makes "edit any prompt" safe: a bad edit that breaks
// tool use is one Reset away.
//
// resolvePrompt(id, fallback) is what call-sites use — it returns the override when
// present and non-empty, else the inline default (which is also what gets registered
// for the console listing). The store imports nothing from the backends, so wiring a
// call-site (backends import THIS) never risks a circular import; defaults are
// registered centrally at startup (see registerBuiltinPrompts in index.ts).
import { EventEmitter } from "node:events";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { logger } from "../utils/logger.js";
import { assertNotWritingRealHomeInTests } from "./test-isolation-guard.js";
const REGISTRY = new Map();
const emitter = new EventEmitter();
/** Overrides file path. Overridable for tests. */
export function panelPromptsPath() {
return (process.env.COMFYUI_MCP_PANEL_PROMPTS ||
join(homedir(), ".comfyui-mcp", "panel-prompts.json"));
}
/**
* Load the overrides, distinguishing ABSENT from UNREADABLE (#796).
*
* `{}` is right for a file that is not there and WRONG for one that exists and
* could not be read, because setPromptOverride is a read-modify-write:
* `{}` + one new override, written back, erases every other prompt the user has
* written. This file holds USER-AUTHORED PROMPT TEXT — as the write guard below
* already says — so that is lost writing, not a lost setting.
*/
function readOverridesState() {
const p = panelPromptsPath();
if (!existsSync(p))
return { values: {} };
try {
const parsed = JSON.parse(readFileSync(p, "utf-8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return { values: {}, unreadable: "it is valid JSON but not an object" };
}
const out = {};
for (const [k, v] of Object.entries(parsed)) {
if (typeof v === "string")
out[k] = v;
}
return { values: out };
}
catch (err) {
logger.warn(`[prompt-overrides] could not parse ${p}: ${err instanceof Error ? err.message : String(err)}`);
return { values: {}, unreadable: err instanceof Error ? err.message : String(err) };
}
}
/** The lenient read, for QUERIES (which prompt text applies right now). An
* unreadable file means the built-in defaults apply — a wrong answer, but not a
* destructive one, and throwing here would break every prompt lookup. */
function readOverrides() {
return readOverridesState().values;
}
function writeOverrides(o) {
const p = panelPromptsPath();
// User-authored prompt text lives here — a test run writing the real file
// rewrites what the user's prompts say. Refuse at the write.
assertNotWritingRealHomeInTests(p, "the panel prompt overrides");
mkdirSync(dirname(p), { recursive: true });
writeFileSync(p, JSON.stringify(o, null, 2), "utf-8");
}
/** Register a prompt's built-in default so the editor can list + reset it. Idempotent. */
export function registerPrompt(id, label, def, help) {
REGISTRY.set(id, { id, label, default: def, help });
}
/**
* The effective prompt text for `id`: the stored override if present and non-empty,
* otherwise `fallback` (the call-site's inline default, which also seeds the registry
* default). A whitespace-only override counts as "use default".
*/
export function resolvePrompt(id, fallback) {
const ov = readOverrides()[id];
if (typeof ov === "string" && ov.trim().length > 0)
return ov;
return fallback;
}
/** Persist an override. Empty/whitespace clears it (→ back to default). Emits change. */
export function setPromptOverride(id, value) {
const { values: o, unreadable } = readOverridesState();
// The file exists and we could not read it, so `o` is EMPTY — not because the
// user has no overrides, but because we could not read the ones they wrote.
// Writing now would replace all of their prompt text with this single entry.
// This IS our file, so it is preserved and replaced (the ruling for
// ~/.claude.json is different — that one belongs to another program and is
// refused instead of moved).
if (unreadable !== undefined)
preserveUnreadableOverrides(unreadable);
if (!value || !value.trim())
delete o[id];
else
o[id] = value;
writeOverrides(o);
emitter.emit("change", id);
}
/**
* Move an unreadable overrides file aside so the write that follows cannot
* destroy it. Throws if it cannot be moved — refusing to persist is better than
* overwriting prompt text the user typed, which is the same ruling the session
* store and defaults manager make.
*/
function preserveUnreadableOverrides(reason) {
const p = panelPromptsPath();
const aside = `${p}.unreadable-${Date.now()}`;
try {
renameSync(p, aside);
logger.warn(`[prompt-overrides] preserved the unreadable overrides as ${aside} before writing a fresh ` +
`file (it could not be read: ${reason}). Any prompt text you had customised is in there.`);
}
catch (err) {
throw new Error(`Refusing to overwrite ${p}: it could not be read (${reason}) and could not be moved aside ` +
`either (${err instanceof Error ? err.message : String(err)}), so writing would destroy ` +
`prompt text that is probably still there. Fix or move that file, then set the prompt again.`);
}
}
/** Remove an override → back to the built-in default. Emits change. */
export function clearPromptOverride(id) {
const o = readOverrides();
if (id in o) {
delete o[id];
writeOverrides(o);
}
emitter.emit("change", id);
}
/** All registered prompts with their default + current override, for the editor. */
export function listPrompts() {
const ov = readOverrides();
return [...REGISTRY.values()].map((e) => {
const o = ov[e.id];
const overridden = typeof o === "string" && o.trim().length > 0;
return { id: e.id, label: e.label, help: e.help, default: e.default, override: overridden ? o : null, overridden };
});
}
/** Is `id` a prompt we know about? (Guards the write endpoint.) */
export function isKnownPrompt(id) {
return REGISTRY.has(id);
}
/** Subscribe to "a prompt override changed" (arg: the changed id). Returns unsubscribe. */
export function onPromptsChanged(cb) {
emitter.on("change", cb);
return () => emitter.off("change", cb);
}
//# sourceMappingURL=prompt-overrides.js.map