comfyui-mcp 0.14.0 → 0.16.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/CHANGELOG.md +96 -0
- package/PACK-SPLIT-STATUS.md +87 -0
- package/README.md +10 -4
- package/assets/controlnet_demo.png +0 -0
- package/assets/ideogram_demo.png +0 -0
- package/assets/ltx_sharp_demo.png +0 -0
- package/assets/ltx_t2v_demo.png +0 -0
- package/assets/qwen_edit_demo.png +0 -0
- package/assets/sample_woman.png +0 -0
- package/assets/sample_woman_video.mp4 +0 -0
- package/assets/wan_demo.png +0 -0
- package/assets/wan_sharp_demo.png +0 -0
- package/assets/wan_transparent_demo.png +0 -0
- package/assets/wan_v2v_demo.png +0 -0
- package/dist/comfyui/client.js +39 -4
- package/dist/comfyui/client.js.map +1 -1
- package/dist/index.js +3 -65
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/index.js +547 -27
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +814 -59
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +437 -0
- package/dist/orchestrator/panel-tools.js.map +1 -0
- package/dist/services/download-cache.js +48 -9
- package/dist/services/download-cache.js.map +1 -1
- package/dist/services/download-progress.js +57 -0
- package/dist/services/download-progress.js.map +1 -0
- package/dist/services/extra-paths.js +208 -0
- package/dist/services/extra-paths.js.map +1 -0
- package/dist/services/job-watcher.js +28 -1
- package/dist/services/job-watcher.js.map +1 -1
- package/dist/services/model-resolver.js +23 -7
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-management.js +99 -49
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/panel-settings.js +53 -0
- package/dist/services/panel-settings.js.map +1 -0
- package/dist/services/queue-manager.js +98 -9
- package/dist/services/queue-manager.js.map +1 -1
- package/dist/services/ui-bridge.js +40 -9
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/user-mcp-config.js +111 -0
- package/dist/services/user-mcp-config.js.map +1 -0
- package/dist/services/workflow-converter.js +420 -62
- package/dist/services/workflow-converter.js.map +1 -1
- package/dist/tools/diagnostics.js +22 -9
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/extra-paths.js +87 -0
- package/dist/tools/extra-paths.js.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/queue-management.js +65 -3
- package/dist/tools/queue-management.js.map +1 -1
- package/dist/tools/report-issue.js +56 -0
- package/dist/tools/report-issue.js.map +1 -0
- package/dist/tools/workflow-library.js +6 -3
- package/dist/tools/workflow-library.js.map +1 -1
- package/dist/transport/cli.js +1 -8
- package/dist/transport/cli.js.map +1 -1
- package/model-settings.json +1 -1
- package/package.json +5 -2
- package/packs/anima/install-runpod.sh +12 -1
- package/packs/anima/install-windows.bat +11 -1
- package/packs/anima/workflow.json +87 -87
- package/packs/anima-img2img/install-runpod.sh +56 -0
- package/packs/anima-img2img/install-windows.bat +61 -0
- package/packs/anima-img2img/manifest.yaml +69 -0
- package/packs/anima-img2img/pack.yaml +30 -0
- package/packs/anima-img2img/workflow.json +7076 -0
- package/packs/anima-inpaint/install-runpod.sh +49 -0
- package/packs/anima-inpaint/install-windows.bat +54 -0
- package/packs/anima-inpaint/manifest.yaml +50 -0
- package/packs/anima-inpaint/pack.yaml +29 -0
- package/packs/anima-inpaint/workflow.json +2009 -0
- package/packs/anima-txt2img/install-runpod.sh +55 -0
- package/packs/anima-txt2img/install-windows.bat +60 -0
- package/packs/anima-txt2img/manifest.yaml +62 -0
- package/packs/anima-txt2img/pack.yaml +28 -0
- package/packs/anima-txt2img/workflow.json +6442 -0
- package/packs/cozy-flow/install-runpod.sh +12 -1
- package/packs/cozy-flow/install-windows.bat +11 -1
- package/packs/ernie/_slices/combo.json +9661 -0
- package/packs/ernie/_slices/img2img.json +2568 -0
- package/packs/ernie/_slices/txt2img.json +2379 -0
- package/packs/ernie/install-runpod.sh +18 -1
- package/packs/ernie/install-windows.bat +17 -1
- package/packs/ernie/manifest.yaml +22 -4
- package/packs/ernie/pack.yaml +12 -5
- package/packs/ernie/workflow.json +1 -1
- package/packs/ernie-combo/install-runpod.sh +50 -0
- package/packs/ernie-combo/install-windows.bat +55 -0
- package/packs/ernie-combo/manifest.yaml +40 -0
- package/packs/ernie-combo/pack.yaml +37 -0
- package/packs/ernie-combo/workflow.json +9661 -0
- package/packs/ernie-img2img/install-runpod.sh +47 -0
- package/packs/ernie-img2img/install-windows.bat +52 -0
- package/packs/ernie-img2img/manifest.yaml +35 -0
- package/packs/ernie-img2img/pack.yaml +35 -0
- package/packs/ernie-img2img/workflow.json +2568 -0
- package/packs/ernie-txt2img/install-runpod.sh +47 -0
- package/packs/ernie-txt2img/install-windows.bat +52 -0
- package/packs/ernie-txt2img/manifest.yaml +35 -0
- package/packs/ernie-txt2img/pack.yaml +38 -0
- package/packs/ernie-txt2img/workflow.json +2379 -0
- package/packs/ideogram/install-runpod.sh +12 -1
- package/packs/ideogram/install-windows.bat +11 -1
- package/packs/ideogram-img2img/install-runpod.sh +40 -0
- package/packs/ideogram-img2img/install-windows.bat +45 -0
- package/packs/ideogram-img2img/manifest.yaml +34 -0
- package/packs/ideogram-img2img/pack.yaml +32 -0
- package/packs/ideogram-img2img/workflow.json +3124 -0
- package/packs/ideogram-txt2img/install-runpod.sh +40 -0
- package/packs/ideogram-txt2img/install-windows.bat +45 -0
- package/packs/ideogram-txt2img/manifest.yaml +33 -0
- package/packs/ideogram-txt2img/pack.yaml +30 -0
- package/packs/ideogram-txt2img/workflow.json +3041 -0
- package/packs/ltx-2.3/install-runpod.sh +12 -1
- package/packs/ltx-2.3/install-windows.bat +11 -1
- package/packs/ltx-2.3-extender/install-runpod.sh +55 -0
- package/packs/ltx-2.3-extender/install-windows.bat +60 -0
- package/packs/ltx-2.3-extender/manifest.yaml +58 -0
- package/packs/ltx-2.3-extender/pack.yaml +20 -0
- package/packs/ltx-2.3-extender/workflow.json +5772 -0
- package/packs/ltx-2.3-extender-no-audio/install-runpod.sh +55 -0
- package/packs/ltx-2.3-extender-no-audio/install-windows.bat +60 -0
- package/packs/ltx-2.3-extender-no-audio/manifest.yaml +58 -0
- package/packs/ltx-2.3-extender-no-audio/pack.yaml +20 -0
- package/packs/ltx-2.3-extender-no-audio/workflow.json +5968 -0
- package/packs/ltx-2.3-flf/install-runpod.sh +55 -0
- package/packs/ltx-2.3-flf/install-windows.bat +60 -0
- package/packs/ltx-2.3-flf/manifest.yaml +58 -0
- package/packs/ltx-2.3-flf/pack.yaml +20 -0
- package/packs/ltx-2.3-flf/workflow.json +6173 -0
- package/packs/ltx-2.3-img2vid/install-runpod.sh +40 -0
- package/packs/ltx-2.3-img2vid/install-windows.bat +45 -0
- package/packs/ltx-2.3-img2vid/manifest.yaml +41 -0
- package/packs/ltx-2.3-img2vid/pack.yaml +20 -0
- package/packs/ltx-2.3-img2vid/workflow.json +5062 -0
- package/packs/ltx-2.3-txt2vid/install-runpod.sh +40 -0
- package/packs/ltx-2.3-txt2vid/install-windows.bat +45 -0
- package/packs/ltx-2.3-txt2vid/manifest.yaml +40 -0
- package/packs/ltx-2.3-txt2vid/pack.yaml +20 -0
- package/packs/ltx-2.3-txt2vid/workflow.json +1 -0
- package/packs/ltx-2.3-xy-plot/install-runpod.sh +55 -0
- package/packs/ltx-2.3-xy-plot/install-windows.bat +60 -0
- package/packs/ltx-2.3-xy-plot/manifest.yaml +58 -0
- package/packs/ltx-2.3-xy-plot/pack.yaml +24 -0
- package/packs/ltx-2.3-xy-plot/workflow.json +8103 -0
- package/packs/qwen-image/install-runpod.sh +12 -1
- package/packs/qwen-image/install-windows.bat +11 -1
- package/packs/qwen-image-edit/install-runpod.sh +12 -1
- package/packs/qwen-image-edit/install-windows.bat +11 -1
- package/packs/qwen-image-edit-edit/install-runpod.sh +47 -0
- package/packs/qwen-image-edit-edit/install-windows.bat +52 -0
- package/packs/qwen-image-edit-edit/manifest.yaml +68 -0
- package/packs/qwen-image-edit-edit/pack.yaml +34 -0
- package/packs/qwen-image-edit-edit/workflow.json +980 -0
- package/packs/wan-animate/install-runpod.sh +12 -1
- package/packs/wan-animate/install-windows.bat +11 -1
- package/packs/wan-animate-character/install-runpod.sh +48 -0
- package/packs/wan-animate-character/install-windows.bat +53 -0
- package/packs/wan-animate-character/manifest.yaml +53 -0
- package/packs/wan-animate-character/pack.yaml +44 -0
- package/packs/wan-animate-character/workflow.json +1 -0
- package/packs/wan-longer-videos/install-runpod.sh +12 -1
- package/packs/wan-longer-videos/install-windows.bat +11 -1
- package/packs/wan-longer-videos-i2v/install-runpod.sh +44 -0
- package/packs/wan-longer-videos-i2v/install-windows.bat +49 -0
- package/packs/wan-longer-videos-i2v/manifest.yaml +39 -0
- package/packs/wan-longer-videos-i2v/pack.yaml +40 -0
- package/packs/wan-longer-videos-i2v/workflow.json +1 -0
- package/packs/wan-longer-videos-i2v-96gb/install-runpod.sh +44 -0
- package/packs/wan-longer-videos-i2v-96gb/install-windows.bat +49 -0
- package/packs/wan-longer-videos-i2v-96gb/manifest.yaml +40 -0
- package/packs/wan-longer-videos-i2v-96gb/pack.yaml +40 -0
- package/packs/wan-longer-videos-i2v-96gb/workflow.json +1 -0
- package/packs/wan-longer-videos-t2v/install-runpod.sh +49 -0
- package/packs/wan-longer-videos-t2v/install-windows.bat +54 -0
- package/packs/wan-longer-videos-t2v/manifest.yaml +50 -0
- package/packs/wan-longer-videos-t2v/pack.yaml +40 -0
- package/packs/wan-longer-videos-t2v/workflow.json +1 -0
- package/packs/wan-longer-videos-t2v-96gb/install-runpod.sh +49 -0
- package/packs/wan-longer-videos-t2v-96gb/install-windows.bat +54 -0
- package/packs/wan-longer-videos-t2v-96gb/manifest.yaml +50 -0
- package/packs/wan-longer-videos-t2v-96gb/pack.yaml +40 -0
- package/packs/wan-longer-videos-t2v-96gb/workflow.json +1 -0
- package/packs/wan-longer-videos-v2v/install-runpod.sh +44 -0
- package/packs/wan-longer-videos-v2v/install-windows.bat +49 -0
- package/packs/wan-longer-videos-v2v/manifest.yaml +39 -0
- package/packs/wan-longer-videos-v2v/pack.yaml +40 -0
- package/packs/wan-longer-videos-v2v/workflow.json +1 -0
- package/packs/wan-longer-videos-v2v-96gb/install-runpod.sh +44 -0
- package/packs/wan-longer-videos-v2v-96gb/install-windows.bat +49 -0
- package/packs/wan-longer-videos-v2v-96gb/manifest.yaml +40 -0
- package/packs/wan-longer-videos-v2v-96gb/pack.yaml +40 -0
- package/packs/wan-longer-videos-v2v-96gb/workflow.json +1 -0
- package/packs/wan-transparent/install-runpod.sh +12 -1
- package/packs/wan-transparent/install-windows.bat +11 -1
- package/packs/wan-transparent-img2vid/install-runpod.sh +45 -0
- package/packs/wan-transparent-img2vid/install-windows.bat +50 -0
- package/packs/wan-transparent-img2vid/manifest.yaml +47 -0
- package/packs/wan-transparent-img2vid/pack.yaml +44 -0
- package/packs/wan-transparent-img2vid/workflow.json +1 -0
- package/packs/wan-transparent-img2vid-96gb/install-runpod.sh +45 -0
- package/packs/wan-transparent-img2vid-96gb/install-windows.bat +50 -0
- package/packs/wan-transparent-img2vid-96gb/manifest.yaml +48 -0
- package/packs/wan-transparent-img2vid-96gb/pack.yaml +44 -0
- package/packs/wan-transparent-img2vid-96gb/workflow.json +1 -0
- package/packs/z-image-base/install-runpod.sh +13 -2
- package/packs/z-image-base/install-windows.bat +12 -2
- package/packs/z-image-base/manifest.yaml +2 -2
- package/packs/z-image-base/workflow.json +1 -1
- package/packs/z-image-base-combo/install-runpod.sh +51 -0
- package/packs/z-image-base-combo/install-windows.bat +56 -0
- package/packs/z-image-base-combo/manifest.yaml +42 -0
- package/packs/z-image-base-combo/pack.yaml +31 -0
- package/packs/z-image-base-combo/workflow.json +4918 -0
- package/packs/z-image-base-controlnet/install-runpod.sh +53 -0
- package/packs/z-image-base-controlnet/install-windows.bat +58 -0
- package/packs/z-image-base-controlnet/manifest.yaml +51 -0
- package/packs/z-image-base-controlnet/pack.yaml +33 -0
- package/packs/z-image-base-controlnet/workflow.json +3992 -0
- package/packs/z-image-base-img2img/install-runpod.sh +50 -0
- package/packs/z-image-base-img2img/install-windows.bat +55 -0
- package/packs/z-image-base-img2img/manifest.yaml +37 -0
- package/packs/z-image-base-img2img/pack.yaml +29 -0
- package/packs/z-image-base-img2img/workflow.json +1621 -0
- package/packs/z-image-base-inpaint/install-runpod.sh +47 -0
- package/packs/z-image-base-inpaint/install-windows.bat +52 -0
- package/packs/z-image-base-inpaint/manifest.yaml +34 -0
- package/packs/z-image-base-inpaint/pack.yaml +29 -0
- package/packs/z-image-base-inpaint/workflow.json +1780 -0
- package/packs/z-image-base-txt2img/install-runpod.sh +50 -0
- package/packs/z-image-base-txt2img/install-windows.bat +55 -0
- package/packs/z-image-base-txt2img/manifest.yaml +37 -0
- package/packs/z-image-base-txt2img/pack.yaml +29 -0
- package/packs/z-image-base-txt2img/workflow.json +1416 -0
- package/packs/z-image-turbo/install-runpod.sh +13 -2
- package/packs/z-image-turbo/install-windows.bat +12 -2
- package/packs/z-image-turbo/manifest.yaml +1 -1
- package/packs/z-image-turbo/workflow.json +7 -7
- package/packs/z-image-turbo-combo/install-runpod.sh +50 -0
- package/packs/z-image-turbo-combo/install-windows.bat +55 -0
- package/packs/z-image-turbo-combo/manifest.yaml +35 -0
- package/packs/z-image-turbo-combo/pack.yaml +31 -0
- package/packs/z-image-turbo-combo/workflow.json +2038 -0
- package/packs/z-image-turbo-controlnet/install-runpod.sh +52 -0
- package/packs/z-image-turbo-controlnet/install-windows.bat +57 -0
- package/packs/z-image-turbo-controlnet/manifest.yaml +45 -0
- package/packs/z-image-turbo-controlnet/pack.yaml +33 -0
- package/packs/z-image-turbo-controlnet/workflow.json +2607 -0
- package/packs/z-image-turbo-detail-daemon/install-runpod.sh +50 -0
- package/packs/z-image-turbo-detail-daemon/install-windows.bat +55 -0
- package/packs/z-image-turbo-detail-daemon/manifest.yaml +35 -0
- package/packs/z-image-turbo-detail-daemon/pack.yaml +31 -0
- package/packs/z-image-turbo-detail-daemon/workflow.json +1963 -0
- package/packs/z-image-turbo-img2img/install-runpod.sh +50 -0
- package/packs/z-image-turbo-img2img/install-windows.bat +55 -0
- package/packs/z-image-turbo-img2img/manifest.yaml +35 -0
- package/packs/z-image-turbo-img2img/pack.yaml +28 -0
- package/packs/z-image-turbo-img2img/workflow.json +1285 -0
- package/packs/z-image-turbo-inpainting/install-runpod.sh +47 -0
- package/packs/z-image-turbo-inpainting/install-windows.bat +52 -0
- package/packs/z-image-turbo-inpainting/manifest.yaml +32 -0
- package/packs/z-image-turbo-inpainting/pack.yaml +28 -0
- package/packs/z-image-turbo-inpainting/workflow.json +1131 -0
- package/packs/z-image-turbo-txt2img/install-runpod.sh +44 -0
- package/packs/z-image-turbo-txt2img/install-windows.bat +49 -0
- package/packs/z-image-turbo-txt2img/manifest.yaml +29 -0
- package/packs/z-image-turbo-txt2img/pack.yaml +27 -0
- package/packs/z-image-turbo-txt2img/workflow.json +1137 -0
- package/packs/z-image-xy-plot/install-runpod.sh +13 -2
- package/packs/z-image-xy-plot/install-windows.bat +12 -2
- package/packs/z-image-xy-plot/manifest.yaml +1 -1
- package/packs/z-image-xy-plot/pack.yaml +1 -1
- package/packs/z-image-xy-plot/workflow.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/skills/ernie-image/SKILL.md +12 -0
- package/plugin/skills/flux-txt2img/SKILL.md +1 -1
- package/plugin/skills/ltxv2-video/SKILL.md +51 -0
- package/plugin/skills/report-bug/SKILL.md +135 -0
- package/plugin/skills/workflow-layout/SKILL.md +114 -0
- package/plugin/skills/z-image-txt2img/SKILL.md +3 -3
- package/scripts/check-pack-models.mjs +155 -0
- package/scripts/gen-pack-installers.mjs +25 -7
- package/scripts/gen-tool-docs.ts +3 -1
- package/scripts/mock-panel.mjs +156 -0
- package/scripts/probe-bridge.mjs +39 -0
- package/scripts/probe-models.mjs +18 -0
- package/scripts/slice-pipeline.mjs +79 -0
- package/scripts/test-agent.mjs +313 -0
- package/scripts/verify-render.mjs +103 -0
- package/dist/tools/panel.js +0 -249
- package/dist/tools/panel.js.map +0 -1
|
@@ -3,21 +3,100 @@
|
|
|
3
3
|
// Claude session stays free. Launch with `comfyui-mcp --panel-orchestrator`
|
|
4
4
|
// (or COMFYUI_MCP_PANEL_ORCHESTRATOR=1).
|
|
5
5
|
//
|
|
6
|
-
// It owns the UI bridge (port
|
|
6
|
+
// It owns the UI bridge (port 9180) directly — so it SEES panel messages instead
|
|
7
7
|
// of relying on an idle interactive session to notice a channel push — and spawns
|
|
8
8
|
// one Claude Agent SDK streaming session per panel tab (src/orchestrator/
|
|
9
9
|
// panel-agent.ts). Each agent runs on the user's Claude SUBSCRIPTION with no API
|
|
10
10
|
// key. See docs/design/panel-orchestrator.md.
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
11
|
+
import { existsSync, writeFileSync, unlinkSync, readFileSync, readdirSync } from "node:fs";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
12
15
|
import { fileURLToPath } from "node:url";
|
|
13
16
|
import { startUiBridge } from "../services/ui-bridge.js";
|
|
14
17
|
import { logger } from "../utils/logger.js";
|
|
15
|
-
import { PanelAgentManager } from "./panel-agent.js";
|
|
18
|
+
import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffort, } from "./panel-agent.js";
|
|
19
|
+
import { createPanelMcpServer } from "./panel-tools.js";
|
|
20
|
+
import { readUserMcpServers } from "../services/user-mcp-config.js";
|
|
16
21
|
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.
|
|
17
22
|
|
|
18
|
-
You
|
|
23
|
+
You can SEE and EDIT the workflow the user currently has open, via the panel_* tools (panel_get_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 with panel_get_graph first, 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).
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
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. 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).
|
|
26
|
+
|
|
27
|
+
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_get_graph and the panel_* edit tools operate on the subgraph's inner nodes — and panel_exit_subgraph when you're done.
|
|
28
|
+
|
|
29
|
+
You also have the comfyui MCP tools to generate images, video, and audio and to inspect, download models for, and manage their ComfyUI instance. Use them to actually do what's asked, then tell them what you did and name or link any output. If a request is ambiguous, make a sensible choice and say what you chose rather than stalling.
|
|
30
|
+
|
|
31
|
+
You are running in the background on the user's own machine. For routine, reversible actions that follow from the request, act without asking permission.
|
|
32
|
+
|
|
33
|
+
You can extend your own capabilities by connecting MCP servers: panel_list_mcp shows what's connected, panel_add_mcp writes a new server to the user's Claude config, and panel_remove_mcp removes one — then call panel_reload to load the change into this session (it restarts you and resumes automatically). For example, if a task needs Civitai model search and it isn't connected, offer to add the official CivitAI MCP (transport 'http', url 'https://mcp.civitai.com/mcp'), then reload. ALWAYS ask the user before connecting a remote MCP — it's an external service connection. After editing your own orchestrator/panel code, you can also call panel_reload to pick it up without a ComfyUI restart.
|
|
34
|
+
|
|
35
|
+
When you genuinely need the user to choose between options, use the panel_ask tool — it renders an interactive question card in the panel chat and returns their pick (the card always includes an 'Other…' free-text field, so they can answer freely too). Reserve it for decisions that actually change what you do; for a simple yes/no or quick confirmation a plain-text question in your reply is fine. Do NOT use the built-in AskUserQuestion tool — it does not render in this panel and gets auto-dismissed, which makes you think the user declined.
|
|
36
|
+
|
|
37
|
+
For any genuinely multi-step task, set a plan with panel_set_todo and then WORK THROUGH IT LIKE A LOOP: do each step, mark it done and the next one active as you go, and keep going autonomously all the way to the end. Do NOT stop between steps to ask "should I continue?", to report routine progress, or to seek permission for steps that plainly follow from the plan — the todo list is your commitment to FINISH, not a menu to re-confirm. Clear it (panel_set_todo with an empty array) once every item is done. The ONLY reason to pause the loop is a decision you genuinely cannot make yourself or a true blocker — then use panel_ask to get the answer and immediately resume the loop. When the user hands you a list of things to do, default to executing the WHOLE list before yielding the turn.
|
|
38
|
+
|
|
39
|
+
Adult / NSFW content is gated behind an explicit, persistent consent mode — query it with panel_get_content_mode (it defaults to OFF). ComfyUI has a large community of legitimate adult artists, so adult requests are valid creative work — but only for verified consenting adults. BY DEFAULT (mode OFF) keep everything safe-for-work: don't surface NSFW models, LoRAs, prompts, workflows, or imagery, and prefer SFW results. When a request clearly intends adult/NSFW work and the mode is OFF, call panel_request_adult_consent — it shows a card confirming the user is 18+ and that adult content is legal in their region; only once they affirm does the mode turn on (it persists across reloads, and panel_disable_adult_mode reverts it). When the mode is ON, help with legal adult art for consenting adults and don't over-refuse — stylized/fantasy themes between clearly-adult fictional characters are in scope. ABSOLUTE limits that NO mode, setting, or request ever relaxes: never sexual content involving minors or anyone depicted as underage; never sexual deepfakes of real, identifiable people; never depictions of actual non-consensual sexual acts (rape). If a request crosses these, refuse regardless of the mode.`;
|
|
40
|
+
/**
|
|
41
|
+
* Lockfile path for a given bridge port. The orchestrator self-registers its
|
|
42
|
+
* REAL node pid here (not the npx shim's), plus the ComfyUI pid that launched
|
|
43
|
+
* it, so the panel pack can reliably identify and replace a stale orchestrator
|
|
44
|
+
* left over from a previous ComfyUI session (the "orphan on the port" trap).
|
|
45
|
+
*/
|
|
46
|
+
function orchLockPath(port) {
|
|
47
|
+
return join(tmpdir(), `comfyui-mcp-panel-orch-${port}.json`);
|
|
48
|
+
}
|
|
49
|
+
function readWindowsProcessStartedAtMs(pid) {
|
|
50
|
+
const script = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
|
|
51
|
+
`if ($p) { ([Management.ManagementDateTimeConverter]::ToDateTime($p.CreationDate)).ToUniversalTime().ToString("o") }`;
|
|
52
|
+
for (const exe of ["powershell.exe", "powershell"]) {
|
|
53
|
+
try {
|
|
54
|
+
const out = execFileSync(exe, ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
timeout: 2000,
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
}).trim();
|
|
59
|
+
if (!out)
|
|
60
|
+
return null;
|
|
61
|
+
const ms = Date.parse(out);
|
|
62
|
+
return Number.isFinite(ms) ? ms : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Try the next PowerShell executable name.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function readProcessStartedAtMs(pid) {
|
|
71
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
72
|
+
return null;
|
|
73
|
+
if (process.platform === "win32")
|
|
74
|
+
return readWindowsProcessStartedAtMs(pid);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function pidExists(pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0); // signal 0 = existence probe, doesn't actually signal
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// EPERM = exists but not ours to signal.
|
|
84
|
+
return err.code === "EPERM";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function parentIdentityMatches(pid, expectedStartedAtMs) {
|
|
88
|
+
if (!pidExists(pid))
|
|
89
|
+
return false;
|
|
90
|
+
if (!expectedStartedAtMs)
|
|
91
|
+
return true; // legacy/manual launch: PID liveness only.
|
|
92
|
+
const actualStartedAtMs = readProcessStartedAtMs(pid);
|
|
93
|
+
// Couldn't read the start time (transient PowerShell failure / no reader): the
|
|
94
|
+
// pid IS alive, so DON'T false-positive "parent gone" and suicide — fall back
|
|
95
|
+
// to liveness. The pack's Connect-time orphan check is the backstop for reuse.
|
|
96
|
+
if (!actualStartedAtMs)
|
|
97
|
+
return true;
|
|
98
|
+
return Math.abs(actualStartedAtMs - expectedStartedAtMs) <= 2000;
|
|
99
|
+
}
|
|
21
100
|
/**
|
|
22
101
|
* Tie the orchestrator's lifetime to ComfyUI's. The launcher (the panel pack)
|
|
23
102
|
* passes its own PID as COMFYUI_MCP_PARENT_PID; we poll whether that process is
|
|
@@ -31,16 +110,20 @@ function startParentWatchdog(onParentGone) {
|
|
|
31
110
|
const ppid = raw ? Number(raw) : NaN;
|
|
32
111
|
if (!Number.isInteger(ppid) || ppid <= 0)
|
|
33
112
|
return;
|
|
113
|
+
const expectedStartedAtMs = Number(process.env.COMFYUI_MCP_PARENT_STARTED_AT_MS) || null;
|
|
114
|
+
// Cheap pid-liveness probe every 5s; the expensive start-time identity check
|
|
115
|
+
// (which shells out to PowerShell on Windows) only every ~30s — enough to
|
|
116
|
+
// catch pid reuse without spawning a process every 5s for the orchestrator's
|
|
117
|
+
// whole life.
|
|
118
|
+
let polls = 0;
|
|
34
119
|
const timer = setInterval(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// ESRCH = gone; EPERM = exists but not ours to signal (treat as alive).
|
|
41
|
-
alive = err.code === "EPERM";
|
|
120
|
+
polls += 1;
|
|
121
|
+
if (!pidExists(ppid)) {
|
|
122
|
+
clearInterval(timer);
|
|
123
|
+
onParentGone();
|
|
124
|
+
return;
|
|
42
125
|
}
|
|
43
|
-
if (!
|
|
126
|
+
if (expectedStartedAtMs && polls % 6 === 0 && !parentIdentityMatches(ppid, expectedStartedAtMs)) {
|
|
44
127
|
clearInterval(timer);
|
|
45
128
|
onParentGone();
|
|
46
129
|
}
|
|
@@ -54,25 +137,80 @@ function startParentWatchdog(onParentGone) {
|
|
|
54
137
|
* process alive until SIGINT/SIGTERM or the parent (ComfyUI) exits.
|
|
55
138
|
*/
|
|
56
139
|
export async function runPanelOrchestrator() {
|
|
140
|
+
// Crash guard: the orchestrator is a long-lived background process the user
|
|
141
|
+
// can't see. A stray rejection (e.g. a fire-and-forget push to a tab that
|
|
142
|
+
// vanished mid-flight, or an SDK hiccup) must never silently kill it —
|
|
143
|
+
// otherwise the panel goes dead with no explanation. Log and keep running.
|
|
144
|
+
process.on("unhandledRejection", (reason) => {
|
|
145
|
+
// Benign strays are common here (a fire-and-forget push to a tab that vanished
|
|
146
|
+
// mid-flight, an SDK hiccup) and must NOT kill the orchestrator — log + continue.
|
|
147
|
+
logger.error(`[panel-orchestrator] unhandled rejection (ignored): ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
|
|
148
|
+
});
|
|
149
|
+
process.on("uncaughtException", (err) => {
|
|
150
|
+
// A synchronous uncaught throw leaves the process in an UNDEFINED state. The
|
|
151
|
+
// old "log + continue" here was a zombie root cause — the orchestrator stayed
|
|
152
|
+
// alive but broken, so the panel couldn't reconnect and a ComfyUI restart just
|
|
153
|
+
// reattached to it. Exit so the pack respawns a clean orchestrator (Node's own
|
|
154
|
+
// default is to crash on uncaughtException anyway).
|
|
155
|
+
logger.error(`[panel-orchestrator] FATAL uncaught exception — exiting so a fresh orchestrator can take over: ${err.stack ?? err.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
57
158
|
// Subscription lane: the background agent must authenticate against the user's
|
|
58
159
|
// claude.ai login, never an API key. Unset the key for the SDK subprocess.
|
|
59
160
|
delete process.env.ANTHROPIC_API_KEY;
|
|
60
|
-
|
|
161
|
+
// Dedicated PANEL bridge port (default 9180).
|
|
162
|
+
const bridge = startUiBridge(Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180);
|
|
61
163
|
// Owning the bridge port is the orchestrator's whole job — if another process
|
|
62
|
-
// holds it
|
|
63
|
-
//
|
|
64
|
-
//
|
|
164
|
+
// holds it, fail loudly instead of running uselessly. (This also avoids the
|
|
165
|
+
// case where a failed bind leaves the process with no live handles and it
|
|
166
|
+
// exits silently.)
|
|
65
167
|
const bound = await bridge.whenReady();
|
|
66
168
|
if (!bound) {
|
|
67
|
-
logger.error(`[panel-orchestrator] could not bind the panel bridge port — another process owns it
|
|
169
|
+
logger.error(`[panel-orchestrator] could not bind the panel bridge port — another process owns it. Free that port and restart the orchestrator. Override the port with COMFYUI_MCP_BRIDGE_PORT.`);
|
|
68
170
|
process.exit(1);
|
|
69
171
|
}
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
172
|
+
// We own the port — register our REAL pid + the launching ComfyUI pid so the
|
|
173
|
+
// panel pack can detect and replace us if we're ever orphaned across a Comfy
|
|
174
|
+
// restart. Written only after a successful bind (so the file always names the
|
|
175
|
+
// process that actually holds the port).
|
|
176
|
+
const lockPort = Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180;
|
|
177
|
+
const lockPath = orchLockPath(lockPort);
|
|
178
|
+
try {
|
|
179
|
+
writeFileSync(lockPath, JSON.stringify({
|
|
180
|
+
pid: process.pid,
|
|
181
|
+
// Our OWN process creation time, captured now while we KNOW this pid is
|
|
182
|
+
// the real orchestrator. The pack matches a live pid's creation time
|
|
183
|
+
// against this before ever killing it, so a reused pid (a shell, node,
|
|
184
|
+
// anything that inherits our old pid) can't be mistaken for us and
|
|
185
|
+
// terminated — the TOCTOU pid-reuse guard. Null on platforms we can't
|
|
186
|
+
// read it (the pack then falls back to the cmdline identity check).
|
|
187
|
+
pidStartedAt: readProcessStartedAtMs(process.pid),
|
|
188
|
+
parent: Number(process.env.COMFYUI_MCP_PARENT_PID) || null,
|
|
189
|
+
parentStartedAt: Number(process.env.COMFYUI_MCP_PARENT_STARTED_AT_MS) || null,
|
|
190
|
+
port: lockPort,
|
|
191
|
+
startedAt: new Date().toISOString(),
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
logger.debug(`[panel-orchestrator] could not write lockfile ${lockPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
196
|
+
}
|
|
197
|
+
// The spawned agent runs THIS comfyui-mcp build as its MCP server in normal
|
|
198
|
+
// mode — so it generates against the live ComfyUI over COMFYUI_URL and never
|
|
199
|
+
// tries to bind the bridge port we own here.
|
|
73
200
|
const mcpEntry = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
74
201
|
const comfyuiUrl = process.env.COMFYUI_URL ?? "http://127.0.0.1:8188";
|
|
202
|
+
// ComfyUI install path — when set, the spawned agent's MCP runs in LOCAL mode,
|
|
203
|
+
// so download_model / apply_manifest / installer-pack / model-scan tools work
|
|
204
|
+
// instead of degrading to remote-only. The panel pack supplies this.
|
|
205
|
+
const comfyuiPath = process.env.COMFYUI_PATH;
|
|
75
206
|
const model = process.env.COMFYUI_MCP_PANEL_MODEL ?? "claude-opus-4-8";
|
|
207
|
+
const envEffort = process.env.COMFYUI_MCP_PANEL_EFFORT;
|
|
208
|
+
const effort = isEffort(envEffort) ? envEffort : undefined;
|
|
209
|
+
const bridgePort = Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180;
|
|
210
|
+
// Cross-process download-progress channel: each tab's comfyui MCP subprocess
|
|
211
|
+
// writes per-download JSON here; the watcher below broadcasts it to the panel
|
|
212
|
+
// tray. Port-scoped so parallel orchestrators don't cross streams.
|
|
213
|
+
const progressDir = join(tmpdir(), `comfyui-mcp-progress-${bridgePort}`);
|
|
76
214
|
// The bundled plugin (skills) ships alongside dist/ in the package root. Load
|
|
77
215
|
// it so the background agents are ComfyUI experts out of the box.
|
|
78
216
|
const pluginPath = fileURLToPath(new URL("../../plugin", import.meta.url));
|
|
@@ -80,23 +218,317 @@ export async function runPanelOrchestrator() {
|
|
|
80
218
|
if (!pluginAvailable) {
|
|
81
219
|
logger.warn(`[panel-orchestrator] bundled plugin not found at ${pluginPath} — agents run without model-expertise skills.`);
|
|
82
220
|
}
|
|
221
|
+
// Build an agent_status frame from a usage snapshot — used both live (per
|
|
222
|
+
// assistant response) and to re-push the last value when a tab reconnects.
|
|
223
|
+
function pushStatus(tabId, status) {
|
|
224
|
+
bridge.push({
|
|
225
|
+
type: "agent_status",
|
|
226
|
+
...(typeof status.contextPct === "number" ? { context_pct: status.contextPct } : {}),
|
|
227
|
+
...(typeof status.used === "number" ? { used: status.used } : {}),
|
|
228
|
+
...(typeof status.contextWindow === "number" ? { context_window: status.contextWindow } : {}),
|
|
229
|
+
...(status.model ? { model: status.model } : {}),
|
|
230
|
+
...(typeof status.costUsd === "number" ? { cost_usd: status.costUsd } : {}),
|
|
231
|
+
}, tabId);
|
|
232
|
+
}
|
|
233
|
+
// Inherit the user's own MCP servers (the same ones their normal `claude`
|
|
234
|
+
// session uses), read from ~/.claude.json. Conflicting comfyui entries are
|
|
235
|
+
// filtered out by the reader so they can't grab our bridge port. This is what
|
|
236
|
+
// makes "add the CivitAI MCP" work: panel_add_mcp writes it here, a reload
|
|
237
|
+
// re-reads it, and the agent gains those tools. Re-read on every (re)start so
|
|
238
|
+
// new servers are picked up on the next soft reload.
|
|
239
|
+
const userMcpServers = readUserMcpServers();
|
|
240
|
+
const userMcpNames = Object.keys(userMcpServers);
|
|
241
|
+
if (userMcpNames.length) {
|
|
242
|
+
logger.info(`[panel-orchestrator] inheriting user MCP servers: ${userMcpNames.join(", ")}`);
|
|
243
|
+
}
|
|
83
244
|
const manager = new PanelAgentManager({
|
|
84
245
|
model,
|
|
246
|
+
effort,
|
|
247
|
+
comfyuiUrl, // for fetching image bytes to inline into agent turns
|
|
85
248
|
systemAppend: PANEL_SYSTEM_APPEND,
|
|
86
249
|
pluginPath: pluginAvailable ? pluginPath : undefined,
|
|
250
|
+
// Live-graph control of the user's open workflow, per tab (in-process).
|
|
251
|
+
makePanelServer: (tabId) => createPanelMcpServer(bridge, tabId),
|
|
87
252
|
mcpServers: {
|
|
253
|
+
// The user's inherited servers first…
|
|
254
|
+
...userMcpServers,
|
|
255
|
+
// …then our own comfyui server LAST, so it always wins over any user
|
|
256
|
+
// entry that slipped through (defensive — the reader already filters them).
|
|
88
257
|
comfyui: {
|
|
89
258
|
type: "stdio",
|
|
90
259
|
command: process.execPath, // node
|
|
91
|
-
args: [mcpEntry], // dist/index.js
|
|
92
|
-
env: {
|
|
260
|
+
args: [mcpEntry], // dist/index.js
|
|
261
|
+
env: {
|
|
262
|
+
COMFYUI_URL: comfyuiUrl,
|
|
263
|
+
// Where download_model writes live progress for the panel tray.
|
|
264
|
+
COMFYUI_MCP_PROGRESS_DIR: progressDir,
|
|
265
|
+
// Local mode → enables download_model, apply_manifest (installer packs),
|
|
266
|
+
// and model scans so the agent installs the right way instead of curl.
|
|
267
|
+
...(comfyuiPath ? { COMFYUI_PATH: comfyuiPath } : {}),
|
|
268
|
+
},
|
|
93
269
|
},
|
|
94
270
|
},
|
|
95
|
-
onSay: (tabId, text) => {
|
|
96
|
-
|
|
271
|
+
onSay: (tabId, text, meta) => {
|
|
272
|
+
// `id` lets the panel reconcile this committed message with its live
|
|
273
|
+
// streaming preview (same id) instead of rendering a duplicate bubble.
|
|
274
|
+
bridge.push({ type: "say", text, id: meta?.id, streamed: meta?.streamed }, tabId);
|
|
275
|
+
},
|
|
276
|
+
// Live streaming deltas → the panel's think-window + streaming reply bubble.
|
|
277
|
+
onStream: (tabId, ev) => {
|
|
278
|
+
bridge.push({ type: "stream", phase: ev.phase, id: ev.id, delta: ev.delta }, tabId);
|
|
279
|
+
},
|
|
280
|
+
// Per-response usage → the panel's context/usage meter (updates live).
|
|
281
|
+
onStatus: pushStatus,
|
|
282
|
+
// Report the SDK session id so the panel can persist it and resume on reload.
|
|
283
|
+
onSession: (tabId, sessionId) => {
|
|
284
|
+
bridge.push({ type: "session", session_id: sessionId }, tabId);
|
|
285
|
+
},
|
|
286
|
+
// Per-turn rewind anchor (assistant UUID) → the panel stores it so a later
|
|
287
|
+
// "rewind conversation to here" can fork the session at that point.
|
|
288
|
+
onTurnAnchor: (tabId, uuid) => {
|
|
289
|
+
bridge.push({ type: "turn_anchor", uuid }, tabId);
|
|
290
|
+
},
|
|
291
|
+
// Turn lifecycle → the panel's "working" indicator (stays up through silent
|
|
292
|
+
// tool work; clears on done).
|
|
293
|
+
onTurn: (tabId, state) => {
|
|
294
|
+
bridge.push({ type: "turn", state }, tabId);
|
|
295
|
+
},
|
|
296
|
+
// Live extended-thinking token count → "thinking… (N)" indicator.
|
|
297
|
+
onThinking: (tabId, tokens) => {
|
|
298
|
+
bridge.push({ type: "thinking", tokens }, tabId);
|
|
299
|
+
},
|
|
300
|
+
// The agent dequeued a message (the true "read" moment) → flip that bubble
|
|
301
|
+
// from queued/muted to read.
|
|
302
|
+
onSeen: (tabId, mid) => {
|
|
303
|
+
bridge.push({ type: "ack", ok: true, kind: "seen", mid }, tabId);
|
|
97
304
|
},
|
|
98
305
|
});
|
|
306
|
+
// Debounce the connect ack: the panel re-sends `hello` on reconnect and on
|
|
307
|
+
// workflow-title changes, which would otherwise stack duplicate greetings.
|
|
308
|
+
const lastAckAt = new Map();
|
|
309
|
+
const ACK_DEBOUNCE_MS = 4000;
|
|
310
|
+
// The account's real model list — probed once from the SDK (the only way that
|
|
311
|
+
// works on the subscription lane) and cached. Pushed to each tab so the
|
|
312
|
+
// panel's model/effort picker reflects what's actually available, with each
|
|
313
|
+
// model's supported effort levels, instead of a hardcoded list.
|
|
314
|
+
let modelsPromise = null;
|
|
315
|
+
function ensureModels() {
|
|
316
|
+
if (!modelsPromise) {
|
|
317
|
+
modelsPromise = fetchSupportedModels(model).then((list) => {
|
|
318
|
+
// Don't cache an empty/failed probe forever — let the next hello retry.
|
|
319
|
+
if (!list.length)
|
|
320
|
+
modelsPromise = null;
|
|
321
|
+
return list;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return modelsPromise;
|
|
325
|
+
}
|
|
326
|
+
function pushModels(tabId) {
|
|
327
|
+
void ensureModels()
|
|
328
|
+
.then((models) => {
|
|
329
|
+
if (models.length) {
|
|
330
|
+
bridge.push({ type: "models", models, current: model }, tabId);
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
.catch(() => {
|
|
334
|
+
/* probe already logged; panel keeps its fallback list */
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// The SDK's slash commands (built-ins like /compact, plus any loaded skills) —
|
|
338
|
+
// probed once and surfaced in the panel composer's completion menu.
|
|
339
|
+
let commandsPromise = null;
|
|
340
|
+
function ensureCommands() {
|
|
341
|
+
if (!commandsPromise) {
|
|
342
|
+
commandsPromise = fetchSupportedCommands(model).then((list) => {
|
|
343
|
+
if (!list.length)
|
|
344
|
+
commandsPromise = null; // let the next hello retry
|
|
345
|
+
return list;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return commandsPromise;
|
|
349
|
+
}
|
|
350
|
+
// The SDK reports EVERY command the user's Claude install exposes — including
|
|
351
|
+
// all their unrelated skills/plugins (Cloudflare, codex:*, etc.). Surface only
|
|
352
|
+
// the built-ins that make sense inside the ComfyUI panel chat.
|
|
353
|
+
const PANEL_SLASH_ALLOWLIST = new Set(["compact", "context", "usage", "loop", "goal", "clear"]);
|
|
354
|
+
function pushCommands(tabId) {
|
|
355
|
+
void ensureCommands()
|
|
356
|
+
.then((commands) => {
|
|
357
|
+
const useful = commands.filter((c) => PANEL_SLASH_ALLOWLIST.has(c.name));
|
|
358
|
+
if (useful.length)
|
|
359
|
+
bridge.push({ type: "commands", commands: useful }, tabId);
|
|
360
|
+
})
|
|
361
|
+
.catch(() => {
|
|
362
|
+
/* probe already logged; panel just won't show SDK commands */
|
|
363
|
+
});
|
|
364
|
+
}
|
|
99
365
|
bridge.onPanelMessage = (event) => {
|
|
366
|
+
// Connect ack: the instant a panel tab connects, the orchestrator announces
|
|
367
|
+
// itself so "connected" means "a real agent is attending" — not merely "a
|
|
368
|
+
// socket is open." A bare/undriven bridge stays silent, so the panel can
|
|
369
|
+
// tell the difference (and warn if no ack arrives).
|
|
370
|
+
if (event.type === "hello" && event.tab_id) {
|
|
371
|
+
// Reload restore: the panel re-sends the last session id it saw so the
|
|
372
|
+
// agent's memory continues. Only honored before the tab's agent spawns.
|
|
373
|
+
const resume = typeof event.resume === "string" ? event.resume : undefined;
|
|
374
|
+
if (resume)
|
|
375
|
+
manager.setResume(event.tab_id, resume);
|
|
376
|
+
// Send the live model list so the picker reflects the real subscription,
|
|
377
|
+
// and the SDK's slash commands so the composer can surface them.
|
|
378
|
+
pushModels(event.tab_id);
|
|
379
|
+
pushCommands(event.tab_id);
|
|
380
|
+
// Re-push the last usage so the context meter isn't blank after a reload.
|
|
381
|
+
const lastStatus = manager.lastStatusFor(event.tab_id);
|
|
382
|
+
if (lastStatus)
|
|
383
|
+
pushStatus(event.tab_id, lastStatus);
|
|
384
|
+
const tabId = event.tab_id;
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
if (now - (lastAckAt.get(tabId) ?? 0) < ACK_DEBOUNCE_MS)
|
|
387
|
+
return;
|
|
388
|
+
lastAckAt.set(tabId, now);
|
|
389
|
+
// TRUTHFUL "connected": only claim ready after PROVING the SDK can run, by
|
|
390
|
+
// probing the model list (same machinery the agent uses to spawn). If the
|
|
391
|
+
// probe fails — the "connected but dead" wedge — say so and send a degraded
|
|
392
|
+
// ack instead of a green ready, so the panel can show the real state.
|
|
393
|
+
void ensureModels()
|
|
394
|
+
.then((models) => {
|
|
395
|
+
if (models.length) {
|
|
396
|
+
// Greet only on a FRESH session. On a reconnect/resume — a panel swap,
|
|
397
|
+
// a WS blip, or a real restart (all carry `resume`) — the user already
|
|
398
|
+
// has their thread, so re-greeting is just noise. The ack still fires.
|
|
399
|
+
if (!resume) {
|
|
400
|
+
bridge.push({ type: "say", text: `🟢 comfyui-mcp agent ready — ${model} on your Claude subscription. Ask away.` }, tabId);
|
|
401
|
+
}
|
|
402
|
+
bridge.push({ type: "ack", ok: true, kind: "ready", agent: model }, tabId);
|
|
403
|
+
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} connected — agent healthy, sent ready ack`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
bridge.push({
|
|
407
|
+
type: "say",
|
|
408
|
+
text: "⚠️ 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.",
|
|
409
|
+
}, tabId);
|
|
410
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, tabId);
|
|
411
|
+
logger.warn(`[panel-orchestrator] tab ${tabId.slice(0, 8)} connected but model probe empty — sent degraded ack`);
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
.catch(() => {
|
|
415
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, tabId);
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// Model / effort picker: apply and confirm. Model switches live; an effort
|
|
420
|
+
// change restarts the session (resumed) so the conversation carries over.
|
|
421
|
+
if (event.type === "set_options" && event.tab_id) {
|
|
422
|
+
const tabId = event.tab_id;
|
|
423
|
+
const reqModel = typeof event.model === "string" ? event.model : undefined;
|
|
424
|
+
const nextEffort = event.effort === null
|
|
425
|
+
? null
|
|
426
|
+
: isEffort(event.effort)
|
|
427
|
+
? event.effort
|
|
428
|
+
: undefined;
|
|
429
|
+
void (async () => {
|
|
430
|
+
let nextModel = reqModel;
|
|
431
|
+
// Guard: never switch to a model the account can't use — an unknown id
|
|
432
|
+
// makes the SDK session hang on init. (Defense in depth; the panel only
|
|
433
|
+
// sends ids from the live catalog.)
|
|
434
|
+
if (nextModel) {
|
|
435
|
+
const known = await ensureModels().catch(() => []);
|
|
436
|
+
if (known.length && !known.some((m) => m.value === nextModel)) {
|
|
437
|
+
logger.warn(`[panel-orchestrator] ignoring unknown model "${nextModel}" — keeping current`);
|
|
438
|
+
nextModel = undefined;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const applied = await manager.setOptions(tabId, { model: nextModel, effort: nextEffort });
|
|
442
|
+
bridge.push({
|
|
443
|
+
type: "ack",
|
|
444
|
+
ok: true,
|
|
445
|
+
kind: "options",
|
|
446
|
+
model: applied.model,
|
|
447
|
+
effort: applied.effort ?? null,
|
|
448
|
+
restarted: applied.restarted,
|
|
449
|
+
// Effort changed mid-turn → it takes effect once the current turn ends
|
|
450
|
+
// (we never interrupt a live reply). The panel can note this.
|
|
451
|
+
deferred: applied.deferred,
|
|
452
|
+
}, tabId);
|
|
453
|
+
})().catch((err) => {
|
|
454
|
+
bridge.push({ type: "say", text: `⚠️ Could not change model/effort: ${err?.message ?? err}` }, tabId);
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Execution event from the panel (run finished / errored). Feed it to the
|
|
459
|
+
// tab's live agent so it knows its render landed and can comment/iterate.
|
|
460
|
+
// Dropped silently if no agent is attending the tab (we don't spawn one).
|
|
461
|
+
if (event.type === "agent_event" && event.tab_id) {
|
|
462
|
+
const delivered = manager.injectEvent(event.tab_id, event);
|
|
463
|
+
if (delivered) {
|
|
464
|
+
logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} event → agent: ${event.kind}`);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Interrupt: stop the current turn without ending the session (Ctrl+C in
|
|
469
|
+
// the panel). The session stays open for the next message.
|
|
470
|
+
if (event.type === "interrupt" && event.tab_id) {
|
|
471
|
+
const tabId = event.tab_id;
|
|
472
|
+
void manager.interrupt(tabId);
|
|
473
|
+
bridge.push({ type: "ack", ok: true, kind: "interrupt" }, tabId);
|
|
474
|
+
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} interrupted`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// The user edited/deleted a still-QUEUED message before the agent read it —
|
|
478
|
+
// drop it from the agent's queue so it's never processed.
|
|
479
|
+
if (event.type === "cancel_message" && event.tab_id) {
|
|
480
|
+
const tabId = event.tab_id;
|
|
481
|
+
const mid = typeof event.mid === "string" ? event.mid : undefined;
|
|
482
|
+
const removed = mid ? manager.cancelQueued(tabId, mid) : false;
|
|
483
|
+
bridge.push({ type: "ack", ok: true, kind: "cancel_message", mid, removed }, tabId);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
// New chat: forget this tab's session so the next message starts fresh (no
|
|
487
|
+
// memory of the prior conversation). Tell the panel to drop its stored id.
|
|
488
|
+
if (event.type === "new_session" && event.tab_id) {
|
|
489
|
+
const tabId = event.tab_id;
|
|
490
|
+
// reset() is synchronous (map cleared now), so no concurrent send() can
|
|
491
|
+
// spawn an agent before we report the cleared session.
|
|
492
|
+
manager.reset(tabId);
|
|
493
|
+
bridge.push({ type: "session", session_id: null }, tabId);
|
|
494
|
+
bridge.push({ type: "ack", ok: true, kind: "new_session" }, tabId);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Rewind the conversation: fork the live session at `anchor` (an assistant
|
|
498
|
+
// UUID the panel stored from onTurnAnchor) so everything after it is dropped,
|
|
499
|
+
// optionally continuing with the user's edited `text`. The panel handles the
|
|
500
|
+
// graph (code) scope locally; this is the conversation scope.
|
|
501
|
+
if (event.type === "rewind" && event.tab_id) {
|
|
502
|
+
const tabId = event.tab_id;
|
|
503
|
+
const anchor = typeof event.anchor === "string" ? event.anchor : null;
|
|
504
|
+
const ok = manager.rewind(tabId, anchor);
|
|
505
|
+
bridge.push({ type: "ack", ok, kind: "rewind" }, tabId);
|
|
506
|
+
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} rewind (anchor=${anchor ? anchor.slice(0, 8) : "fresh"}, ok=${ok})`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Reorder still-queued messages to the panel's desired flush order.
|
|
510
|
+
if (event.type === "reorder" && event.tab_id) {
|
|
511
|
+
const tabId = event.tab_id;
|
|
512
|
+
const order = Array.isArray(event.order)
|
|
513
|
+
? event.order.filter((m) => typeof m === "string")
|
|
514
|
+
: [];
|
|
515
|
+
const ok = manager.reorderQueue(tabId, order);
|
|
516
|
+
bridge.push({ type: "ack", ok, kind: "reorder" }, tabId);
|
|
517
|
+
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} reorder queue (${order.length} mids, ok=${ok})`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// Switch to a historical chat: drop the live agent and arm a resume so the
|
|
521
|
+
// next message continues THAT conversation. Both calls are synchronous, so
|
|
522
|
+
// the resume is armed before any later message can spawn a fresh agent.
|
|
523
|
+
if (event.type === "resume_session" && event.tab_id) {
|
|
524
|
+
const tabId = event.tab_id;
|
|
525
|
+
const sid = typeof event.session_id === "string" ? event.session_id : undefined;
|
|
526
|
+
manager.reset(tabId);
|
|
527
|
+
if (sid)
|
|
528
|
+
manager.setResume(tabId, sid);
|
|
529
|
+
bridge.push({ type: "ack", ok: true, kind: "resume_session" }, tabId);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
100
532
|
if (event.type !== "user_message" ||
|
|
101
533
|
typeof event.text !== "string" ||
|
|
102
534
|
!event.tab_id) {
|
|
@@ -104,18 +536,106 @@ export async function runPanelOrchestrator() {
|
|
|
104
536
|
}
|
|
105
537
|
// Echo so the user immediately sees their own message land in the chat.
|
|
106
538
|
bridge.push({ type: "echo", text: event.text }, event.tab_id);
|
|
539
|
+
// Per-message ack: a live server-side signal that the agent received this
|
|
540
|
+
// turn and is working — distinct from the panel's own optimistic spinner.
|
|
541
|
+
// Echo the client mid so the panel can mark that exact bubble delivered.
|
|
542
|
+
const userMid = typeof event.mid === "string" ? event.mid : undefined;
|
|
543
|
+
bridge.push({ type: "ack", ok: true, kind: "working", ...(userMid ? { mid: userMid } : {}) }, event.tab_id);
|
|
544
|
+
// Show the working indicator immediately (before the first assistant token).
|
|
545
|
+
bridge.push({ type: "turn", state: "working" }, event.tab_id);
|
|
107
546
|
logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} → agent: ${event.text.slice(0, 80)}`);
|
|
108
|
-
manager.send(event.tab_id, event.text, {
|
|
547
|
+
manager.send(event.tab_id, event.text, {
|
|
548
|
+
title: event.title,
|
|
549
|
+
images: event.images,
|
|
550
|
+
mid: userMid,
|
|
551
|
+
});
|
|
109
552
|
};
|
|
110
|
-
|
|
553
|
+
// ---- Download-progress watcher ----
|
|
554
|
+
// Each tab's comfyui MCP (download_model) writes per-download JSON into
|
|
555
|
+
// progressDir; poll it and broadcast the rows to every panel tab's tray.
|
|
556
|
+
// Done/error rows linger briefly (so completion is visible), then are pruned;
|
|
557
|
+
// a downloading row that stops updating for 60s is treated as a dead writer.
|
|
558
|
+
const DOWNLOAD_LINGER_MS = 8000;
|
|
559
|
+
const downloadRemoveAt = new Map();
|
|
560
|
+
let lastDownloadSnapshot = "[]";
|
|
561
|
+
const pollDownloads = () => {
|
|
562
|
+
let files = [];
|
|
563
|
+
try {
|
|
564
|
+
files = readdirSync(progressDir).filter((f) => f.endsWith(".json"));
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
files = []; // dir not created yet — nothing downloading
|
|
568
|
+
}
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
const downloads = [];
|
|
571
|
+
for (const f of files) {
|
|
572
|
+
const full = join(progressDir, f);
|
|
573
|
+
let row;
|
|
574
|
+
try {
|
|
575
|
+
row = JSON.parse(readFileSync(full, "utf8"));
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
continue; // mid-write or corrupt — retry next tick
|
|
579
|
+
}
|
|
580
|
+
if (!row || typeof row !== "object")
|
|
581
|
+
continue;
|
|
582
|
+
const status = row.status;
|
|
583
|
+
const updated = typeof row.updated === "number" ? row.updated : now;
|
|
584
|
+
if (status === "done" || status === "error") {
|
|
585
|
+
const due = downloadRemoveAt.get(full);
|
|
586
|
+
if (due == null) {
|
|
587
|
+
downloadRemoveAt.set(full, now + DOWNLOAD_LINGER_MS); // start the linger
|
|
588
|
+
}
|
|
589
|
+
else if (now >= due) {
|
|
590
|
+
try {
|
|
591
|
+
unlinkSync(full);
|
|
592
|
+
}
|
|
593
|
+
catch { /* already gone */ }
|
|
594
|
+
downloadRemoveAt.delete(full);
|
|
595
|
+
continue; // pruned from the tray
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
downloadRemoveAt.delete(full);
|
|
600
|
+
if (now - updated > 60000) {
|
|
601
|
+
try {
|
|
602
|
+
unlinkSync(full);
|
|
603
|
+
}
|
|
604
|
+
catch { /* ignore */ }
|
|
605
|
+
continue; // dead writer (crashed mid-download)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
downloads.push(row);
|
|
609
|
+
}
|
|
610
|
+
downloads.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? "")));
|
|
611
|
+
const snapshot = JSON.stringify(downloads);
|
|
612
|
+
if (snapshot !== lastDownloadSnapshot) {
|
|
613
|
+
lastDownloadSnapshot = snapshot;
|
|
614
|
+
bridge.push({ type: "download_progress", downloads }); // broadcast to all tabs
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const downloadTimer = setInterval(pollDownloads, 700);
|
|
618
|
+
downloadTimer.unref?.();
|
|
619
|
+
logger.info(`[panel-orchestrator] ready — bridge on ws://127.0.0.1:${bridgePort}; an agent spawns per ComfyUI tab on its first message (model=${model}, comfyui=${comfyuiUrl}${comfyuiPath ? `, path=${comfyuiPath}` : " — no COMFYUI_PATH, local install/pack tools limited"})`);
|
|
111
620
|
let shuttingDown = false;
|
|
112
621
|
const shutdown = async () => {
|
|
113
622
|
if (shuttingDown)
|
|
114
623
|
return;
|
|
115
624
|
shuttingDown = true;
|
|
116
625
|
logger.info("[panel-orchestrator] shutting down — stopping agents…");
|
|
626
|
+
clearInterval(downloadTimer);
|
|
117
627
|
await manager.stopAll();
|
|
118
628
|
await bridge.stop();
|
|
629
|
+
// Only remove the lockfile if it still names us — avoid clobbering a fresh
|
|
630
|
+
// orchestrator that may have replaced us.
|
|
631
|
+
try {
|
|
632
|
+
const cur = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
633
|
+
if (cur?.pid === process.pid)
|
|
634
|
+
unlinkSync(lockPath);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
// No lockfile / unreadable — nothing to clean up.
|
|
638
|
+
}
|
|
119
639
|
process.exit(0);
|
|
120
640
|
};
|
|
121
641
|
process.on("SIGINT", shutdown);
|