comfyui-mcp 0.24.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -574,6 +574,8 @@ npx -y comfyui-mcp@latest --comfyui-url http://localhost:8188 --force-remote
574
574
  | `COMFYUI_PORT` | *(auto-detect)* | ComfyUI server port (tries 8188, then 8000) |
575
575
  | `COMFYUI_PATH` | *(auto-detect)* | Path to ComfyUI data directory. Auto-detection suppressed in remote/cloud modes. |
576
576
  | `COMFYUI_PYTHON` | `python` | Python interpreter for cm-cli subprocess operations (`useCmCli`, `sync_node_dependencies`) — point it at your ComfyUI venv's python. cm-cli needs the local filesystem, so these are **local-mode only**; on remote targets use the Manager HTTP path (the default). |
577
+ | `COMFYUI_MCP_BRIDGE_HOST` | `127.0.0.1` | Panel-bridge bind host. Set `0.0.0.0` (or a LAN IP) to run the orchestrator on a 24/7 server and connect panels from other machines — **requires a token** (below); the orchestrator prints a ready-to-paste `ws://…/?token=…` Bridge URL. |
578
+ | `COMFYUI_MCP_BRIDGE_TOKEN` | *(generated when needed)* | Shared secret gating every bridge connection (checked constant-time on the WS upgrade). Mandatory for a non-loopback `COMFYUI_MCP_BRIDGE_HOST`; pin it so the Bridge URL survives restarts. Never logged beyond the startup banner. |
577
579
  | `COMFYUI_MCP_DATA_DIR` | `~/.comfyui-mcp` | Base dir for per-instance data (the `generations.db` used by `suggest_settings`) when there's no local `COMFYUI_PATH` (remote/cloud/undetected). Scoped per target under `instances/<host_port>/`. |
578
580
  | `COMFYUI_API_KEY` | | Comfy Cloud API key. When set, **cloud mode** is active and the server talks to `cloud.comfy.org`. Never logged. |
579
581
  | `COMFYUI_CLOUD_URL` | `https://cloud.comfy.org` | Override the Comfy Cloud endpoint (testing/staging). |
@@ -90,6 +90,13 @@ export function backendReadiness(backend, opts) {
90
90
  const cli = ollamaInstalled(home);
91
91
  return { backend: "ollama", cli, auth: cli ? true : null, ready: cli };
92
92
  }
93
+ if (b === "openrouter") {
94
+ // Hosted — no CLI. Readiness = an OpenRouter API key in the orchestrator's
95
+ // env (OPENROUTER_API_KEY, or the shared COMFYUI_MCP_OLLAMA_API_KEY). A bad
96
+ // key still surfaces via the connect ack's model probe (degraded).
97
+ const key = !!(process.env.OPENROUTER_API_KEY || process.env.COMFYUI_MCP_OLLAMA_API_KEY);
98
+ return { backend: "openrouter", cli: key, auth: key ? true : false, ready: key };
99
+ }
93
100
  return { backend: b, cli: false, auth: false, ready: false };
94
101
  }
95
102
  /** Readiness for every known backend, plus a rolled-up any_ready. */
