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
|
@@ -229,10 +229,61 @@ export function buildPanelToolDefs() {
|
|
|
229
229
|
// Local helper so each def reads like the original `tool(...)` call.
|
|
230
230
|
const def = (name, description, schema, handler) => ({ name, description, schema, handler });
|
|
231
231
|
return [
|
|
232
|
-
def("
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
def("panel_query_graph", "QUERY the workflow the user is CURRENTLY VIEWING — filter, traverse, project, and aggregate over the live canvas WITHOUT dumping the whole graph (replaces the old panel_get_graph full-JSON dump; output is TOKEN-BOUNDED with an explicit truncation marker, so a big graph can never flood your context). Combine: `types` (node type contains any), `title` (contains), `where` widget predicates ANDed ('cfg>7', 'steps<=20', 'sampler_name=euler', 'text~sunset' — ops = != >= <= > < ~contains), `ids` (exact nodes — THE way to read ONE node's exact slot/widget detail: {ids:[42], fields:'detail'}), `upstream_of`/`downstream_of` + `depth` (dependency traversal: upstream = what FEEDS that node, downstream = what CONSUMES it; seed at depth 0), `fields` ('compact' one line per node [default], 'ids', 'detail' = the full node summary with slots + connections + mode), `group_by:'type'` (counts only), `limit` (default 40). detail rows include each node's MODE — a 'bypass' node is skipped and a 'mute' node kills everything downstream, so check modes on the path you care about before running (fix with panel_set_node_mode). Every result also carries `groups` (id, title, member node_ids — groups are geometric, trust this list) and, when viewing a SUBGRAPH (after panel_enter_subgraph), `rails` (boundary rail ids/slots). Typical flow: panel_graph_outline to orient → panel_query_graph to pinpoint/inspect → edit. Read-only.", {
|
|
233
|
+
types: z.array(z.string()).optional().describe("Node type contains ANY of these (case-insensitive)."),
|
|
234
|
+
title: z.string().optional().describe("Node title contains this."),
|
|
235
|
+
where: z
|
|
236
|
+
.array(z.string())
|
|
237
|
+
.optional()
|
|
238
|
+
.describe("Widget predicates, ANDed: 'cfg>7', 'sampler_name=euler', 'text~sunset'."),
|
|
239
|
+
ids: z
|
|
240
|
+
.array(z.union([z.string(), z.number()]))
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("Exact node ids — with fields:'detail' this reads one node's full slot/widget detail."),
|
|
243
|
+
upstream_of: z
|
|
244
|
+
.union([z.string(), z.number()])
|
|
245
|
+
.optional()
|
|
246
|
+
.describe("Scope to the dependency closure FEEDING this node id."),
|
|
247
|
+
downstream_of: z
|
|
248
|
+
.union([z.string(), z.number()])
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("Scope to the nodes CONSUMING this node id's outputs."),
|
|
251
|
+
depth: z
|
|
252
|
+
.number()
|
|
253
|
+
.int()
|
|
254
|
+
.min(0)
|
|
255
|
+
.optional()
|
|
256
|
+
.describe("Max hops from the traversal seed (seed=0). Absent = full closure."),
|
|
257
|
+
fields: z
|
|
258
|
+
.enum(["ids", "compact", "detail"])
|
|
259
|
+
.optional()
|
|
260
|
+
.describe("Projection: compact one-liners (default), bare ids, or full node summaries."),
|
|
261
|
+
group_by: z.enum(["type"]).optional().describe("Aggregate: counts per node type instead of listing."),
|
|
262
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max nodes listed (default 40)."),
|
|
263
|
+
max_chars: z
|
|
264
|
+
.number()
|
|
265
|
+
.int()
|
|
266
|
+
.min(500)
|
|
267
|
+
.max(60000)
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Output character bound (default 12000). Raise only for deliberate full reads, e.g. layout passes needing every node's geometry."),
|
|
270
|
+
}, async (args, ctx) => ctx.call({
|
|
271
|
+
cmd: "graph_query",
|
|
272
|
+
types: args.types,
|
|
273
|
+
title: args.title,
|
|
274
|
+
where: args.where,
|
|
275
|
+
ids: args.ids,
|
|
276
|
+
upstream_of: args.upstream_of,
|
|
277
|
+
downstream_of: args.downstream_of,
|
|
278
|
+
depth: args.depth,
|
|
279
|
+
fields: args.fields,
|
|
280
|
+
group_by: args.group_by,
|
|
281
|
+
limit: args.limit,
|
|
282
|
+
max_chars: args.max_chars,
|
|
283
|
+
})),
|
|
284
|
+
def("panel_graph_outline", "Read a COMPACT, dependency-ordered TEXT MAP of the workflow the user is viewing — the FASTEST way to UNDERSTAND a graph (especially a big loaded pack/template) before you touch it. Returns one `outline` string built for you to read top→down: nodes are topologically sorted (sources first, sinks last), each shown on its own block as `id Type \"title\" [bypass/mute] [OUTPUT] · group:X widget=value …` with `← inputs` (as source_node.output_name) and `→ outputs` (as target_node.input_name), preceded by a GROUPS index (title → member node ids). It shows the WIRING you'd otherwise have to reconstruct. Use this FIRST to get oriented; then panel_query_graph to filter/traverse/inspect (e.g. {ids:[42], fields:'detail'} for one node's exact slot/widget detail), or panel_find_nodes for free-text search. Read-only.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_outline" })),
|
|
285
|
+
def("panel_get_subgraph", "Read INSIDE a subgraph node on the user's open graph: ids, types, widget values, and connections of its inner nodes. Use after panel_graph_outline / panel_query_graph shows a node with is_subgraph=true. Read-only.", { node_id: z.number().int().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => ctx.call({ cmd: "graph_get_subgraph", node_id: args.node_id })),
|
|
286
|
+
def("panel_find_nodes", "SEARCH the workflow the user is CURRENTLY VIEWING for nodes matching filters — the right way to PINPOINT a node (a specific loader, sampler, save, switch) in a LARGE graph instead of dumping the whole graph and scanning it. This searches the LIVE graph ON THE CANVAS — NOT the installable node registry (that's panel_search_nodes). It scans EVERY node (no truncation). Give a free-text `query` (matched case-insensitively across node type, title, description, widget NAMES, widget VALUES, and input/output port names+types — a node hits if ANY of those contain it) and/or targeted filters: type, title, input, output, widget (name), widget_value (contents), is_output, is_subgraph, mode. Targeted filters are ANDed together; the free `query` ORs across fields. Each match is the SAME rich summary as panel_query_graph's detail rows (id, type, title, widgets, inputs WITH their connected_from sources, outputs, mode, is_output, …) PLUS the node's description and a `matched_on` list saying WHY it matched. Read-only. Examples — the video loader: {query:'tiktok'} or {type:'LoadVideo'} or {input:'video'}; every output node: {is_output:true}; the node whose widget holds a file: {widget_value:'.png'}; a bypassed switch: {type:'Switch', mode:'bypass'}.", {
|
|
236
287
|
query: z
|
|
237
288
|
.string()
|
|
238
289
|
.optional()
|
|
@@ -295,7 +346,7 @@ export function buildPanelToolDefs() {
|
|
|
295
346
|
.describe("Canvas [x, y] (two numbers). Auto-placed beside existing nodes when omitted."),
|
|
296
347
|
title: z.string().optional().describe("Optional custom node title."),
|
|
297
348
|
}, async (args, ctx) => ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title })),
|
|
298
|
-
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: z.number().int().describe("Node id from
|
|
349
|
+
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
|
|
299
350
|
def("panel_clear", "Remove EVERY node from the user's open graph — only for an explicit 'clear/reset the canvas'. Just CALL THIS DIRECTLY when they ask to clear: the tool itself pops a confirm card and only wipes on a yes (don't ask separately first). The wipe is a single Ctrl+Z undo. NEVER use this for a 'new workflow' — that's panel_new_workflow (a new tab, leaves this graph intact).", {}, async (_args, ctx) => {
|
|
300
351
|
if (!(await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas"))) {
|
|
301
352
|
return ok("Cancelled — the canvas was left as-is.");
|
|
@@ -444,33 +495,70 @@ export function buildPanelToolDefs() {
|
|
|
444
495
|
return fail(err);
|
|
445
496
|
}
|
|
446
497
|
}),
|
|
447
|
-
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index.
|
|
498
|
+
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index. If both slot args are omitted the panel picks the first type-compatible pairing. On failure the error lists every slot with its type and [connected] flag — re-check with panel_query_graph ({ids:[node_id], fields:'detail'}). Undoable.", {
|
|
448
499
|
from_node_id: z.number().int().describe("Source node id."),
|
|
449
|
-
from_output: slotRef
|
|
500
|
+
from_output: slotRef
|
|
501
|
+
.optional()
|
|
502
|
+
.describe("Source output slot name or index; omit to auto-match by type (prefers an unconnected, exact-type input; `*` wildcards match last)."),
|
|
450
503
|
to_node_id: z.number().int().describe("Target node id."),
|
|
451
|
-
to_input: slotRef
|
|
504
|
+
to_input: slotRef
|
|
505
|
+
.optional()
|
|
506
|
+
.describe("Target input slot name or index; omit to auto-match by type (prefers an unconnected, exact-type input; `*` wildcards match last)."),
|
|
507
|
+
auto_match: z
|
|
508
|
+
.boolean()
|
|
509
|
+
.optional()
|
|
510
|
+
.describe("Default true. Set false to force legacy exact resolution (omitted slot = index 0)."),
|
|
452
511
|
}, async (args, ctx) => ctx.call({
|
|
453
512
|
cmd: "graph_connect",
|
|
454
513
|
from_node_id: args.from_node_id,
|
|
455
514
|
from_output: args.from_output,
|
|
456
515
|
to_node_id: args.to_node_id,
|
|
457
516
|
to_input: args.to_input,
|
|
517
|
+
auto_match: args.auto_match,
|
|
458
518
|
})),
|
|
459
519
|
def("panel_disconnect", "Disconnect an input slot of a node in the user's open graph. Undoable with Ctrl+Z.", {
|
|
460
520
|
node_id: z.number().int().describe("Node id whose input to disconnect."),
|
|
461
521
|
input: slotRef.optional().describe("Input slot name or index (default 0)."),
|
|
462
522
|
}, async (args, ctx) => ctx.call({ cmd: "graph_disconnect", node_id: args.node_id, input: args.input })),
|
|
463
523
|
def("panel_set_widget", "Set a widget value on a node in the user's open graph (steps, cfg, seed, ckpt_name, text prompts, …). Returns the previous and new value. Undoable with Ctrl+Z.", {
|
|
464
|
-
node_id: z.number().int().describe("Node id from
|
|
524
|
+
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
465
525
|
widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
|
|
466
526
|
value: z
|
|
467
527
|
.union([z.string(), z.number(), z.boolean()])
|
|
468
528
|
.describe("New value. Must match the widget's expected type."),
|
|
469
529
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_widget", node_id: args.node_id, widget: args.widget, value: args.value })),
|
|
470
530
|
def("panel_move_node", "Move a node to a new canvas position [x, y] in the user's open graph. Undoable.", {
|
|
471
|
-
node_id: z.number().int().describe("Node id from
|
|
531
|
+
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
472
532
|
pos: xy().describe("New canvas [x, y] (two numbers)."),
|
|
473
533
|
}, async (args, ctx) => ctx.call({ cmd: "graph_move_node", node_id: args.node_id, pos: args.pos })),
|
|
534
|
+
def("panel_auto_layout", "Automatically arrange the user's open graph (or a subset of nodes) into a clean left-to-right / top-to-bottom / grid layout based on the real link topology. Group boxes move with their members and are re-fit. Use dry_run:true to preview proposed positions without touching the canvas. Undoable (one Ctrl+Z).", {
|
|
535
|
+
node_ids: z
|
|
536
|
+
.array(z.number().int())
|
|
537
|
+
.optional()
|
|
538
|
+
.describe("Node ids to arrange (default: every node in the active graph)."),
|
|
539
|
+
mode: z
|
|
540
|
+
.enum(["flow_horizontal", "flow_vertical", "grid"])
|
|
541
|
+
.optional()
|
|
542
|
+
.describe("Layout strategy (default flow_horizontal — left-to-right by dependency depth)."),
|
|
543
|
+
spacing: z
|
|
544
|
+
.number()
|
|
545
|
+
.min(0.25)
|
|
546
|
+
.max(4)
|
|
547
|
+
.optional()
|
|
548
|
+
.describe("Gap multiplier (1 = compact default, 1.5 = 50% roomier)."),
|
|
549
|
+
groups: z.enum(["preserve", "cluster", "ignore"]).optional(),
|
|
550
|
+
dry_run: z
|
|
551
|
+
.boolean()
|
|
552
|
+
.optional()
|
|
553
|
+
.describe("Compute and return proposed positions without moving anything."),
|
|
554
|
+
}, async (args, ctx) => ctx.call({
|
|
555
|
+
cmd: "graph_auto_layout",
|
|
556
|
+
node_ids: args.node_ids,
|
|
557
|
+
mode: args.mode,
|
|
558
|
+
spacing: args.spacing,
|
|
559
|
+
groups: args.groups,
|
|
560
|
+
dry_run: args.dry_run,
|
|
561
|
+
}, 15000)),
|
|
474
562
|
def("panel_canvas", "Control the user's canvas view: 'fit' frames the whole graph, 'center_on_node' jumps to a node (give node_id), 'pan' shifts by dx/dy, 'zoom' sets an absolute scale. View-only.", {
|
|
475
563
|
action: z.enum(["fit", "center_on_node", "pan", "zoom"]),
|
|
476
564
|
node_id: z.number().int().optional().describe("Required for center_on_node."),
|
|
@@ -485,7 +573,7 @@ export function buildPanelToolDefs() {
|
|
|
485
573
|
dy: args.dy,
|
|
486
574
|
scale: args.scale,
|
|
487
575
|
})),
|
|
488
|
-
def("panel_run", "Queue the workflow the user has OPEN — exactly like them pressing Queue Prompt (current widget values, the live graph they can see). Returns queued:true, or queued:false with node_errors when frontend validation fails. Pass to_node_id to RUN ONLY ONE BRANCH ('run to node'): ComfyUI renders just that output node plus everything upstream of it and SKIPS every other output branch — handy for previewing or debugging part of a big graph without rendering the whole thing. to_node_id MUST be an OUTPUT node (SaveImage, PreviewImage, SaveVideo, …) — pick the one at the END of the branch you want; nodes are tagged is_output:true in
|
|
576
|
+
def("panel_run", "Queue the workflow the user has OPEN — exactly like them pressing Queue Prompt (current widget values, the live graph they can see). Returns queued:true, or queued:false with node_errors when frontend validation fails. Pass to_node_id to RUN ONLY ONE BRANCH ('run to node'): ComfyUI renders just that output node plus everything upstream of it and SKIPS every other output branch — handy for previewing or debugging part of a big graph without rendering the whole thing. to_node_id MUST be an OUTPUT node (SaveImage, PreviewImage, SaveVideo, …) — pick the one at the END of the branch you want; nodes are tagged is_output:true in panel_query_graph's detail rows. Omit it to run the whole graph. Use this so the render runs on THEIR canvas and they see the result.", {
|
|
489
577
|
batch_count: z
|
|
490
578
|
.number()
|
|
491
579
|
.int()
|
|
@@ -497,7 +585,7 @@ export function buildPanelToolDefs() {
|
|
|
497
585
|
.number()
|
|
498
586
|
.int()
|
|
499
587
|
.optional()
|
|
500
|
-
.describe("Output node id to render UP TO (partial execution). Omit to run the whole graph. Must be an OUTPUT node — one with is_output:true in
|
|
588
|
+
.describe("Output node id to render UP TO (partial execution). Omit to run the whole graph. Must be an OUTPUT node — one with is_output:true in panel_query_graph's detail rows."),
|
|
501
589
|
}, async (args, ctx) => {
|
|
502
590
|
// BACKPRESSURE: the agent can't see ComfyUI's queue, so re-queuing while a
|
|
503
591
|
// render is already running silently stacks behind it (this is how a stuck
|
|
@@ -738,10 +826,10 @@ export function buildPanelToolDefs() {
|
|
|
738
826
|
}, async (args, ctx) => ctx.call({ cmd: "workflow_close", path: args.path, force: args.force }, 15000)),
|
|
739
827
|
def("panel_select_nodes", "Select nodes on the user's canvas by id (highlights them, sets the multi-selection). Useful before panel_create_subgraph.", { node_ids: z.array(z.number().int()).describe("Node ids to select.") }, async (args, ctx) => ctx.call({ cmd: "graph_select_nodes", node_ids: args.node_ids })),
|
|
740
828
|
def("panel_create_subgraph", "Group the given nodes into a SUBGRAPH (ComfyUI 'Convert to Subgraph') on the user's canvas — collapses them into one subgraph node. Returns the new subgraph node id. Undoable with Ctrl+Z. To wrap an existing GROUP, prefer panel_subgraph_group (you don't have to list the node_ids yourself).", { node_ids: z.array(z.number().int()).describe("Node ids to group into a subgraph.") }, async (args, ctx) => ctx.call({ cmd: "graph_create_subgraph", node_ids: args.node_ids }, 15000)),
|
|
741
|
-
def("panel_subgraph_group", "Wrap an existing GROUP's nodes into ONE subgraph node in a single step — the clean way to refactor a big graph into readable, TOGGLEABLE units. Pass the group by `group` (its title, e.g. 'REPLACEMENT MODE', or its numeric id from
|
|
829
|
+
def("panel_subgraph_group", "Wrap an existing GROUP's nodes into ONE subgraph node in a single step — the clean way to refactor a big graph into readable, TOGGLEABLE units. Pass the group by `group` (its title, e.g. 'REPLACEMENT MODE', or its numeric id from panel_query_graph's groups[]). LiteGraph groups don't own nodes — membership is geometric — so this computes which nodes sit inside the group box, selects them, and collapses them via ComfyUI 'Convert to Subgraph', returning the new subgraph node id + the wrapped node ids. After this you can toggle that whole region as ONE unit: panel_set_node_mode(node_id, 'bypass'/'active') on the subgraph node, then panel_run — e.g. queue one run with the region ON and one with it OFF. Undoable with Ctrl+Z. (For an arbitrary set of nodes that isn't a group, use panel_create_subgraph with explicit node_ids.)", {
|
|
742
830
|
group: z
|
|
743
831
|
.union([z.string(), z.number()])
|
|
744
|
-
.describe("Group to wrap: its title (case-insensitive substring, e.g. 'replacement mode') or its numeric id from
|
|
832
|
+
.describe("Group to wrap: its title (case-insensitive substring, e.g. 'replacement mode') or its numeric id from panel_query_graph groups[].id."),
|
|
745
833
|
}, async (args, ctx) => ctx.call({ cmd: "graph_subgraph_group", group: args.group }, 15000)),
|
|
746
834
|
def("panel_copy_nodes", "Copy nodes from the user's open graph to the clipboard. Pass node_ids to copy those nodes (they're selected first), or omit to copy the current canvas selection. The clipboard PERSISTS across workflow switches, so this is how you MERGE one workflow into another: copy here, then panel_open_workflow/panel_new_workflow to the destination, then panel_paste_nodes. Returns {copied: count}.", {
|
|
747
835
|
node_ids: z
|
|
@@ -784,13 +872,13 @@ export function buildPanelToolDefs() {
|
|
|
784
872
|
color: args.color,
|
|
785
873
|
font_size: args.font_size,
|
|
786
874
|
}, 15000)),
|
|
787
|
-
def("panel_move_group", "Move a group box to a new top-left [x, y] on the user's open graph. By default the nodes inside the group move with it (like dragging the group header); pass move_nodes:false to move only the box. Group id comes from
|
|
788
|
-
group_id: z.number().int().describe("Group id from
|
|
875
|
+
def("panel_move_group", "Move a group box to a new top-left [x, y] on the user's open graph. By default the nodes inside the group move with it (like dragging the group header); pass move_nodes:false to move only the box. Group id comes from panel_query_graph (the `groups` array on every result) or panel_create_group. Undoable.", {
|
|
876
|
+
group_id: z.number().int().describe("Group id from panel_query_graph's groups[] / panel_create_group."),
|
|
789
877
|
pos: xy().describe("New top-left [x, y] (two numbers)."),
|
|
790
878
|
move_nodes: z.boolean().optional().describe("Move the contained nodes too (default true)."),
|
|
791
879
|
}, async (args, ctx) => ctx.call({ cmd: "graph_move_group", group_id: args.group_id, pos: args.pos, move_nodes: args.move_nodes })),
|
|
792
880
|
def("panel_edit_group", "Edit a group box: its title, color, font_size, and/or bounds [x, y, width, height]. Only the fields you pass are changed. Undoable.", {
|
|
793
|
-
group_id: z.number().int().describe("Group id from
|
|
881
|
+
group_id: z.number().int().describe("Group id from panel_query_graph's groups[] / panel_create_group."),
|
|
794
882
|
title: z.string().optional().describe("New label."),
|
|
795
883
|
color: z.string().optional().describe("New box/header color, e.g. '#3f789e'."),
|
|
796
884
|
font_size: z.number().optional().describe("New title font size."),
|
|
@@ -805,27 +893,27 @@ export function buildPanelToolDefs() {
|
|
|
805
893
|
font_size: args.font_size,
|
|
806
894
|
bounds: args.bounds,
|
|
807
895
|
}, 15000)),
|
|
808
|
-
def("panel_remove_group", "Remove a group box from the user's open graph. The nodes inside the group are NOT deleted — only the box. Undoable.", { group_id: z.number().int().describe("Group id from
|
|
896
|
+
def("panel_remove_group", "Remove a group box from the user's open graph. The nodes inside the group are NOT deleted — only the box. Undoable.", { group_id: z.number().int().describe("Group id from panel_query_graph's groups[] / panel_create_group.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_group", group_id: args.group_id }, 15000)),
|
|
809
897
|
def("panel_set_node_title", "Rename a node's TITLE (the label on its header) — e.g. to label a node by its purpose. Different from panel_set_widget (which changes a value). Undoable with Ctrl+Z.", {
|
|
810
|
-
node_id: z.number().int().describe("Node id from
|
|
898
|
+
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
811
899
|
title: z.string().describe("New title text."),
|
|
812
900
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_title", node_id: args.node_id, title: args.title }, 15000)),
|
|
813
901
|
def("panel_set_node_collapsed", "Collapse (minimize) or expand a node on the user's open graph. Collapsed nodes shrink to just their title bar — handy for tidying loaders or rarely-touched nodes. Undoable.", {
|
|
814
|
-
node_id: z.number().int().describe("Node id from
|
|
902
|
+
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
815
903
|
collapsed: z.boolean().optional().describe("true = collapse/minimize (default), false = expand."),
|
|
816
904
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_node_collapsed", node_id: args.node_id, collapsed: args.collapsed })),
|
|
817
905
|
def("panel_set_node_mode", "Set a node's EXECUTION MODE on the user's open graph — active, bypass, or mute — and return { node_id, mode, previous_mode }. This is how you turn a node ON or OFF without deleting it. Modes:\n" +
|
|
818
906
|
"• 'active' — normal: the node executes.\n" +
|
|
819
907
|
"• 'bypass' — the node is SKIPPED and PASSES ITS INPUT THROUGH to its output (downstream still runs, just as if this node weren't there). Use to disable a single processing node (an upscaler, a LoRA, a detailer) while keeping the pipeline connected.\n" +
|
|
820
908
|
"• 'mute' — the node AND everything DOWNSTREAM of it do NOT execute (no pass-through). Use to fully switch off a branch/output.\n" +
|
|
821
|
-
"CRITICAL — modes silently change what a render produces, so they are a top cause of 'wrong output'. A BYPASSED node contributes nothing of its own and a MUTED node kills its branch. Use this tool to ENABLE the path you actually want and DISABLE the one you don't — e.g. to drive a workflow from its Ideogram/JSON prompt builder you must set the manual-prompt node to 'bypass' and the JSON-builder path to 'active' (or vice-versa); likewise to pick one branch of an rgthree 'Fast Groups Bypasser'/Muter or a prompt-source switch. ALWAYS read modes first
|
|
822
|
-
node_id: z.number().int().describe("Node id from
|
|
909
|
+
"CRITICAL — modes silently change what a render produces, so they are a top cause of 'wrong output'. A BYPASSED node contributes nothing of its own and a MUTED node kills its branch. Use this tool to ENABLE the path you actually want and DISABLE the one you don't — e.g. to drive a workflow from its Ideogram/JSON prompt builder you must set the manual-prompt node to 'bypass' and the JSON-builder path to 'active' (or vice-versa); likewise to pick one branch of an rgthree 'Fast Groups Bypasser'/Muter or a prompt-source switch. ALWAYS read modes first (panel_graph_outline marks [bypass]/[mute]; panel_query_graph detail rows carry mode): if the intended path is bypassed/muted, fix it HERE before running, and never assume a switch/route is already active. Undoable with Ctrl+Z.", {
|
|
910
|
+
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
823
911
|
mode: z
|
|
824
912
|
.enum(["active", "bypass", "mute"])
|
|
825
913
|
.describe("'active' = runs normally; 'bypass' = skipped, passes input through (downstream still runs); 'mute' = node and everything downstream do not execute."),
|
|
826
914
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_node_mode", node_id: args.node_id, mode: args.mode })),
|
|
827
915
|
def("panel_set_node_color", "Set a node's title-bar and/or body color on the user's open graph. Easiest: pass a `preset` from ComfyUI's palette (red, brown, green, blue, pale_blue, cyan, purple, yellow, black) for matched colors. Or set explicit `color` (title bar) and/or `bgcolor` (body) as hex like '#3f789e'. Pass null for a field to reset it to the theme default. Great for colour-coding stages. Undoable.", {
|
|
828
|
-
node_id: z.number().int().describe("Node id from
|
|
916
|
+
node_id: z.number().int().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
829
917
|
preset: z
|
|
830
918
|
.enum(["red", "brown", "green", "blue", "pale_blue", "cyan", "purple", "yellow", "black"])
|
|
831
919
|
.optional()
|
|
@@ -850,19 +938,19 @@ export function buildPanelToolDefs() {
|
|
|
850
938
|
return fail(err);
|
|
851
939
|
}
|
|
852
940
|
}),
|
|
853
|
-
def("panel_enter_subgraph", "Navigate INTO a subgraph node so you can read and EDIT its inner nodes — after this,
|
|
941
|
+
def("panel_enter_subgraph", "Navigate INTO a subgraph node so you can read and EDIT its inner nodes — after this, panel_query_graph / panel_graph_outline and all panel_* edit tools target the subgraph's inner graph (the user sees the canvas drill in). This is how you edit inside a subgraph (e.g. tweak a widget on an inner node). Call panel_exit_subgraph when done. Returns the new viewing scope.", { node_id: z.number().int().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => ctx.call({ cmd: "graph_enter_subgraph", node_id: args.node_id }, 15000)),
|
|
854
942
|
def("panel_exit_subgraph", "Leave the current subgraph and return to the root graph (undo a panel_enter_subgraph). After this, panel_* tools target the root graph again.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_exit_subgraph" }, 15000)),
|
|
855
|
-
def("panel_move_rail", "Reposition a subgraph's input or output RAIL (the boundary I/O node that the inner wires connect to). You MUST be INSIDE the subgraph first (panel_enter_subgraph). Read current rail positions from
|
|
943
|
+
def("panel_move_rail", "Reposition a subgraph's input or output RAIL (the boundary I/O node that the inner wires connect to). You MUST be INSIDE the subgraph first (panel_enter_subgraph). Read current rail positions from panel_query_graph's `rails` field (present when viewing a subgraph). Use this to place the input rail just left of the first node column and the output rail just right of the last one, so a tidy interior layout doesn't leave the rails stranded. rail is 'input' or 'output'.", {
|
|
856
944
|
rail: z.enum(["input", "output"]).describe("Which boundary rail to move."),
|
|
857
945
|
pos: xy().describe("New top-left [x, y] (two numbers)."),
|
|
858
946
|
}, async (args, ctx) => ctx.call({ cmd: "graph_move_rail", rail: args.rail, pos: args.pos })),
|
|
859
|
-
def("panel_promote_widget", "Expose (promote) an INNER subgraph widget on the PARENT subgraph node, so it can be set from outside without opening the subgraph — e.g. surface an inner KSampler's `seed`/`steps` on the subgraph node. You MUST be inside the subgraph first (call panel_enter_subgraph): `node_id` is an inner node (from
|
|
860
|
-
node_id: z.number().int().describe("Inner node id (from
|
|
947
|
+
def("panel_promote_widget", "Expose (promote) an INNER subgraph widget on the PARENT subgraph node, so it can be set from outside without opening the subgraph — e.g. surface an inner KSampler's `seed`/`steps` on the subgraph node. You MUST be inside the subgraph first (call panel_enter_subgraph): `node_id` is an inner node (from panel_query_graph while inside) and `widget` is one of its widget names. Pass demote:true to un-promote. Undoable with Ctrl+Z.", {
|
|
948
|
+
node_id: z.number().int().describe("Inner node id (from panel_query_graph while inside the subgraph)."),
|
|
861
949
|
widget: z.string().describe("Name of the widget on that node to promote (e.g. 'seed', 'steps', 'text')."),
|
|
862
950
|
demote: z.boolean().optional().describe("Set true to UN-promote (remove the widget from the parent node)."),
|
|
863
951
|
}, async (args, ctx) => ctx.call({ cmd: "graph_promote_widget", node_id: args.node_id, widget: args.widget, demote: args.demote }, 15000)),
|
|
864
|
-
def("panel_expose_subgraph_output", "Wire an interior node's OUTPUT to the subgraph's OUTPUT RAIL — i.e. expose it as a SUBGRAPH OUTPUT on the boundary so the PARENT graph can connect to the subgraph node's new output slot. You MUST be INSIDE the subgraph first (panel_enter_subgraph). This is the correct way to \"wire an internal output to the subgraph's output rail\": do NOT panel_connect to a guessed rail node id — call this with the interior node + the output you want exposed. Read
|
|
865
|
-
from_node_id: z.number().int().describe("Interior (inner) node id whose output to expose (from
|
|
952
|
+
def("panel_expose_subgraph_output", "Wire an interior node's OUTPUT to the subgraph's OUTPUT RAIL — i.e. expose it as a SUBGRAPH OUTPUT on the boundary so the PARENT graph can connect to the subgraph node's new output slot. You MUST be INSIDE the subgraph first (panel_enter_subgraph). This is the correct way to \"wire an internal output to the subgraph's output rail\": do NOT panel_connect to a guessed rail node id — call this with the interior node + the output you want exposed. Read panel_query_graph's `rails` to see the resulting boundary slots. `from_output` is an output slot NAME ('IMAGE', 'LATENT') or numeric index. Optional `name` titles the new boundary output (defaults from the source slot). Undoable with Ctrl+Z.", {
|
|
953
|
+
from_node_id: z.number().int().describe("Interior (inner) node id whose output to expose (from panel_query_graph while inside the subgraph)."),
|
|
866
954
|
from_output: slotRef.describe("Output slot name (e.g. 'IMAGE', 'LATENT') or numeric index on that node."),
|
|
867
955
|
name: z.string().optional().describe("Optional name for the new subgraph output (boundary slot). Defaults from the source slot."),
|
|
868
956
|
}, async (args, ctx) => ctx.call({
|
|
@@ -871,8 +959,8 @@ export function buildPanelToolDefs() {
|
|
|
871
959
|
from_output: args.from_output,
|
|
872
960
|
name: args.name,
|
|
873
961
|
}, 15000)),
|
|
874
|
-
def("panel_expose_subgraph_input", "Wire an interior node's INPUT to the subgraph's INPUT RAIL — i.e. expose it as a SUBGRAPH INPUT on the boundary so the PARENT graph can feed the subgraph node's new input slot. You MUST be INSIDE the subgraph first (panel_enter_subgraph). This is the correct way to wire an internal input to the subgraph's input rail: do NOT panel_connect to a guessed rail node id — call this with the interior node + the input you want exposed. Read
|
|
875
|
-
to_node_id: z.number().int().describe("Interior (inner) node id whose input to expose (from
|
|
962
|
+
def("panel_expose_subgraph_input", "Wire an interior node's INPUT to the subgraph's INPUT RAIL — i.e. expose it as a SUBGRAPH INPUT on the boundary so the PARENT graph can feed the subgraph node's new input slot. You MUST be INSIDE the subgraph first (panel_enter_subgraph). This is the correct way to wire an internal input to the subgraph's input rail: do NOT panel_connect to a guessed rail node id — call this with the interior node + the input you want exposed. Read panel_query_graph's `rails` to see the resulting boundary slots. `to_input` is an input slot NAME ('model', 'pixels') or numeric index. Optional `name` titles the new boundary input (defaults from the target slot). Undoable with Ctrl+Z.", {
|
|
963
|
+
to_node_id: z.number().int().describe("Interior (inner) node id whose input to expose (from panel_query_graph while inside the subgraph)."),
|
|
876
964
|
to_input: slotRef.describe("Input slot name (e.g. 'model', 'pixels') or numeric index on that node."),
|
|
877
965
|
name: z.string().optional().describe("Optional name for the new subgraph input (boundary slot). Defaults from the target slot."),
|
|
878
966
|
}, async (args, ctx) => ctx.call({
|
|
@@ -881,7 +969,7 @@ export function buildPanelToolDefs() {
|
|
|
881
969
|
to_input: args.to_input,
|
|
882
970
|
name: args.name,
|
|
883
971
|
}, 15000)),
|
|
884
|
-
def("panel_unpack_subgraph", "EXPAND / DISSOLVE a subgraph node on the user's open graph — inline its interior nodes back into the PARENT graph, rewire all external links to those now-inlined nodes, and remove the subgraph wrapper. This is the frontend's \"Unpack Subgraph\" (litegraph LGraph.unpackSubgraph) and the exact INVERSE of panel_create_subgraph. Use it to flatten a stage that was over-nested, or to edit interior nodes directly at the parent level. The interior nodes reappear on the parent canvas with their connections preserved. Undoable with Ctrl+Z.", { node_id: z.number().int().describe("Subgraph node id to unpack/dissolve (is_subgraph=true, from
|
|
972
|
+
def("panel_unpack_subgraph", "EXPAND / DISSOLVE a subgraph node on the user's open graph — inline its interior nodes back into the PARENT graph, rewire all external links to those now-inlined nodes, and remove the subgraph wrapper. This is the frontend's \"Unpack Subgraph\" (litegraph LGraph.unpackSubgraph) and the exact INVERSE of panel_create_subgraph. Use it to flatten a stage that was over-nested, or to edit interior nodes directly at the parent level. The interior nodes reappear on the parent canvas with their connections preserved. Undoable with Ctrl+Z.", { node_id: z.number().int().describe("Subgraph node id to unpack/dissolve (is_subgraph=true, from panel_graph_outline / panel_query_graph).") }, async (args, ctx) => ctx.call({ cmd: "graph_unpack_subgraph", node_id: args.node_id }, 15000)),
|
|
885
973
|
def("panel_search_nodes", "Search installable custom-node packs via the user's BUILT-IN ComfyUI Manager (the same source the Manager UI uses). Returns matching packs {id, title, description}. Use the `id` with panel_install_node. Prefer this over the headless search_custom_nodes tool — it works against the user's actual (Desktop) Manager.", { query: z.string().describe("Search text, e.g. 'kjnodes', 'controlnet', 'ipadapter'."), limit: z.number().int().min(1).max(40).optional() }, async (args, ctx) => ctx.call({ cmd: "nodes_search", query: args.query, limit: args.limit }, 20000)),
|
|
886
974
|
def("panel_list_nodes", "List the custom-node packs currently installed in the user's ComfyUI (via the built-in Manager). Read-only.", {}, async (_args, ctx) => ctx.call({ cmd: "nodes_list" }, 20000)),
|
|
887
975
|
def("panel_install_node", "Install a custom-node pack into the user's ComfyUI via the BUILT-IN Manager (queues the install). Pass `id` (registry id like 'comfyui-kjnodes' or 'author/repo') from panel_search_nodes, or `repository` (git URL) for a nightly install. A ComfyUI restart (panel_restart_comfyui) is usually required afterward to load the nodes — poll panel_node_queue_status first. Prefer this over the headless install_custom_node tool. " +
|
|
@@ -966,17 +1054,49 @@ export function buildPanelToolDefs() {
|
|
|
966
1054
|
resolved.push({ kind, dataUrl, filename, caption: item.caption });
|
|
967
1055
|
}
|
|
968
1056
|
else {
|
|
969
|
-
// ComfyUI /view ref
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1057
|
+
// ComfyUI /view ref. A browser panel fetches it same-origin — but a
|
|
1058
|
+
// HEADLESS (mobile/remote) client can't reach ComfyUI, so resolve the
|
|
1059
|
+
// bytes HERE and inline them as a data URL. Best-effort: any failure
|
|
1060
|
+
// (fetch error, non-media, too big) falls back to forwarding the ref,
|
|
1061
|
+
// which the client renders as a caption card.
|
|
1062
|
+
let inlined = false;
|
|
1063
|
+
if (ctx.bridge.isHeadless(ctx.tabId)) {
|
|
1064
|
+
try {
|
|
1065
|
+
const base = (process.env.COMFYUI_URL ?? "http://127.0.0.1:8188").replace(/\/+$/, "");
|
|
1066
|
+
const qs = new URLSearchParams({ filename: src.filename, type: src.type ?? "output" });
|
|
1067
|
+
if (src.subfolder)
|
|
1068
|
+
qs.set("subfolder", src.subfolder);
|
|
1069
|
+
const resp = await fetch(`${base}/view?${qs.toString()}`);
|
|
1070
|
+
if (resp.ok) {
|
|
1071
|
+
const mime = resp.headers.get("content-type") ?? "";
|
|
1072
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
1073
|
+
if ((mime.startsWith("image/") || mime.startsWith("video/")) && buf.length <= MAX_BYTES) {
|
|
1074
|
+
resolved.push({
|
|
1075
|
+
kind: mime.startsWith("video/") ? "video" : "image",
|
|
1076
|
+
dataUrl: `data:${mime};base64,${buf.toString("base64")}`,
|
|
1077
|
+
filename: src.filename,
|
|
1078
|
+
caption: item.caption,
|
|
1079
|
+
});
|
|
1080
|
+
inlined = true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
catch {
|
|
1085
|
+
// fall through to the viewRef path
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (!inlined) {
|
|
1089
|
+
resolved.push({
|
|
1090
|
+
kind: "viewRef",
|
|
1091
|
+
viewRef: {
|
|
1092
|
+
filename: src.filename,
|
|
1093
|
+
subfolder: src.subfolder,
|
|
1094
|
+
type: src.type,
|
|
1095
|
+
},
|
|
973
1096
|
filename: src.filename,
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
filename: src.filename,
|
|
978
|
-
caption: item.caption,
|
|
979
|
-
});
|
|
1097
|
+
caption: item.caption,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
980
1100
|
}
|
|
981
1101
|
}
|
|
982
1102
|
return ctx.call({ cmd: "show_media", items: resolved }, 60000);
|