comfyui-mcp 0.28.0 → 0.30.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/dist/comfyui/client.js +86 -0
- package/dist/comfyui/client.js.map +1 -1
- package/dist/orchestrator/backend-readiness.js +20 -0
- package/dist/orchestrator/backend-readiness.js.map +1 -1
- package/dist/orchestrator/index.js +295 -35
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/ollama-backend.js +15 -5
- package/dist/orchestrator/ollama-backend.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +162 -42
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/calc-evaluator.js +443 -0
- package/dist/services/calc-evaluator.js.map +1 -0
- package/dist/services/graph-query.js +228 -0
- package/dist/services/graph-query.js.map +1 -0
- package/dist/services/llamacpp-probe.js +88 -0
- package/dist/services/llamacpp-probe.js.map +1 -0
- package/dist/services/node-dev.js +718 -0
- package/dist/services/node-dev.js.map +1 -0
- package/dist/services/node-management.js +2 -2
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/panel-secrets.js +2 -1
- package/dist/services/panel-secrets.js.map +1 -1
- package/dist/services/panel-settings.js +6 -0
- package/dist/services/panel-settings.js.map +1 -1
- package/dist/services/slot-compat.js +53 -0
- package/dist/services/slot-compat.js.map +1 -0
- package/dist/services/ui-bridge.js +170 -16
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/workflow-health.js +244 -0
- package/dist/services/workflow-health.js.map +1 -0
- package/dist/services/workflow-validator.js +28 -2
- package/dist/services/workflow-validator.js.map +1 -1
- package/dist/tools/calculate.js +97 -0
- package/dist/tools/calculate.js.map +1 -0
- package/dist/tools/comfyui-settings.js +68 -0
- package/dist/tools/comfyui-settings.js.map +1 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/node-dev.js +213 -0
- package/dist/tools/node-dev.js.map +1 -0
- package/dist/tools/workflow-dsl.js +87 -3
- package/dist/tools/workflow-dsl.js.map +1 -1
- package/dist/tools/workflow-library.js +128 -2
- package/dist/tools/workflow-library.js.map +1 -1
- package/dist/tools/workflow-validate.js +21 -4
- package/dist/tools/workflow-validate.js.map +1 -1
- package/package.json +2 -2
- package/plugin/skills/debug-render/SKILL.md +2 -2
- package/plugin/skills/director/SKILL.md +1 -1
- package/plugin/skills/local-llm-free/SKILL.md +6 -6
- package/plugin/skills/workflow-layout/SKILL.md +5 -5
- package/scripts/gen-tool-docs.ts +8 -3
|
@@ -17,6 +17,7 @@ import { randomBytes } from "node:crypto";
|
|
|
17
17
|
import readline from "node:readline";
|
|
18
18
|
import { startUiBridge, isLoopbackBindHost } from "../services/ui-bridge.js";
|
|
19
19
|
import { setupSecureBridge } from "../services/secure-bridge.js";
|
|
20
|
+
import { startQuickTunnel } from "../services/tunnel.js";
|
|
20
21
|
import { detectInstallMode } from "../services/self-update.js";
|
|
21
22
|
import { SessionStore } from "./session-store.js";
|
|
22
23
|
import { logger } from "../utils/logger.js";
|
|
@@ -24,7 +25,7 @@ import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffo
|
|
|
24
25
|
import { createPanelMcpServer } from "./panel-tools.js";
|
|
25
26
|
import { readUserMcpServers } from "../services/user-mcp-config.js";
|
|
26
27
|
import { isForceRemoteFlagSet, isLoopbackHost, detectLocalComfyUIPath } from "../config.js";
|
|
27
|
-
import { buildComfyuiMcpEnv, comfyuiSecretKeys, onComfyuiSecretsChanged, hydrateAgentSecretsIntoEnv, onAgentSecretsChanged, setAgentSecret, } from "../services/panel-secrets.js";
|
|
28
|
+
import { buildComfyuiMcpEnv, comfyuiSecretKeys, onComfyuiSecretsChanged, hydrateAgentSecretsIntoEnv, onAgentSecretsChanged, setAgentSecret, setComfyuiSecret, isAllowedComfyuiSecretKey, } from "../services/panel-secrets.js";
|
|
28
29
|
import { CodexBackend } from "./codex-backend.js";
|
|
29
30
|
import { GeminiBackend, GEMINI_DEFAULT_MODEL } from "./gemini-backend.js";
|
|
30
31
|
import { OllamaBackend } from "./ollama-backend.js";
|
|
@@ -34,15 +35,16 @@ import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
|
|
|
34
35
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
35
36
|
import { unloadAllOllama, warmOllama, resolveOllamaHost } from "../services/ollama-vram.js";
|
|
36
37
|
import { isLocalLmstudio, startLmstudioServer, unloadAllLmstudio, warmLmstudio, } from "../services/lmstudio-lifecycle.js";
|
|
38
|
+
import { llamacppProps, llamacppToolsReady } from "../services/llamacpp-probe.js";
|
|
37
39
|
import { getAgentSettings, setAgentSettings } from "../services/panel-settings.js";
|
|
38
40
|
import { gatherEnvCapabilities, buildPanelSystemAppend, } from "../services/env-capabilities.js";
|
|
39
41
|
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.
|
|
40
42
|
|
|
41
|
-
You can SEE and EDIT the workflow the user currently has open, via the panel_* tools (
|
|
43
|
+
You can SEE and EDIT the workflow the user currently has open, via the panel_* tools (panel_graph_outline, panel_query_graph, panel_add_node, panel_connect, panel_set_widget, panel_run, panel_get_errors, panel_save_workflow, …). STRONGLY PREFER building on their live canvas: read it first (panel_graph_outline, then panel_query_graph for specifics), add/wire/configure nodes with the panel_* tools, then panel_run to queue it — so the user watches the work happen and the result loads in their own workflow with full Ctrl+Z undo. Only fall back to the headless generate_image/enqueue_workflow tools when the user explicitly wants a one-off they don't need on their canvas, or when no panel tab is connected (a panel_* call will error if so). On a LARGE graph (a loaded pack/template with dozens of nodes), do NOT dump the whole thing and scan it — and NEVER shell out to grep/jq/python over a saved workflow file. To UNDERSTAND the graph, call panel_graph_outline FIRST: a compact, dependency-ordered TEXT map (nodes topologically sorted source→sink, each with its key widgets and ← inputs / → outputs wiring, plus a groups index) made for you to read top-to-bottom. To PINPOINT and INSPECT specific nodes, use panel_query_graph: filter by types/title/widget predicates ('cfg>7'), traverse upstream_of/downstream_of a node, aggregate with group_by:'type', and read ONE node's exact slot/widget detail with {ids:[id], fields:'detail'} — output is token-bounded so it can never flood your context. panel_find_nodes remains for free-text search across all fields.
|
|
42
44
|
|
|
43
45
|
TRUST REPORTED MANUAL CHANGES. The user can edit the canvas BY HAND between your turns (bypass/mute a node, change a widget, rewire, add/remove nodes). When that happens, your turn opens with a "⟳ MANUAL CANVAS CHANGES since your last turn" block listing exactly what they changed. Treat that block as GROUND TRUTH about the current graph — it overrides what you remember from earlier in the conversation. Do NOT assume the graph still matches your last edit or your earlier reading; if the listed changes are substantial (or contradict a plan you were mid-execution on), re-read with panel_graph_outline before you act or draw conclusions. This is also how you learn the user already tried something (e.g. they bypassed a node and it worked) — believe it over your own prior reasoning.
|
|
44
46
|
|
|
45
|
-
REFACTOR BIG GRAPHS INTO TOGGLEABLE SUBGRAPHS — don't reconstruct group membership by hand.
|
|
47
|
+
REFACTOR BIG GRAPHS INTO TOGGLEABLE SUBGRAPHS — don't reconstruct group membership by hand. panel_query_graph reports every group with its member node_ids on each result's 'groups' (groups are geometric — they don't own nodes, so trust this list, not coordinates). To make a region readable and switchable as a UNIT (e.g. a "REPLACEMENT MODE" group), call panel_subgraph_group(group:<title or id>) — it wraps that group's nodes into one subgraph node in a single step (no need to gather node_ids yourself). Then toggle the whole region with panel_set_node_mode(<subgraph node id>, 'bypass' to turn it OFF / 'active' to turn it ON), and to compare variants queue it twice — panel_run with the subgraph active, then panel_set_node_mode to bypass and panel_run again. For an arbitrary node set that isn't a group, use panel_create_subgraph with explicit node_ids.
|
|
46
48
|
|
|
47
49
|
If a workflow needs a custom node the user doesn't have, don't silently skip it — offer to install it. Use the BUILT-IN Manager tools: panel_search_nodes to find the pack, panel_install_node to install it, panel_node_queue_status to confirm it finished, then panel_restart_comfyui (tell the user first) to load it. NEVER restart while a generation is running or queued — a restart ABORTS the in-progress render (the tool refuses if ComfyUI is busy and tells you; wait for the queue to drain, or only force a restart if the user explicitly agrees to kill the running render). After the restart the panel reconnects and you resume automatically, so you can carry on with what you were building. Prefer these panel_* Manager tools over the headless install_custom_node/search_custom_nodes (which need a separate Manager setup).
|
|
48
50
|
|
|
@@ -50,11 +52,11 @@ CRASH RECOVERY — when a custom node BREAKS or CRASHED ComfyUI, fix it before g
|
|
|
50
52
|
|
|
51
53
|
WEDGED RENDER / OOM / VRAM PINNED — when a generation is stuck or hits CUDA out-of-memory, or a cancel didn't actually free GPU memory (models still resident, VRAM pinned, the next run still OOMs), call panel_free_vram to UNLOAD all models and free VRAM before retrying — it does NOT restart ComfyUI, so it's the cheap first move. Escalation ladder: cancel the run → panel_free_vram (unload + free) → retry; only as a LAST RESORT panel_restart_comfyui (which refuses mid-render and guards the running generation). Reach for panel_free_vram before a restart whenever a cancel left memory pinned.
|
|
52
54
|
|
|
53
|
-
CRITICAL — never destroy the user's work. When they ask for a "new workflow", a "fresh canvas", or to "start over for a new project", call panel_new_workflow (it opens a NEW TAB and leaves their current workflow intact). NEVER use panel_clear for that — panel_clear wipes the CURRENTLY OPEN graph and is ONLY for an explicit "clear/reset this canvas". You can manage tabs with panel_list_workflows / panel_open_workflow / panel_rename_workflow / panel_close_workflow, and group nodes with panel_select_nodes / panel_create_subgraph. To label a node by its purpose, use panel_set_node_title. To read or edit nodes INSIDE a subgraph, call panel_enter_subgraph(node_id) first — then
|
|
55
|
+
CRITICAL — never destroy the user's work. When they ask for a "new workflow", a "fresh canvas", or to "start over for a new project", call panel_new_workflow (it opens a NEW TAB and leaves their current workflow intact). NEVER use panel_clear for that — panel_clear wipes the CURRENTLY OPEN graph and is ONLY for an explicit "clear/reset this canvas". You can manage tabs with panel_list_workflows / panel_open_workflow / panel_rename_workflow / panel_close_workflow, and group nodes with panel_select_nodes / panel_create_subgraph. To label a node by its purpose, use panel_set_node_title. To read or edit nodes INSIDE a subgraph, call panel_enter_subgraph(node_id) first — then panel_query_graph / panel_graph_outline and the panel_* edit tools operate on the subgraph's inner nodes — and panel_exit_subgraph when you're done.
|
|
54
56
|
|
|
55
|
-
SUBGRAPH I/O — exposing interior nodes to the boundary. To wire an interior node to the subgraph's boundary from INSIDE a subgraph, do NOT panel_connect to a guessed rail node id — that's the rail and you'll get it wrong. Use panel_expose_subgraph_output(from_node_id, from_output) to expose an interior OUTPUT on the output rail (so the parent graph can wire the subgraph node's new output), and panel_expose_subgraph_input(to_node_id, to_input) to expose an interior INPUT on the input rail. Read
|
|
57
|
+
SUBGRAPH I/O — exposing interior nodes to the boundary. To wire an interior node to the subgraph's boundary from INSIDE a subgraph, do NOT panel_connect to a guessed rail node id — that's the rail and you'll get it wrong. Use panel_expose_subgraph_output(from_node_id, from_output) to expose an interior OUTPUT on the output rail (so the parent graph can wire the subgraph node's new output), and panel_expose_subgraph_input(to_node_id, to_input) to expose an interior INPUT on the input rail. Read panel_query_graph's \`rails\` field (present when viewing a subgraph) to see the current boundary slots — what's already exposed and what still needs it. To EXPAND/DISSOLVE a subgraph back into the parent graph (inline its inner nodes and rewire external links, removing the wrapper — the inverse of panel_create_subgraph), use panel_unpack_subgraph(node_id). All three are undoable with Ctrl+Z.
|
|
56
58
|
|
|
57
|
-
MERGE / COMPOSE WORKFLOWS — to bring nodes from ONE workflow into ANOTHER (combine two graphs, copy a section across tabs, reuse part of a saved workflow), use copy/paste: panel_open_workflow (the source) → panel_select_nodes (the section you want, or select all the nodes from
|
|
59
|
+
MERGE / COMPOSE WORKFLOWS — to bring nodes from ONE workflow into ANOTHER (combine two graphs, copy a section across tabs, reuse part of a saved workflow), use copy/paste: panel_open_workflow (the source) → panel_select_nodes (the section you want, or select all the nodes from panel_query_graph {fields:'ids'}) → panel_copy_nodes → panel_open_workflow or panel_new_workflow (the destination) → panel_paste_nodes (returns the new node ids) → then wire and tidy them, applying the workflow-layout skill so the merged result is clean (no overlaps). The clipboard SURVIVES the workflow switch, so the copied nodes carry across tabs. Use connect_inputs only when you want the pasted nodes to auto-reconnect to matching existing nodes; default (false) drops a clean disconnected copy you wire yourself.
|
|
58
60
|
|
|
59
61
|
REUSE SUBGRAPHS via the blueprint library — when the user builds a useful subgraph and wants to reuse it (now or in other workflows), SAVE it: panel_create_subgraph to group the nodes (if not already a subgraph), then panel_save_subgraph(node_id, name) publishes it to their library programmatically (no dialog). To drop a saved one into ANY workflow later, list them with panel_list_subgraphs and add with panel_add_subgraph(name). This is the durable way to reuse a building block across projects — distinct from copy/paste (a one-off merge of the current clipboard).
|
|
60
62
|
|
|
@@ -84,13 +86,13 @@ Adult / NSFW content is gated behind an explicit, persistent consent mode — qu
|
|
|
84
86
|
|
|
85
87
|
SHOW / DISPLAY IMAGES AND VIDEOS — whenever the user asks to see, show, or display an image or video that you generated, composited, downloaded, or found — whether it is a file on disk (absolute path on the orchestrator host) or a ComfyUI output ref ({ filename, subfolder?, type? }) — call panel_show_media to render it as a media card directly in this chat. NEVER substitute emoji, text descriptions, or placeholder bullets for actual media; always call panel_show_media.
|
|
86
88
|
|
|
87
|
-
INSPECT NODE MODES BEFORE YOU RUN. After loading a pack/template/workflow — and before any panel_run —
|
|
89
|
+
INSPECT NODE MODES BEFORE YOU RUN. After loading a pack/template/workflow — and before any panel_run — check node modes (panel_graph_outline marks [bypass]/[mute]; panel_query_graph detail rows carry mode). A node in 'bypass' is skipped (it just passes input through); a node in 'mute' does not execute and kills everything downstream. Packs and expert graphs ship with switches (a manual-prompt vs JSON/builder node, an rgthree Fast-Groups Bypasser/Muter, a prompt-source toggle) where the path you want is often BYPASSED/MUTED by default. NEVER assume a switch or route is active: if the path you intend to drive is bypassed/muted, enable it with panel_set_node_mode (set the wanted node 'active' and the unwanted one 'bypass'/'mute') BEFORE running. A wrong/stale mode is a top cause of renders that come out wrong.
|
|
88
90
|
|
|
89
91
|
VERIFY THE OUTPUT MATCHES THE REQUEST. After a render completes, actually LOOK at the image/video the panel delivers and confirm it matches what was asked BEFORE you declare success or move to the next step. If it doesn't match, do NOT report progress — diagnose (wrong prompt path? a bypassed/muted builder or switch? wrong widget value?), fix it (often panel_set_node_mode or panel_set_widget), and rerun. Only claim something works once you've SEEN that it does — never report progress you haven't verified.
|
|
90
92
|
|
|
91
93
|
AFTER PANEL_RUN — once you call panel_run to queue a render, you will be notified automatically with the output image(s)/video when it finishes. Do not poll get_queue, get_history, or list_output_images waiting for the result — just end your turn and the finished render will be delivered to you.
|
|
92
94
|
|
|
93
|
-
DEBUG WRONG RENDERS BY INSPECTING INTERMEDIATE STEPS (run-to-node). When a final asset comes out WRONG — artifacts, wrong subject/pose/composition/color, blur, a ControlNet/IPAdapter/mask/LoRA not taking, a refiner or upscale stage degrading it — do NOT just re-roll the whole graph. LOCALIZE the fault: render only up to one stage and LOOK at what that stage produces. panel_run takes to_node_id to run ONE output branch (ComfyUI partial execution) — only that output node plus everything upstream of it renders, the rest is skipped, so it's fast and cheap, and the result is delivered to you automatically like any run. to_node_id MUST be an OUTPUT node (is_output:true in
|
|
95
|
+
DEBUG WRONG RENDERS BY INSPECTING INTERMEDIATE STEPS (run-to-node). When a final asset comes out WRONG — artifacts, wrong subject/pose/composition/color, blur, a ControlNet/IPAdapter/mask/LoRA not taking, a refiner or upscale stage degrading it — do NOT just re-roll the whole graph. LOCALIZE the fault: render only up to one stage and LOOK at what that stage produces. panel_run takes to_node_id to run ONE output branch (ComfyUI partial execution) — only that output node plus everything upstream of it renders, the rest is skipped, so it's fast and cheap, and the result is delivered to you automatically like any run. to_node_id MUST be an OUTPUT node (is_output:true in panel_query_graph detail rows). To inspect a point that ISN'T an output — a latent, a preprocessor/depth/pose map, a mask, an intermediate image — TAP it: add a PreviewImage on an IMAGE wire (or VAEDecode→PreviewImage on a LATENT, MaskToImage→PreviewImage on a MASK), panel_run(to_node_id=that preview), read the delivered image, then panel_remove_node the tap when done. Bisect upstream→downstream until you find the FIRST stage whose output is bad — that node (or its inputs/widgets) is what to fix, then run-to-node there again to confirm before a full run. For the full method (probe recipes, symptom→probe map) read the debug-render skill via read_skill. This is for renders that COMPLETE but look wrong; for runs that fail with an error/OOM/missing node, use the troubleshooting skill instead.
|
|
94
96
|
|
|
95
97
|
CHAIN A STAGE'S OUTPUT INTO THE NEXT STAGE'S LOADER — when a multi-stage pipeline (e.g. Krea2 image → LTX video → WAN extend) needs one stage's OUTPUT fed into the next stage's loader (LoadImage / VHS_LoadVideo / LoadAudio), call stage_output_as_input with the output's { filename, subfolder?, type? } and drop the returned input filename into the loader's image/video/audio widget. (Or, for a file already on disk, upload_image / upload_video / upload_audio.) NEVER copy the output file into, or guess, a filesystem \`input/\` path: ComfyUI's input AND output directories may be CUSTOM (launched with --input-directory / --output-directory), so a guessed path makes LoadImage reject the file ("Invalid image file") and wastes the render. stage_output_as_input goes through the server API (/view → /upload/image), which resolves the real dirs correctly every time. VERIFY A VIDEO RENDER VIA THE FILESYSTEM, NOT /history — VHS_VideoCombine and similar video nodes write the .mp4 but frequently do NOT register an output in ComfyUI's /history (the prompt shows done with no output and no error), so do NOT conclude a clip "silently dropped" from get_history/get_job_status; confirm it with list_output_images (which now lists videos, each tagged kind:"video") by filename/prefix + fresh mtime, then chain it forward with stage_output_as_input.
|
|
96
98
|
|
|
@@ -129,7 +131,7 @@ const injectedQueueNotes = new Set();
|
|
|
129
131
|
* prepended to each headless-tab turn like the crash/queue notes, tells it to run
|
|
130
132
|
* headless and deliver the result ITSELF, in-turn. */
|
|
131
133
|
const HEADLESS_DIRECTIVE = "[HEADLESS SESSION — no ComfyUI panel/canvas is connected] The panel_* live-canvas tools " +
|
|
132
|
-
"(panel_run,
|
|
134
|
+
"(panel_run, panel_query_graph, panel_set_widget, panel_add_node, …) are UNAVAILABLE here and will fail — " +
|
|
133
135
|
"do everything through the comfyui MCP tools (generate_image, or create_workflow + enqueue_workflow). " +
|
|
134
136
|
"There is NO panel to auto-deliver a finished render, so you MUST deliver the result YOURSELF IN THIS SAME TURN: " +
|
|
135
137
|
"enqueuing returns a prompt_id immediately, so wait for it with get_job_status(prompt_id) — poll it briefly until " +
|
|
@@ -584,6 +586,24 @@ export async function runPanelOrchestrator() {
|
|
|
584
586
|
const lockPort = Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180;
|
|
585
587
|
const lockPath = orchLockPath(lockPort);
|
|
586
588
|
const bridge = startUiBridge(lockPort, bridgeToken, bridgeHost);
|
|
589
|
+
// On-demand phone pairing (the panel "Remote control" button). Off by default:
|
|
590
|
+
// the FIRST pair request lazily binds a SECOND, token-gated listener on all
|
|
591
|
+
// interfaces (so a phone can reach it), while the primary loopback bridge stays
|
|
592
|
+
// token-less. LAN mode returns a same-wifi ws:// URL; tunnel mode fronts the same
|
|
593
|
+
// token-gated listener with a cloudflared wss:// for anywhere access.
|
|
594
|
+
const pairPort = lockPort + 2; // avoid the panel_* HTTP-MCP port (lockPort + 1)
|
|
595
|
+
let pairToken = null;
|
|
596
|
+
let pairListenerStarted = false;
|
|
597
|
+
let pairTunnel = null;
|
|
598
|
+
const ensurePairListener = async () => {
|
|
599
|
+
if (!pairToken)
|
|
600
|
+
pairToken = randomBytes(24).toString("hex");
|
|
601
|
+
if (!pairListenerStarted) {
|
|
602
|
+
await bridge.addListener("0.0.0.0", pairPort, pairToken);
|
|
603
|
+
pairListenerStarted = true;
|
|
604
|
+
}
|
|
605
|
+
return pairToken;
|
|
606
|
+
};
|
|
587
607
|
if (lanBridge) {
|
|
588
608
|
// Ready-to-paste connection info: the panel's Settings → Advanced →
|
|
589
609
|
// Bridge URL takes the full URL incl. ?token= verbatim.
|
|
@@ -853,6 +873,33 @@ export async function runPanelOrchestrator() {
|
|
|
853
873
|
const LMSTUDIO_BASE_URL = (process.env.COMFYUI_MCP_LMSTUDIO_HOST ?? "http://127.0.0.1:1234/v1").replace(/[/]$/, "");
|
|
854
874
|
let lmstudioModel = process.env.COMFYUI_MCP_LMSTUDIO_MODEL ?? persistedAgent.lmstudio?.model ?? "";
|
|
855
875
|
const lmstudioDeps = () => ({ api: "openai", host: LMSTUDIO_BASE_URL });
|
|
876
|
+
// llama.cpp (issue #161) — llama-server's OpenAI-compatible /v1. No key, no
|
|
877
|
+
// login, ONE model fixed at launch (-m). Context is a LAUNCH flag (-c), and
|
|
878
|
+
// tool calling needs --jinja — both probed at connect (services/llamacpp-probe).
|
|
879
|
+
const LLAMACPP_BASE_URL = (process.env.COMFYUI_MCP_LLAMACPP_HOST ?? "http://127.0.0.1:8080/v1").replace(/[/]$/, "");
|
|
880
|
+
let llamacppModel = process.env.COMFYUI_MCP_LLAMACPP_MODEL ?? persistedAgent.llamacpp?.model ?? "";
|
|
881
|
+
const llamacppDeps = () => ({ api: "openai", host: LLAMACPP_BASE_URL });
|
|
882
|
+
// Custom OpenAI-compatible endpoint (issue #162) — the same openai dialect
|
|
883
|
+
// pointed anywhere the user says: vLLM, DeepSeek, Together, Azure, a box on
|
|
884
|
+
// the LAN… Base URL + model come from panel Settings (persisted) or env; the
|
|
885
|
+
// API key from the 0600 secrets store (COMFYUI_MCP_CUSTOM_API_KEY, set via
|
|
886
|
+
// the panel's masked "Set API key…" — many local endpoints need none). NO
|
|
887
|
+
// default URL on purpose: unconfigured degrades with an actionable ack
|
|
888
|
+
// instead of dialing a guess.
|
|
889
|
+
let customBaseUrl = (process.env.COMFYUI_MCP_CUSTOM_BASE_URL ?? persistedAgent.custom?.baseUrl ?? "").replace(/[/]$/, "");
|
|
890
|
+
let customModel = process.env.COMFYUI_MCP_CUSTOM_MODEL ?? persistedAgent.custom?.model ?? "";
|
|
891
|
+
// Key read FRESH each call (not a startup const) so a key set later via the
|
|
892
|
+
// panel — setAgentSecret hydrates it into env — applies on the next backend
|
|
893
|
+
// build without an orchestrator restart.
|
|
894
|
+
const customApiKey = () => process.env.COMFYUI_MCP_CUSTOM_API_KEY;
|
|
895
|
+
const customDeps = () => {
|
|
896
|
+
const key = customApiKey();
|
|
897
|
+
return {
|
|
898
|
+
api: "openai",
|
|
899
|
+
host: customBaseUrl,
|
|
900
|
+
...(key ? { apiKey: key } : {}),
|
|
901
|
+
};
|
|
902
|
+
};
|
|
856
903
|
// ── Per-tab backend (single-port multi-provider) ──────────────────────────
|
|
857
904
|
// ONE orchestrator on ONE bridge port serves ALL providers; the panel picks a
|
|
858
905
|
// provider per tab via the `hello`/`set_backend` handshake, instead of the node
|
|
@@ -862,7 +909,7 @@ export async function runPanelOrchestrator() {
|
|
|
862
909
|
// provider (the panel replays the transcript to seed it) while a same-provider
|
|
863
910
|
// reconnect RESUMES. `backendId`/`codexModel`/`geminiModel` above are the
|
|
864
911
|
// DEFAULT + per-provider model config; the process is no longer pinned to one.
|
|
865
|
-
const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini", "ollama", "openrouter", "lmstudio"]);
|
|
912
|
+
const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini", "ollama", "openrouter", "lmstudio", "llamacpp", "custom"]);
|
|
866
913
|
const defaultBackend = KNOWN_BACKENDS.has(backendId) ? backendId : "claude";
|
|
867
914
|
const AGENT_KEY_SEP = "::";
|
|
868
915
|
const tabBackends = new Map(); // panel tabId -> selected backend
|
|
@@ -1030,6 +1077,26 @@ export async function runPanelOrchestrator() {
|
|
|
1030
1077
|
...lmstudioDeps(),
|
|
1031
1078
|
});
|
|
1032
1079
|
}
|
|
1080
|
+
if (backend === "llamacpp") {
|
|
1081
|
+
return new OllamaBackend({
|
|
1082
|
+
cwd: comfyuiPath ?? process.cwd(),
|
|
1083
|
+
model: llamacppModel,
|
|
1084
|
+
systemAppend: panelSystemAppend,
|
|
1085
|
+
comfyuiUrl,
|
|
1086
|
+
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1087
|
+
...llamacppDeps(),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (backend === "custom") {
|
|
1091
|
+
return new OllamaBackend({
|
|
1092
|
+
cwd: comfyuiPath ?? process.cwd(),
|
|
1093
|
+
model: customModel,
|
|
1094
|
+
systemAppend: panelSystemAppend,
|
|
1095
|
+
comfyuiUrl,
|
|
1096
|
+
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1097
|
+
...customDeps(),
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1033
1100
|
return undefined; // claude → built-in ClaudeBackend
|
|
1034
1101
|
};
|
|
1035
1102
|
logger.info(`[panel-orchestrator] single-port multi-provider: default backend=${defaultBackend}; ` +
|
|
@@ -1054,7 +1121,11 @@ export async function runPanelOrchestrator() {
|
|
|
1054
1121
|
? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: openrouterModel, ...openrouterDeps() })
|
|
1055
1122
|
: backend === "lmstudio"
|
|
1056
1123
|
? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: lmstudioModel, ...lmstudioDeps() })
|
|
1057
|
-
:
|
|
1124
|
+
: backend === "llamacpp"
|
|
1125
|
+
? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: llamacppModel, ...llamacppDeps() })
|
|
1126
|
+
: backend === "custom"
|
|
1127
|
+
? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: customModel, ...customDeps() })
|
|
1128
|
+
: new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
|
|
1058
1129
|
probeBackends.set(backend, pb);
|
|
1059
1130
|
}
|
|
1060
1131
|
return pb;
|
|
@@ -1274,16 +1345,20 @@ export async function runPanelOrchestrator() {
|
|
|
1274
1345
|
// without a reconnect.
|
|
1275
1346
|
const unsubscribeAgentSecrets = onAgentSecretsChanged(() => {
|
|
1276
1347
|
hydrateAgentSecretsIntoEnv();
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1348
|
+
// A key change can affect either keyed endpoint provider — drop both
|
|
1349
|
+
// caches so the next probe carries the fresh credentials.
|
|
1350
|
+
for (const b of ["openrouter", "custom"]) {
|
|
1351
|
+
modelsByBackend.delete(b);
|
|
1352
|
+
const pb = probeBackends.get(b);
|
|
1353
|
+
if (pb?.close)
|
|
1354
|
+
void pb.close().catch(() => { });
|
|
1355
|
+
probeBackends.delete(b);
|
|
1356
|
+
}
|
|
1282
1357
|
for (const tabId of tabBackends.keys()) {
|
|
1283
1358
|
pushReadiness(tabId);
|
|
1284
1359
|
pushModels(tabId);
|
|
1285
1360
|
}
|
|
1286
|
-
logger.info("[panel-orchestrator]
|
|
1361
|
+
logger.info("[panel-orchestrator] provider key saved → readiness + models refreshed");
|
|
1287
1362
|
});
|
|
1288
1363
|
// Debounce the connect ack: the panel re-sends `hello` on reconnect and on
|
|
1289
1364
|
// workflow-title changes, which would otherwise stack duplicate greetings.
|
|
@@ -1322,14 +1397,31 @@ export async function runPanelOrchestrator() {
|
|
|
1322
1397
|
p = probe.then((list) => {
|
|
1323
1398
|
if (!list.length)
|
|
1324
1399
|
modelsByBackend.delete(backend); // don't cache a failed probe
|
|
1325
|
-
// LM Studio
|
|
1326
|
-
// user downloaded) — adopt the server's first
|
|
1400
|
+
// LM Studio / llama.cpp ship no sane hardcoded default (the model is
|
|
1401
|
+
// whatever the user downloaded/launched) — adopt the server's first
|
|
1402
|
+
// offering when unset. llama-server serves exactly one model, so this
|
|
1403
|
+
// IS the model.
|
|
1327
1404
|
if (backend === "lmstudio" && !lmstudioModel && list.length) {
|
|
1328
1405
|
lmstudioModel = list[0].value ?? "";
|
|
1329
1406
|
if (lmstudioModel) {
|
|
1330
1407
|
logger.info(`[panel-orchestrator] lmstudio default model → ${lmstudioModel} (first served)`);
|
|
1331
1408
|
}
|
|
1332
1409
|
}
|
|
1410
|
+
if (backend === "llamacpp" && !llamacppModel && list.length) {
|
|
1411
|
+
llamacppModel = list[0].value ?? "";
|
|
1412
|
+
if (llamacppModel) {
|
|
1413
|
+
logger.info(`[panel-orchestrator] llamacpp model → ${llamacppModel} (the server's loaded model)`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
// Custom endpoint: same adoption — many self-hosted servers (vLLM,
|
|
1417
|
+
// llama-server, TGI) serve exactly one model, so the first listed id
|
|
1418
|
+
// is the sane default when the user hasn't named one.
|
|
1419
|
+
if (backend === "custom" && !customModel && list.length) {
|
|
1420
|
+
customModel = list[0].value ?? "";
|
|
1421
|
+
if (customModel) {
|
|
1422
|
+
logger.info(`[panel-orchestrator] custom endpoint model → ${customModel} (first served)`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1333
1425
|
// User-curated preferred models (panel Settings → set_config) pin to the
|
|
1334
1426
|
// top of the ollama picker, ahead of the discovered catalog. Read fresh
|
|
1335
1427
|
// on every probe; set_config evicts the cache so edits apply live.
|
|
@@ -1366,6 +1458,10 @@ export async function runPanelOrchestrator() {
|
|
|
1366
1458
|
return openrouterModel;
|
|
1367
1459
|
if (backend === "lmstudio")
|
|
1368
1460
|
return lmstudioModel || undefined;
|
|
1461
|
+
if (backend === "llamacpp")
|
|
1462
|
+
return llamacppModel || undefined;
|
|
1463
|
+
if (backend === "custom")
|
|
1464
|
+
return customModel || undefined;
|
|
1369
1465
|
return model;
|
|
1370
1466
|
}
|
|
1371
1467
|
function pushModels(panelTabId) {
|
|
@@ -1419,8 +1515,29 @@ export async function runPanelOrchestrator() {
|
|
|
1419
1515
|
// no CLI). The panel prefers this frame over its GET /backends probe.
|
|
1420
1516
|
function pushReadiness(tabId) {
|
|
1421
1517
|
try {
|
|
1422
|
-
const { backends, any_ready } = allBackendReadiness(KNOWN_BACKENDS
|
|
1518
|
+
const { backends, any_ready } = allBackendReadiness(KNOWN_BACKENDS, {
|
|
1519
|
+
customEndpointConfigured: !!customBaseUrl,
|
|
1520
|
+
});
|
|
1423
1521
|
bridge.push({ type: "backends", backends, any_ready }, tabId);
|
|
1522
|
+
// llama.cpp reality check (async re-push): the binary is often unzipped
|
|
1523
|
+
// anywhere (not on PATH), so static detection says "not installed" while
|
|
1524
|
+
// a server is HAPPILY ANSWERING. A live endpoint beats a missing binary —
|
|
1525
|
+
// probe it and, when it answers, re-push the frame with llamacpp ready.
|
|
1526
|
+
const lc = backends.find((b) => b.backend === "llamacpp");
|
|
1527
|
+
if (lc && !lc.ready) {
|
|
1528
|
+
void fetch(`${LLAMACPP_BASE_URL.replace(/\/v1$/, "")}/health`, {
|
|
1529
|
+
signal: AbortSignal.timeout(1500),
|
|
1530
|
+
})
|
|
1531
|
+
.then((r) => {
|
|
1532
|
+
if (!r.ok)
|
|
1533
|
+
return;
|
|
1534
|
+
lc.cli = true;
|
|
1535
|
+
lc.auth = true;
|
|
1536
|
+
lc.ready = true;
|
|
1537
|
+
bridge.push({ type: "backends", backends, any_ready: true }, tabId);
|
|
1538
|
+
})
|
|
1539
|
+
.catch(() => { });
|
|
1540
|
+
}
|
|
1424
1541
|
}
|
|
1425
1542
|
catch (err) {
|
|
1426
1543
|
logger.warn(`[panel-orchestrator] readiness probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1495,6 +1612,8 @@ export async function runPanelOrchestrator() {
|
|
|
1495
1612
|
const isOl = backend === "ollama";
|
|
1496
1613
|
const isOr = backend === "openrouter";
|
|
1497
1614
|
const isLs = backend === "lmstudio";
|
|
1615
|
+
const isLc = backend === "llamacpp";
|
|
1616
|
+
const isCu = backend === "custom";
|
|
1498
1617
|
// TRUTHFUL "connected": only claim ready after PROVING the SELECTED backend
|
|
1499
1618
|
// can run, by probing its model list. If the probe fails — the "connected
|
|
1500
1619
|
// but dead" wedge — send a degraded ack so the panel shows the real state.
|
|
@@ -1512,6 +1631,18 @@ export async function runPanelOrchestrator() {
|
|
|
1512
1631
|
logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (openrouter) but no API key — degraded ack`);
|
|
1513
1632
|
return;
|
|
1514
1633
|
}
|
|
1634
|
+
// Custom endpoint with no URL: don't dial a guess — degrade up front
|
|
1635
|
+
// with the exact fix (mirrors the OpenRouter keyless guard above).
|
|
1636
|
+
if (isCu && !customBaseUrl) {
|
|
1637
|
+
bridge.push({
|
|
1638
|
+
type: "say",
|
|
1639
|
+
text: "⚠️ No endpoint configured — set the base URL in Settings → Custom endpoint (include the /v1, e.g. http://192.168.1.20:8000/v1 for vLLM, or a hosted provider's OpenAI-compatible URL), " +
|
|
1640
|
+
"plus “Set API key…” if the server needs one. Both apply immediately — then Connect again.",
|
|
1641
|
+
}, panelTab);
|
|
1642
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
|
|
1643
|
+
logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (custom) but no base URL — degraded ack`);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1515
1646
|
void (isLs && isLocalLmstudio(LMSTUDIO_BASE_URL)
|
|
1516
1647
|
? startLmstudioServer(LMSTUDIO_BASE_URL).then(() => ensureModels(backend))
|
|
1517
1648
|
: ensureModels(backend))
|
|
@@ -1525,9 +1656,40 @@ export async function runPanelOrchestrator() {
|
|
|
1525
1656
|
? (ollamaModel ?? models[0].value ?? "Ollama")
|
|
1526
1657
|
: isLs
|
|
1527
1658
|
? (lmstudioModel || (models[0].value ?? "LM Studio"))
|
|
1528
|
-
:
|
|
1529
|
-
? (
|
|
1530
|
-
:
|
|
1659
|
+
: isLc
|
|
1660
|
+
? (llamacppModel || (models[0].value ?? "llama.cpp"))
|
|
1661
|
+
: isCu
|
|
1662
|
+
? (customModel || (models[0].value ?? "Custom endpoint"))
|
|
1663
|
+
: isOr
|
|
1664
|
+
? (openrouterModel ?? models[0].value ?? "OpenRouter")
|
|
1665
|
+
: model;
|
|
1666
|
+
// llama.cpp launch gotchas (issue #161): a reachable server can still
|
|
1667
|
+
// be useless for us — tool calling needs --jinja (rejected requests),
|
|
1668
|
+
// and a launch-time -c under ~16K silently truncates the tool payload.
|
|
1669
|
+
// Probe both and degrade/warn with the exact fix instead of letting
|
|
1670
|
+
// the first real message fail cryptically.
|
|
1671
|
+
if (isLc) {
|
|
1672
|
+
const activeModel = llamacppModel || (models[0].value ?? "");
|
|
1673
|
+
void (async () => {
|
|
1674
|
+
const [tools, props] = await Promise.all([
|
|
1675
|
+
llamacppToolsReady(LLAMACPP_BASE_URL, activeModel),
|
|
1676
|
+
llamacppProps(LLAMACPP_BASE_URL),
|
|
1677
|
+
]);
|
|
1678
|
+
if (tools === "no") {
|
|
1679
|
+
bridge.push({
|
|
1680
|
+
type: "say",
|
|
1681
|
+
text: "⚠️ Your llama-server is running WITHOUT `--jinja`, so tool calling is disabled — every agent action would fail. " +
|
|
1682
|
+
"Restart it with tool support: `llama-server -m <model>.gguf --jinja -c 16384` (current builds enable jinja by default; older ones need the flag), then Disconnect → Connect.",
|
|
1683
|
+
}, panelTab);
|
|
1684
|
+
}
|
|
1685
|
+
else if (props.nCtx && props.nCtx < 16384) {
|
|
1686
|
+
bridge.push({
|
|
1687
|
+
type: "say",
|
|
1688
|
+
text: `ℹ️ Your llama-server context is ${props.nCtx} tokens (launch flag -c). The agent's tool payload wants ≥16384 — below that, long turns silently truncate. Consider restarting with \`-c 16384\` or higher.`,
|
|
1689
|
+
}, panelTab);
|
|
1690
|
+
}
|
|
1691
|
+
})();
|
|
1692
|
+
}
|
|
1531
1693
|
// Greet only on a FRESH session (a resume/reconnect already has the thread).
|
|
1532
1694
|
if (!resume) {
|
|
1533
1695
|
const readyText = isCx
|
|
@@ -1538,9 +1700,13 @@ export async function runPanelOrchestrator() {
|
|
|
1538
1700
|
? `🟢 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.`
|
|
1539
1701
|
: isLs
|
|
1540
1702
|
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via LM Studio (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
|
|
1541
|
-
:
|
|
1542
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} via
|
|
1543
|
-
:
|
|
1703
|
+
: isLc
|
|
1704
|
+
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via llama.cpp (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
|
|
1705
|
+
: isCu
|
|
1706
|
+
? `🟢 comfyui-mcp agent ready — ${agentLabel} via your custom endpoint (${customBaseUrl}). Ask away.`
|
|
1707
|
+
: isOr
|
|
1708
|
+
? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
|
|
1709
|
+
: `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
|
|
1544
1710
|
bridge.push({ type: "say", text: readyText }, panelTab);
|
|
1545
1711
|
}
|
|
1546
1712
|
bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
|
|
@@ -1552,10 +1718,14 @@ export async function runPanelOrchestrator() {
|
|
|
1552
1718
|
: isGm
|
|
1553
1719
|
? "⚠️ The background agent isn't responding — the Gemini CLI couldn't start. Make sure the Gemini CLI is installed and signed in (run `gemini` once and complete the Google sign-in), then Disconnect → Connect to retry."
|
|
1554
1720
|
: isOl
|
|
1555
|
-
? "⚠️ The background agent isn't responding — Ollama isn't reachable. Start it with `ollama serve` and pull our fine-tuned model (`ollama pull artokun/gemma4-comfyui-mcp:e4b` — gemma4 trained on the comfyui-mcp tool suite
|
|
1721
|
+
? "⚠️ The background agent isn't responding — Ollama isn't reachable. Start it with `ollama serve` and pull our fine-tuned model (`ollama pull artokun/gemma4-comfyui-mcp:e4b` — gemma4 trained on the comfyui-mcp tool suite — arena-best local model; `:12b` for ~8 GB VRAM), then Disconnect → Connect to retry."
|
|
1556
1722
|
: isLs
|
|
1557
1723
|
? `⚠️ The background agent isn't responding — LM Studio isn't reachable at ${LMSTUDIO_BASE_URL}. Open LM Studio → Developer → Start Server and load a tool-calling model (our gemma4-comfyui-mcp GGUFs from Hugging Face work great), or set COMFYUI_MCP_LMSTUDIO_HOST if it serves elsewhere — then Disconnect → Connect to retry.`
|
|
1558
|
-
:
|
|
1724
|
+
: isLc
|
|
1725
|
+
? `⚠️ The background agent isn't responding — llama-server isn't reachable at ${LLAMACPP_BASE_URL}. Start it with \`llama-server -m <model>.gguf -c 16384\` (our gemma4-comfyui-mcp GGUFs work great; add --jinja on older builds — required there for tool calling), or set COMFYUI_MCP_LLAMACPP_HOST — then Disconnect → Connect to retry.`
|
|
1726
|
+
: isCu
|
|
1727
|
+
? `⚠️ The background agent isn't responding — your custom endpoint isn't answering at ${customBaseUrl}. Check the base URL in Settings → Custom endpoint (it must be OpenAI-compatible and include the /v1) and the API key if the server requires one — then Connect to retry.`
|
|
1728
|
+
: "⚠️ The background agent isn't responding — the Claude Agent SDK couldn't start. Make sure you're signed in (run `claude` once), then Disconnect → Connect to retry.";
|
|
1559
1729
|
bridge.push({ type: "say", text: degradedText }, panelTab);
|
|
1560
1730
|
bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
|
|
1561
1731
|
logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) but model probe empty — degraded ack`);
|
|
@@ -1654,6 +1824,18 @@ export async function runPanelOrchestrator() {
|
|
|
1654
1824
|
logger.info(`[panel-orchestrator] ollama config → model=${ollamaModel} api=${ollamaApi} host=${ollamaBaseUrl ?? "(default)"}`);
|
|
1655
1825
|
}
|
|
1656
1826
|
}
|
|
1827
|
+
const lccfg = event.llamacpp;
|
|
1828
|
+
if (lccfg && typeof lccfg === "object") {
|
|
1829
|
+
const o = lccfg;
|
|
1830
|
+
if (typeof o.model === "string" && o.model.trim()) {
|
|
1831
|
+
const m = o.model.trim();
|
|
1832
|
+
if (!process.env.COMFYUI_MCP_LLAMACPP_MODEL)
|
|
1833
|
+
llamacppModel = m;
|
|
1834
|
+
setAgentSettings({ llamacpp: { model: m } });
|
|
1835
|
+
modelsByBackend.delete("llamacpp");
|
|
1836
|
+
logger.info(`[panel-orchestrator] llamacpp config → model=${llamacppModel}`);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1657
1839
|
const lcfg = event.lmstudio;
|
|
1658
1840
|
if (lcfg && typeof lcfg === "object") {
|
|
1659
1841
|
const o = lcfg;
|
|
@@ -1670,6 +1852,35 @@ export async function runPanelOrchestrator() {
|
|
|
1670
1852
|
logger.info(`[panel-orchestrator] lmstudio config → model=${lmstudioModel}`);
|
|
1671
1853
|
}
|
|
1672
1854
|
}
|
|
1855
|
+
// Custom endpoint (issue #162): base_url + model, both user-supplied.
|
|
1856
|
+
// base_url accepts "" (clears the endpoint → provider flips unready);
|
|
1857
|
+
// either change evicts the probe/model caches so the next connect dials
|
|
1858
|
+
// the new target, and re-pushes readiness so the picker flips live.
|
|
1859
|
+
const cucfg = event.custom;
|
|
1860
|
+
if (cucfg && typeof cucfg === "object") {
|
|
1861
|
+
const o = cucfg;
|
|
1862
|
+
const patch = {};
|
|
1863
|
+
if (typeof o.model === "string" && o.model.trim()) {
|
|
1864
|
+
patch.model = o.model.trim();
|
|
1865
|
+
if (!process.env.COMFYUI_MCP_CUSTOM_MODEL)
|
|
1866
|
+
customModel = patch.model;
|
|
1867
|
+
}
|
|
1868
|
+
if (typeof o.base_url === "string") {
|
|
1869
|
+
patch.baseUrl = o.base_url.trim().replace(/[/]$/, "");
|
|
1870
|
+
if (!process.env.COMFYUI_MCP_CUSTOM_BASE_URL)
|
|
1871
|
+
customBaseUrl = patch.baseUrl;
|
|
1872
|
+
}
|
|
1873
|
+
if (Object.keys(patch).length) {
|
|
1874
|
+
setAgentSettings({ custom: patch });
|
|
1875
|
+
const pb = probeBackends.get("custom");
|
|
1876
|
+
if (pb?.close)
|
|
1877
|
+
void pb.close().catch(() => { });
|
|
1878
|
+
probeBackends.delete("custom");
|
|
1879
|
+
modelsByBackend.delete("custom");
|
|
1880
|
+
pushReadiness(event.tab_id);
|
|
1881
|
+
logger.info(`[panel-orchestrator] custom endpoint config → model=${customModel || "(first served)"} host=${customBaseUrl || "(unset)"}`);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1673
1884
|
if (ollamaChanged) {
|
|
1674
1885
|
modelsByBackend.delete("ollama");
|
|
1675
1886
|
pushModels(event.tab_id);
|
|
@@ -1677,13 +1888,16 @@ export async function runPanelOrchestrator() {
|
|
|
1677
1888
|
bridge.push({ type: "ack", ok: true, kind: "config" }, event.tab_id);
|
|
1678
1889
|
return;
|
|
1679
1890
|
}
|
|
1680
|
-
// Panel-initiated
|
|
1891
|
+
// Panel-initiated secret (Settings › "Set API key/token…") — NO agent, no
|
|
1681
1892
|
// chat: the panel paints its own masked input and ships the value here
|
|
1682
1893
|
// directly, over the same loopback/token-gated bridge the agent-initiated
|
|
1683
|
-
// request_secret reply already rides.
|
|
1684
|
-
// (OPENROUTER_API_KEY
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1894
|
+
// request_secret reply already rides. Routed by allowlist: PROVIDER keys
|
|
1895
|
+
// (OPENROUTER_API_KEY, COMFYUI_MCP_CUSTOM_API_KEY) → setAgentSecret, which
|
|
1896
|
+
// persists 0600 and hydrates process.env immediately so the refreshed
|
|
1897
|
+
// readiness frame flips the provider picker live; comfyui TOOL tokens
|
|
1898
|
+
// (CivitAI/HuggingFace) → setComfyuiSecret, whose change event already
|
|
1899
|
+
// re-injects the MCP child env + respawns on idle — so EVERY token button
|
|
1900
|
+
// works agent-free with just the bridge connected (no chicken-and-egg).
|
|
1687
1901
|
if (event.type === "set_secret" && event.tab_id) {
|
|
1688
1902
|
const rawKey = event.key;
|
|
1689
1903
|
const rawValue = event.value;
|
|
@@ -1693,8 +1907,11 @@ export async function runPanelOrchestrator() {
|
|
|
1693
1907
|
try {
|
|
1694
1908
|
if (!value.trim())
|
|
1695
1909
|
throw new Error("No token entered — nothing was saved.");
|
|
1696
|
-
|
|
1697
|
-
|
|
1910
|
+
if (isAllowedComfyuiSecretKey(key))
|
|
1911
|
+
setComfyuiSecret(key, value.trim());
|
|
1912
|
+
else
|
|
1913
|
+
setAgentSecret(key, value.trim());
|
|
1914
|
+
logger.info(`[panel-orchestrator] secret set from panel Settings: ${key} (redacted)`);
|
|
1698
1915
|
}
|
|
1699
1916
|
catch (err) {
|
|
1700
1917
|
error = err instanceof Error ? err.message : String(err);
|
|
@@ -1704,6 +1921,49 @@ export async function runPanelOrchestrator() {
|
|
|
1704
1921
|
pushReadiness(event.tab_id);
|
|
1705
1922
|
return;
|
|
1706
1923
|
}
|
|
1924
|
+
// Phone pairing: mint a phone-reachable bridge URL for the panel's QR modal.
|
|
1925
|
+
// `lan` → ws://<lan-ip>:<pairPort>/?token= (same wifi); `tunnel` → a cloudflared
|
|
1926
|
+
// wss://…/?token= (anywhere). Both hit the on-demand token-gated pairing
|
|
1927
|
+
// listener; async (tunnel startup can fail), so reply via a typed frame.
|
|
1928
|
+
if (event.type === "pair" && event.tab_id) {
|
|
1929
|
+
const mode = event.mode === "tunnel" ? "tunnel" : "lan";
|
|
1930
|
+
const tabId = event.tab_id;
|
|
1931
|
+
void (async () => {
|
|
1932
|
+
const token = await ensurePairListener();
|
|
1933
|
+
let url;
|
|
1934
|
+
if (mode === "tunnel") {
|
|
1935
|
+
if (!pairTunnel) {
|
|
1936
|
+
const t = await startQuickTunnel(pairPort, "127.0.0.1");
|
|
1937
|
+
pairTunnel = { url: t.url, stop: t.stop };
|
|
1938
|
+
process.once("exit", () => {
|
|
1939
|
+
try {
|
|
1940
|
+
pairTunnel?.stop();
|
|
1941
|
+
}
|
|
1942
|
+
catch {
|
|
1943
|
+
/* best-effort */
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
const u = new URL(pairTunnel.url);
|
|
1948
|
+
u.protocol = "wss:";
|
|
1949
|
+
u.search = "";
|
|
1950
|
+
u.searchParams.set("token", token);
|
|
1951
|
+
url = u.toString();
|
|
1952
|
+
}
|
|
1953
|
+
else {
|
|
1954
|
+
const ip = firstLanIPv4();
|
|
1955
|
+
if (!ip) {
|
|
1956
|
+
throw new Error("No LAN network found — connect this machine to wifi/ethernet, or use the Internet (tunnel) option.");
|
|
1957
|
+
}
|
|
1958
|
+
url = `ws://${ip}:${pairPort}/?token=${token}`;
|
|
1959
|
+
}
|
|
1960
|
+
logger.info(`[panel-orchestrator] pairing URL minted (${mode})`);
|
|
1961
|
+
bridge.push({ type: "pair_url", mode, url }, tabId);
|
|
1962
|
+
})().catch((err) => {
|
|
1963
|
+
bridge.push({ type: "pair_error", mode, error: err instanceof Error ? err.message : String(err) }, tabId);
|
|
1964
|
+
});
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1707
1967
|
// Model / effort picker: apply and confirm. Model switches live; an effort
|
|
1708
1968
|
// change restarts the session (resumed) so the conversation carries over.
|
|
1709
1969
|
if (event.type === "set_options" && event.tab_id) {
|