@@ -1 +1 @@
1
- {"version":3,"file":"backend-readiness.js","sourceRoot":"","sources":["../../src/orchestrator/backend-readiness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend-readiness.js","sourceRoot":"","sources":["../../src/orchestrator/backend-readiness.ts"],"names":[],"mappings":"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"}
@@ -10,12 +10,12 @@
10
10
  // key. See docs/design/panel-orchestrator.md.
11
11
  import { existsSync, writeFileSync, unlinkSync, readFileSync, readdirSync } from "node:fs";
12
12
  import { execFileSync } from "node:child_process";
13
- import { tmpdir } from "node:os";
13
+ import { tmpdir, networkInterfaces } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { randomBytes } from "node:crypto";
17
17
  import readline from "node:readline";
18
- import { startUiBridge } from "../services/ui-bridge.js";
18
+ import { startUiBridge, isLoopbackBindHost } from "../services/ui-bridge.js";
19
19
  import { setupSecureBridge } from "../services/secure-bridge.js";
20
20
  import { detectInstallMode } from "../services/self-update.js";
21
21
  import { SessionStore } from "./session-store.js";
@@ -24,7 +24,7 @@ import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffo
24
24
  import { createPanelMcpServer } from "./panel-tools.js";
25
25
  import { readUserMcpServers } from "../services/user-mcp-config.js";
26
26
  import { isForceRemoteFlagSet, isLoopbackHost } from "../config.js";
27
- import { buildComfyuiMcpEnv, comfyuiSecretKeys, onComfyuiSecretsChanged, } from "../services/panel-secrets.js";
27
+ import { buildComfyuiMcpEnv, comfyuiSecretKeys, onComfyuiSecretsChanged, hydrateAgentSecretsIntoEnv, onAgentSecretsChanged, setAgentSecret, } from "../services/panel-secrets.js";
28
28
  import { CodexBackend } from "./codex-backend.js";
29
29
  import { GeminiBackend, GEMINI_DEFAULT_MODEL } from "./gemini-backend.js";
30
30
  import { OllamaBackend } from "./ollama-backend.js";
@@ -32,6 +32,7 @@ import { allBackendReadiness } from "./backend-readiness.js";
32
32
  import { startPanelMcpHttpServer } from "./panel-mcp-http.js";
33
33
  import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
34
34
  import { QueueMonitor } from "../services/queue-monitor.js";
35
+ import { getAgentSettings, setAgentSettings } from "../services/panel-settings.js";
35
36
  import { gatherEnvCapabilities, buildPanelSystemAppend, } from "../services/env-capabilities.js";
36
37
  const PANEL_SYSTEM_APPEND = `You are the autonomous assistant embedded directly in a ComfyUI sidebar panel. The person is working in ComfyUI and talks to you through that panel: their messages arrive as your prompts, and everything you write is shown to them in the panel chat. Write for that reader — lead with the result, keep replies short and concrete, and don't narrate routine internal steps.
37
38
 
@@ -402,6 +403,17 @@ export async function runPanelOrchestrator() {
402
403
  // Subscription lane: the background agent must authenticate against the user's
403
404
  // claude.ai login, never an API key. Unset the key for the SDK subprocess.
404
405
  delete process.env.ANTHROPIC_API_KEY;
406
+ // First non-internal IPv4 — display-only, for the LAN-bridge banner when the
407
+ // bind host is 0.0.0.0/:: (the real reachable address depends on the network).
408
+ const firstLanIPv4 = () => {
409
+ for (const addrs of Object.values(networkInterfaces())) {
410
+ for (const a of addrs ?? []) {
411
+ if (!a.internal && a.family === "IPv4")
412
+ return a.address;
413
+ }
414
+ }
415
+ return undefined;
416
+ };
405
417
  // Secure bridge: when driving a REMOTE https ComfyUI (a pod), the pod's HTTPS
406
418
  // panel page can't reach a plain ws:// loopback bridge (mixed-content / Private
407
419
  // Network Access), so auto-upgrade to a token-gated wss:// exposed via a
@@ -410,11 +422,42 @@ export async function runPanelOrchestrator() {
410
422
  const insecureBridge = process.env.COMFYUI_MCP_INSECURE_BRIDGE === "1" ||
411
423
  process.env.COMFYUI_MCP_INSECURE_BRIDGE === "true";
412
424
  const wantSecureBridge = !insecureBridge && isRemoteHttpsUrl(process.env.COMFYUI_URL ?? "");
413
- const bridgeToken = wantSecureBridge ? randomBytes(24).toString("hex") : null;
414
- // Dedicated PANEL bridge port (default 9180). Token-gated only in secure mode.
425
+ // LAN bridge (panel #54 — the 24/7 server / standalone OpenClaw topology):
426
+ // COMFYUI_MCP_BRIDGE_HOST binds the bridge on a non-loopback interface so
427
+ // browsers on OTHER machines can connect. Non-loopback ALWAYS token-gates the
428
+ // WS upgrade — the token comes from COMFYUI_MCP_BRIDGE_TOKEN (pin it for
429
+ // stable reconnects across restarts) or is generated fresh and printed below.
430
+ const bridgeHost = (process.env.COMFYUI_MCP_BRIDGE_HOST ?? "127.0.0.1").trim() || "127.0.0.1";
431
+ const lanBridge = !isLoopbackBindHost(bridgeHost);
432
+ const envBridgeToken = process.env.COMFYUI_MCP_BRIDGE_TOKEN?.trim() || null;
433
+ const bridgeToken = envBridgeToken ?? (wantSecureBridge || lanBridge ? randomBytes(24).toString("hex") : null);
434
+ // Dedicated PANEL bridge port (default 9180). Token-gated in secure/LAN mode.
415
435
  const lockPort = Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180;
416
436
  const lockPath = orchLockPath(lockPort);
417
- const bridge = startUiBridge(lockPort, bridgeToken);
437
+ const bridge = startUiBridge(lockPort, bridgeToken, bridgeHost);
438
+ if (lanBridge) {
439
+ // Ready-to-paste connection info: the panel's Settings → Advanced →
440
+ // Bridge URL takes the full URL incl. ?token= verbatim.
441
+ const displayHost = bridgeHost === "0.0.0.0" || bridgeHost === "::"
442
+ ? (firstLanIPv4() ?? "<this-machine-ip>")
443
+ : bridgeHost;
444
+ process.stderr.write([
445
+ "",
446
+ "════════════════════════════════════════════════════════════════════",
447
+ " ComfyUI MCP — panel bridge exposed on the LAN (token-gated)",
448
+ "════════════════════════════════════════════════════════════════════",
449
+ ` Bridge URL : ws://${displayHost}:${lockPort}/?token=${bridgeToken}`,
450
+ "",
451
+ " In the panel: Settings → Advanced → Bridge URL → paste the URL above,",
452
+ " then click Connect. Anyone with this URL can drive the agent — treat",
453
+ " it like a password.",
454
+ envBridgeToken
455
+ ? " Token source: COMFYUI_MCP_BRIDGE_TOKEN (stable across restarts)."
456
+ : " Token was GENERATED for this run — set COMFYUI_MCP_BRIDGE_TOKEN to keep the same URL across restarts.",
457
+ "════════════════════════════════════════════════════════════════════",
458
+ "",
459
+ ].join("\n") + "\n");
460
+ }
418
461
  // Owning the bridge port is the orchestrator's whole job — if another process
419
462
  // holds it, fail loudly instead of running uselessly. (This also avoids the
420
463
  // case where a failed bind leaves the process with no live handles and it
@@ -570,19 +613,54 @@ export async function runPanelOrchestrator() {
570
613
  // applied PER REQUEST — switching live is free. Default = the LLM Arena's best
571
614
  // performer (scripts/llm-arena.mjs): gemma4:e4b, 9/10 with the cleanest runs
572
615
  // (first-try tool dispatch, no nudges) and multimodal headroom for vision.
573
- const ollamaModel = process.env.COMFYUI_MCP_OLLAMA_MODEL ?? "gemma4:e4b";
616
+ //
617
+ // Config precedence: env (escape hatch, always wins) → persisted user settings
618
+ // (~/.comfyui-mcp/panel-settings.json, edited from the panel Settings dialog
619
+ // via set_config) → built-in default. Mutable (`let`) because set_config can
620
+ // retarget them live; API keys stay env-only and never touch the settings file.
621
+ // Copy any panel-stored provider keys (OPENROUTER_API_KEY) into env BEFORE we
622
+ // read them below, so a key set on a prior run enables its provider on boot.
623
+ const hydratedSecrets = hydrateAgentSecretsIntoEnv();
624
+ if (hydratedSecrets.length) {
625
+ logger.info(`[panel-orchestrator] hydrated agent secrets from store: ${hydratedSecrets.join(", ")}`);
626
+ }
627
+ const persistedAgent = getAgentSettings();
628
+ let ollamaModel = process.env.COMFYUI_MCP_OLLAMA_MODEL ?? persistedAgent.ollama?.model ?? "gemma4:e4b";
574
629
  // The same backend also speaks any OpenAI-compatible endpoint (OpenRouter,
575
630
  // DeepSeek, vLLM, LM Studio): COMFYUI_MCP_OLLAMA_API=openai +
576
631
  // COMFYUI_MCP_OLLAMA_BASE_URL (incl. /v1) + COMFYUI_MCP_OLLAMA_API_KEY
577
632
  // (falls back to OPENROUTER_API_KEY). The chip stays "Ollama (local)".
578
- const ollamaApi = process.env.COMFYUI_MCP_OLLAMA_API === "openai" ? "openai" : "ollama";
579
- const ollamaBaseUrl = process.env.COMFYUI_MCP_OLLAMA_BASE_URL;
633
+ let ollamaApi = (process.env.COMFYUI_MCP_OLLAMA_API
634
+ ? process.env.COMFYUI_MCP_OLLAMA_API === "openai"
635
+ : persistedAgent.ollama?.api === "openai")
636
+ ? "openai"
637
+ : "ollama";
638
+ let ollamaBaseUrl = process.env.COMFYUI_MCP_OLLAMA_BASE_URL ?? persistedAgent.ollama?.baseUrl;
580
639
  const ollamaApiKey = process.env.COMFYUI_MCP_OLLAMA_API_KEY || process.env.OPENROUTER_API_KEY;
581
640
  const ollamaDeps = () => ({
582
641
  api: ollamaApi,
583
642
  ...(ollamaBaseUrl ? { host: ollamaBaseUrl } : {}),
584
643
  ...(ollamaApi === "openai" && ollamaApiKey ? { apiKey: ollamaApiKey } : {}),
585
644
  });
645
+ // OpenRouter is a first-class provider = the Ollama backend hard-wired to
646
+ // OpenRouter's OpenAI-compatible endpoint, so its picker leads with the
647
+ // curated arena-winning models (RECOMMENDED_OPENROUTER_MODELS, MiMo/MiniMax
648
+ // tagged 1M · SOTA). Key comes from OPENROUTER_API_KEY (or the shared ollama
649
+ // key). Default model = the arena's top open-weight, MiMo v2.5.
650
+ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
651
+ let openrouterModel = process.env.COMFYUI_MCP_OPENROUTER_MODEL ?? "xiaomi/mimo-v2.5";
652
+ // Read the key FRESH each call (not a startup const) so a key the user sets
653
+ // later via the panel — setAgentSecret hydrates it into env — takes effect on
654
+ // the next backend build without an orchestrator restart.
655
+ const openrouterApiKey = () => process.env.OPENROUTER_API_KEY || process.env.COMFYUI_MCP_OLLAMA_API_KEY;
656
+ const openrouterDeps = () => {
657
+ const key = openrouterApiKey();
658
+ return {
659
+ api: "openai",
660
+ host: OPENROUTER_BASE_URL,
661
+ ...(key ? { apiKey: key } : {}),
662
+ };
663
+ };
586
664
  // ── Per-tab backend (single-port multi-provider) ──────────────────────────
587
665
  // ONE orchestrator on ONE bridge port serves ALL providers; the panel picks a
588
666
  // provider per tab via the `hello`/`set_backend` handshake, instead of the node
@@ -592,7 +670,7 @@ export async function runPanelOrchestrator() {
592
670
  // provider (the panel replays the transcript to seed it) while a same-provider
593
671
  // reconnect RESUMES. `backendId`/`codexModel`/`geminiModel` above are the
594
672
  // DEFAULT + per-provider model config; the process is no longer pinned to one.
595
- const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini", "ollama"]);
673
+ const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini", "ollama", "openrouter"]);
596
674
  const defaultBackend = KNOWN_BACKENDS.has(backendId) ? backendId : "claude";
597
675
  const AGENT_KEY_SEP = "::";
598
676
  const tabBackends = new Map(); // panel tabId -> selected backend
@@ -739,6 +817,16 @@ export async function runPanelOrchestrator() {
739
817
  ...ollamaDeps(),
740
818
  });
741
819
  }
820
+ if (backend === "openrouter") {
821
+ return new OllamaBackend({
822
+ cwd: comfyuiPath ?? process.cwd(),
823
+ model: openrouterModel,
824
+ systemAppend: panelSystemAppend,
825
+ comfyuiUrl,
826
+ mcpServers: makeHttpBackendMcpServers(panelTabId),
827
+ ...openrouterDeps(),
828
+ });
829
+ }
742
830
  return undefined; // claude → built-in ClaudeBackend
743
831
  };
744
832
  logger.info(`[panel-orchestrator] single-port multi-provider: default backend=${defaultBackend}; ` +
@@ -759,7 +847,9 @@ export async function runPanelOrchestrator() {
759
847
  ? new CodexBackend({ cwd: comfyuiPath ?? process.cwd(), model: codexModel })
760
848
  : backend === "ollama"
761
849
  ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: ollamaModel, ...ollamaDeps() })
762
- : new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
850
+ : backend === "openrouter"
851
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: openrouterModel, ...openrouterDeps() })
852
+ : new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
763
853
  probeBackends.set(backend, pb);
764
854
  }
765
855
  return pb;
@@ -914,6 +1004,24 @@ export async function runPanelOrchestrator() {
914
1004
  manager.restartAllForMcpEnv("🔑 The API token you just provided is now active for the comfyui tools — retry the action that needed it (e.g. the download that returned 401).");
915
1005
  logger.info(`[panel-orchestrator] tool secret saved → comfyui MCP env updated + agents respawn on idle (keys: ${comfyuiSecretKeys().join(", ") || "none"})`);
916
1006
  });
1007
+ // An agent-provider secret changed (e.g. the OpenRouter API key set from the
1008
+ // panel). Hydrate it into env, drop the cached openrouter probe/model list so
1009
+ // the next probe uses the new key, and re-push readiness + models to every
1010
+ // live tab so the OpenRouter provider flips to "ready" and lists its models
1011
+ // without a reconnect.
1012
+ const unsubscribeAgentSecrets = onAgentSecretsChanged(() => {
1013
+ hydrateAgentSecretsIntoEnv();
1014
+ modelsByBackend.delete("openrouter");
1015
+ const pb = probeBackends.get("openrouter");
1016
+ if (pb?.close)
1017
+ void pb.close().catch(() => { });
1018
+ probeBackends.delete("openrouter");
1019
+ for (const tabId of tabBackends.keys()) {
1020
+ pushReadiness(tabId);
1021
+ pushModels(tabId);
1022
+ }
1023
+ logger.info("[panel-orchestrator] OpenRouter key saved → provider readiness + models refreshed");
1024
+ });
917
1025
  // Debounce the connect ack: the panel re-sends `hello` on reconnect and on
918
1026
  // workflow-title changes, which would otherwise stack duplicate greetings.
919
1027
  const lastAckAt = new Map();
@@ -950,7 +1058,23 @@ export async function runPanelOrchestrator() {
950
1058
  : fetchSupportedModels(model);
951
1059
  p = probe.then((list) => {
952
1060
  if (!list.length)
953
- modelsByBackend.delete(backend); // don't cache a failure
1061
+ modelsByBackend.delete(backend); // don't cache a failed probe
1062
+ // User-curated preferred models (panel Settings → set_config) pin to the
1063
+ // top of the ollama picker, ahead of the discovered catalog. Read fresh
1064
+ // on every probe; set_config evicts the cache so edits apply live.
1065
+ if (backend === "ollama") {
1066
+ const preferred = getAgentSettings().preferredModels ?? [];
1067
+ if (preferred.length) {
1068
+ const discovered = new Map(list.map((m) => [m.value, m]));
1069
+ list = [
1070
+ ...preferred.map((id) => (discovered.get(id) ?? {
1071
+ value: id,
1072
+ displayName: `${id} ★`,
1073
+ })),
1074
+ ...list.filter((m) => !preferred.includes(m.value)),
1075
+ ];
1076
+ }
1077
+ }
954
1078
  return list;
955
1079
  });
956
1080
  modelsByBackend.set(backend, p);
@@ -967,6 +1091,8 @@ export async function runPanelOrchestrator() {
967
1091
  return geminiModel;
968
1092
  if (backend === "ollama")
969
1093
  return ollamaModel;
1094
+ if (backend === "openrouter")
1095
+ return openrouterModel;
970
1096
  return model;
971
1097
  }
972
1098
  function pushModels(panelTabId) {
@@ -1088,9 +1214,24 @@ export async function runPanelOrchestrator() {
1088
1214
  const isCx = backend === "codex";
1089
1215
  const isGm = backend === "gemini";
1090
1216
  const isOl = backend === "ollama";
1217
+ const isOr = backend === "openrouter";
1091
1218
  // TRUTHFUL "connected": only claim ready after PROVING the SELECTED backend
1092
1219
  // can run, by probing its model list. If the probe fails — the "connected
1093
1220
  // but dead" wedge — send a degraded ack so the panel shows the real state.
1221
+ // OpenRouter needs an explicit key check FIRST: its /models endpoint is
1222
+ // PUBLIC, so the probe "succeeds" keyless and the tab would greet ready —
1223
+ // then 401 on the first real message. Degrade up front instead.
1224
+ if (isOr && !openrouterApiKey()) {
1225
+ bridge.push({
1226
+ type: "say",
1227
+ text: "⚠️ OpenRouter has no API key — the connection would fail on your first message. " +
1228
+ "Set it in Settings → OpenRouter → “Set API key…” (masked, stored by the orchestrator — takes effect immediately, no reconnect needed), " +
1229
+ "or set the OPENROUTER_API_KEY environment variable and restart the orchestrator. Keys: https://openrouter.ai/keys",
1230
+ }, panelTab);
1231
+ bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
1232
+ logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (openrouter) but no API key — degraded ack`);
1233
+ return;
1234
+ }
1094
1235
  void ensureModels(backend)
1095
1236
  .then((models) => {
1096
1237
  if (models.length) {
@@ -1100,7 +1241,9 @@ export async function runPanelOrchestrator() {
1100
1241
  ? (geminiModel ?? models[0].value ?? "Gemini")
1101
1242
  : isOl
1102
1243
  ? (ollamaModel ?? models[0].value ?? "Ollama")
1103
- : model;
1244
+ : isOr
1245
+ ? (openrouterModel ?? models[0].value ?? "OpenRouter")
1246
+ : model;
1104
1247
  // Greet only on a FRESH session (a resume/reconnect already has the thread).
1105
1248
  if (!resume) {
1106
1249
  const readyText = isCx
@@ -1109,7 +1252,9 @@ export async function runPanelOrchestrator() {
1109
1252
  ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
1110
1253
  : isOl
1111
1254
  ? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via Ollama (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
1112
- : `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
1255
+ : isOr
1256
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
1257
+ : `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
1113
1258
  bridge.push({ type: "say", text: readyText }, panelTab);
1114
1259
  }
1115
1260
  bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
@@ -1157,17 +1302,89 @@ export async function runPanelOrchestrator() {
1157
1302
  logger.info(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} switched backend ${prev} → ${reqBackend}`);
1158
1303
  return;
1159
1304
  }
1160
- // Live panel config (currently just the render-stall threshold). Applied
1161
- // immediately, no reconnect the next turn's watchdog check uses the new
1162
- // value. Sent by the panel on connect and whenever the setting changes.
1305
+ // Live panel config: render-stall threshold, plus the user's agent-model
1306
+ // preferences (preferred_models list + ollama endpoint config), persisted to
1307
+ // ~/.comfyui-mcp/panel-settings.json. Sent by the panel on connect and
1308
+ // whenever a setting changes. Model-list changes apply live (cache evicted,
1309
+ // fresh `models` frame pushed); an endpoint change retargets NEW sessions —
1310
+ // live ollama sessions keep their connection until restarted.
1163
1311
  if (event.type === "set_config" && event.tab_id) {
1164
1312
  if ("stall_seconds" in event) {
1165
1313
  setLiveStallSeconds(event.stall_seconds);
1166
1314
  logger.info(`[panel-orchestrator] live stall threshold → ${liveStallSeconds ?? "default"}s`);
1167
1315
  }
1316
+ const cfg = event;
1317
+ let ollamaChanged = false;
1318
+ if (Array.isArray(cfg.preferred_models)) {
1319
+ const ids = cfg.preferred_models.filter((m) => typeof m === "string");
1320
+ setAgentSettings({ preferredModels: ids });
1321
+ ollamaChanged = true;
1322
+ logger.info(`[panel-orchestrator] preferred models → [${ids.join(", ")}]`);
1323
+ }
1324
+ if (cfg.ollama && typeof cfg.ollama === "object") {
1325
+ const o = cfg.ollama;
1326
+ const patch = {};
1327
+ if (typeof o.model === "string" && o.model.trim()) {
1328
+ patch.model = o.model.trim();
1329
+ if (!process.env.COMFYUI_MCP_OLLAMA_MODEL)
1330
+ ollamaModel = patch.model;
1331
+ }
1332
+ if (o.api === "openai" || o.api === "ollama") {
1333
+ patch.api = o.api;
1334
+ if (!process.env.COMFYUI_MCP_OLLAMA_API)
1335
+ ollamaApi = o.api;
1336
+ }
1337
+ if (typeof o.base_url === "string") {
1338
+ patch.baseUrl = o.base_url.trim();
1339
+ if (!process.env.COMFYUI_MCP_OLLAMA_BASE_URL)
1340
+ ollamaBaseUrl = patch.baseUrl || undefined;
1341
+ }
1342
+ if (Object.keys(patch).length) {
1343
+ setAgentSettings({ ollama: patch });
1344
+ ollamaChanged = true;
1345
+ // Endpoint may have moved — drop the cached probe backend so the next
1346
+ // readiness/model probe hits the NEW host/api with fresh deps.
1347
+ const pb = probeBackends.get("ollama");
1348
+ if (pb?.close)
1349
+ void pb.close().catch(() => { });
1350
+ probeBackends.delete("ollama");
1351
+ logger.info(`[panel-orchestrator] ollama config → model=${ollamaModel} api=${ollamaApi} host=${ollamaBaseUrl ?? "(default)"}`);
1352
+ }
1353
+ }
1354
+ if (ollamaChanged) {
1355
+ modelsByBackend.delete("ollama");
1356
+ pushModels(event.tab_id);
1357
+ }
1168
1358
  bridge.push({ type: "ack", ok: true, kind: "config" }, event.tab_id);
1169
1359
  return;
1170
1360
  }
1361
+ // Panel-initiated provider secret (Settings › "Set API key…") — NO agent, no
1362
+ // chat: the panel paints its own masked input and ships the value here
1363
+ // directly, over the same loopback/token-gated bridge the agent-initiated
1364
+ // request_secret reply already rides. setAgentSecret enforces the allowlist
1365
+ // (OPENROUTER_API_KEY), persists 0600, and hydrates process.env immediately,
1366
+ // so the refreshed readiness frame flips the provider picker live — a user
1367
+ // can enable OpenRouter before ANY backend is ready (no chicken-and-egg).
1368
+ if (event.type === "set_secret" && event.tab_id) {
1369
+ const rawKey = event.key;
1370
+ const rawValue = event.value;
1371
+ const key = typeof rawKey === "string" ? rawKey : "";
1372
+ const value = typeof rawValue === "string" ? rawValue : "";
1373
+ let error;
1374
+ try {
1375
+ if (!value.trim())
1376
+ throw new Error("No token entered — nothing was saved.");
1377
+ setAgentSecret(key, value.trim());
1378
+ logger.info(`[panel-orchestrator] provider secret set from panel Settings: ${key} (redacted)`);
1379
+ }
1380
+ catch (err) {
1381
+ error = err instanceof Error ? err.message : String(err);
1382
+ }
1383
+ bridge.push({ type: "secret_saved", key, ok: !error, ...(error ? { error } : {}) }, event.tab_id);
1384
+ if (!error)
1385
+ pushReadiness(event.tab_id);
1386
+ return;
1387
+ }
1171
1388
  // Model / effort picker: apply and confirm. Model switches live; an effort
1172
1389
  // change restarts the session (resumed) so the conversation carries over.
1173
1390
  if (event.type === "set_options" && event.tab_id) {
@@ -1447,6 +1664,7 @@ export async function runPanelOrchestrator() {
1447
1664
  clearInterval(downloadTimer);
1448
1665
  QueueMonitor.stop();
1449
1666
  unsubscribeSecrets();
1667
+ unsubscribeAgentSecrets();
1450
1668
  await manager.stopAll();
1451
1669
  // Dispose the readiness-probe backends (kills each Codex/Gemini CLI child).
1452
1670
  for (const pb of probeBackends.values()) {