comfyui-mcp 0.50.7 → 0.50.8
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.
|
@@ -65,7 +65,7 @@ import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpSe
|
|
|
65
65
|
import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey, receiptDisclosures, shadowedNote, storeDamageNote, } from "../services/panel-secrets.js";
|
|
66
66
|
import { flattenUiWorkflow } from "../services/flatten-workflow.js";
|
|
67
67
|
import { describeUnappliedFilters } from "./civitai-filter-guard.js";
|
|
68
|
-
import { recordTodo } from "./todo-state.js";
|
|
68
|
+
import { recordTodo, normalizeTodoItems, TODO_STATUS_INPUTS } from "./todo-state.js";
|
|
69
69
|
import { applyCapturedWidgetValues } from "../services/live-widget-overlay.js";
|
|
70
70
|
import { listWorkflowLibraryKeys, userdataFetch } from "../services/userdata-library.js";
|
|
71
71
|
import { getNsfwConsent, setNsfwConsent } from "../services/panel-settings.js";
|
|
@@ -4992,6 +4992,66 @@ function validatePanelEditNodeArgs(args) {
|
|
|
4992
4992
|
* forwarded to the wire UNTOUCHED (contrast workflow_uuid, which is bridge-owned
|
|
4993
4993
|
* and always overwritten). Optional everywhere; never required.
|
|
4994
4994
|
*/
|
|
4995
|
+
/**
|
|
4996
|
+
* #845 — a node id the tools THEMSELVES printed must be accepted back.
|
|
4997
|
+
*
|
|
4998
|
+
* Every panel tool took `z.number().int()` for a node id, while the graph
|
|
4999
|
+
* readers return ids as STRINGS (`"id": "42"`). So the obvious move — copy an id
|
|
5000
|
+
* out of panel_query_graph, paste it into panel_select_nodes — failed on the
|
|
5001
|
+
* first attempt, every time, with a raw zod `expected number, received string`.
|
|
5002
|
+
* The reporter hit it doing exactly that.
|
|
5003
|
+
*
|
|
5004
|
+
* Nothing about `"42"` is ambiguous. Accept both spellings and normalize to the
|
|
5005
|
+
* number the wire has always carried, so the round trip closes.
|
|
5006
|
+
*
|
|
5007
|
+
* DELIBERATELY STRICT about what counts as a node id: only an integer, or a
|
|
5008
|
+
* string that is exactly an integer. `"42px"`, `"4.5"`, `""` and `"5:12"` are
|
|
5009
|
+
* still rejected. The last one matters — a subgraph-qualified id is a real
|
|
5010
|
+
* shape in newer ComfyUI, and silently truncating it to `5` would target the
|
|
5011
|
+
* WRONG node rather than fail. If those need supporting, that is a separate,
|
|
5012
|
+
* deliberate change to the wire contract, not something to fall out of a coerce.
|
|
5013
|
+
*/
|
|
5014
|
+
const nodeId = () => z.union([z.number().int(), z.string().regex(/^-?\d+$/, "a node id must be an integer")]).transform((v) => (typeof v === "number" ? v : Number.parseInt(v, 10)));
|
|
5015
|
+
/**
|
|
5016
|
+
* #845 — which `panel_canvas` arguments the chosen action actually consumes.
|
|
5017
|
+
*
|
|
5018
|
+
* The tool accepted node_id/dx/dy/scale for every action and forwarded them all,
|
|
5019
|
+
* so an argument the action ignores vanished without a word. The reporter passed
|
|
5020
|
+
* `zoom: 0.55` alongside `center_on_node` and got `scale: 0.067` back — which
|
|
5021
|
+
* reads as "your zoom was applied and then overridden", when in truth it was
|
|
5022
|
+
* never applied at all. The panel's center_on_node case sets the offset and
|
|
5023
|
+
* touches the scale not at all.
|
|
5024
|
+
*
|
|
5025
|
+
* Naming what an action ignores is the whole fix. It is NOT an error — passing a
|
|
5026
|
+
* harmless extra argument should not fail a viewport move — but it must not be
|
|
5027
|
+
* silent either.
|
|
5028
|
+
*/
|
|
5029
|
+
const CANVAS_ACTION_ARGS = {
|
|
5030
|
+
fit: [], // computes its own framing from the graph bounds
|
|
5031
|
+
center_on_node: ["node_id"],
|
|
5032
|
+
pan: ["dx", "dy"],
|
|
5033
|
+
zoom: ["scale"],
|
|
5034
|
+
};
|
|
5035
|
+
/** Supplied-but-unused argument names for `action`, in a caller-facing spelling. */
|
|
5036
|
+
export function ignoredCanvasArgs(action, supplied) {
|
|
5037
|
+
const used = CANVAS_ACTION_ARGS[action];
|
|
5038
|
+
if (!used)
|
|
5039
|
+
return []; // unknown action — the enum rejects it; never guess here
|
|
5040
|
+
const label = { scale: "scale/zoom" };
|
|
5041
|
+
return ["node_id", "dx", "dy", "scale"]
|
|
5042
|
+
.filter((k) => supplied[k] !== undefined && !used.includes(k))
|
|
5043
|
+
.map((k) => label[k] ?? k);
|
|
5044
|
+
}
|
|
5045
|
+
/** Append a disclosure line to a successful text result, leaving errors alone. */
|
|
5046
|
+
function appendNote(res, note) {
|
|
5047
|
+
const first = res.content[0];
|
|
5048
|
+
if (!first || first.type !== "text")
|
|
5049
|
+
return res;
|
|
5050
|
+
return {
|
|
5051
|
+
...res,
|
|
5052
|
+
content: [{ ...first, text: `${first.text}\n\n${note}` }, ...res.content.slice(1)],
|
|
5053
|
+
};
|
|
5054
|
+
}
|
|
4995
5055
|
const RETRY_OF_ARG = {
|
|
4996
5056
|
retry_of: z
|
|
4997
5057
|
.string()
|
|
@@ -5252,7 +5312,7 @@ export function buildPanelToolDefs() {
|
|
|
5252
5312
|
},
|
|
5253
5313
|
])),
|
|
5254
5314
|
def("panel_audit_prompt_director", "Audit Prompt Director on the LIVE canvas without changing it. Correlates Prompt Director/Producer/Auto/Context/Reference/Critic widget values and wiring with detected model-loader filenames, every LoRA loader's actual model/CLIP strengths, and Prompt Director's latest sanitized runtime edit plan, resolved Model Explorer metadata, warnings, exact final prompt, and critic verdict. Returns observations plus proposed panel_set_widget changes with requires_confirmation=true. Call this when Prompt Director nodes are present, before saying the model/LoRA setup is correct, or when an edit prompt is ignored. READ-ONLY: present useful findings to the user and ask before applying any recommendation unless they already explicitly asked you to fix it.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_prompt_director_audit" })),
|
|
5255
|
-
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:
|
|
5315
|
+
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: nodeId().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => withTruncationHints(await ctx.call({ cmd: "graph_get_subgraph", node_id: args.node_id }), [
|
|
5256
5316
|
{
|
|
5257
5317
|
flag: "truncated",
|
|
5258
5318
|
key: "truncation_hint",
|
|
@@ -5353,7 +5413,7 @@ export function buildPanelToolDefs() {
|
|
|
5353
5413
|
// placeholder — that fetch can outlast the 6000 ms default on a large
|
|
5354
5414
|
// install. Give it the bounded refresh ack budget.
|
|
5355
5415
|
ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS)),
|
|
5356
|
-
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id:
|
|
5416
|
+
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
|
|
5357
5417
|
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) => {
|
|
5358
5418
|
const decision = await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas");
|
|
5359
5419
|
if (decision === "timeout") {
|
|
@@ -5573,11 +5633,11 @@ export function buildPanelToolDefs() {
|
|
|
5573
5633
|
}
|
|
5574
5634
|
}),
|
|
5575
5635
|
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.", {
|
|
5576
|
-
from_node_id:
|
|
5636
|
+
from_node_id: nodeId().describe("Source node id."),
|
|
5577
5637
|
from_output: slotRef
|
|
5578
5638
|
.optional()
|
|
5579
5639
|
.describe("Source output slot name or index; omit to auto-match by type (prefers an unconnected, exact-type input; `*` wildcards match last)."),
|
|
5580
|
-
to_node_id:
|
|
5640
|
+
to_node_id: nodeId().describe("Target node id."),
|
|
5581
5641
|
to_input: slotRef
|
|
5582
5642
|
.optional()
|
|
5583
5643
|
.describe("Target input slot name or index; omit to auto-match by type (prefers an unconnected, exact-type input; `*` wildcards match last)."),
|
|
@@ -5604,11 +5664,11 @@ export function buildPanelToolDefs() {
|
|
|
5604
5664
|
auto_match: args.auto_match,
|
|
5605
5665
|
})),
|
|
5606
5666
|
def("panel_disconnect", "Disconnect an input slot of a node in the user's open graph. Undoable with Ctrl+Z.", {
|
|
5607
|
-
node_id:
|
|
5667
|
+
node_id: nodeId().describe("Node id whose input to disconnect."),
|
|
5608
5668
|
input: slotRef.optional().describe("Input slot name or index (default 0)."),
|
|
5609
5669
|
}, async (args, ctx) => ctx.call({ cmd: "graph_disconnect", node_id: args.node_id, input: args.input })),
|
|
5610
5670
|
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. To CLEAR a text widget to an empty string, pass `clear: true` (some MCP clients drop an empty-string `value` from the serialized payload, so `value: \"\"` may not arrive — `clear: true` always works). For the LTXDirector timeline node (WhatDreamsCost CSGlide), set `timeline_data` with the FULL timeline JSON (segments + global_prompt) to drive its custom timeline UI — this re-syncs the editor and regenerates its derived `local_prompts`/`segment_lengths`/`guide_strength` widgets; setting those derived widgets directly is refused (they are silently reverted).", {
|
|
5611
|
-
node_id:
|
|
5671
|
+
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
5612
5672
|
widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
|
|
5613
5673
|
value: z
|
|
5614
5674
|
.union([z.string(), z.number(), z.boolean()])
|
|
@@ -5634,7 +5694,7 @@ export function buildPanelToolDefs() {
|
|
|
5634
5694
|
return ctx.call({ cmd: "graph_set_widget", node_id: args.node_id, widget: args.widget, value }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
5635
5695
|
}),
|
|
5636
5696
|
def("panel_set_property", "Set a node's LiteGraph PROPERTY (the right-click → Properties panel), NOT a widget — the counterpart to panel_set_widget, which only reaches `widgets`. Many custom nodes are configured entirely through node properties: e.g. the rgthree Fast Groups Bypasser's filters `matchTitle`, `matchColors`, `sort`, and `toggleRestriction` are node properties, and without `matchTitle` the node enumerates EVERY group in the workflow (a footgun). Sets node.properties[name] and, when the node defines an onPropertyChanged callback (rgthree and many LiteGraph nodes do), invokes it so the change takes effect LIVE (e.g. rgthree re-filters its group list). Returns the previous and new value. Undoable with Ctrl+Z.", {
|
|
5637
|
-
node_id:
|
|
5697
|
+
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
5638
5698
|
name: z
|
|
5639
5699
|
.string()
|
|
5640
5700
|
.describe("Property name from the node's right-click → Properties panel (e.g. 'matchTitle', 'matchColors', 'sort', 'toggleRestriction')."),
|
|
@@ -5643,8 +5703,8 @@ export function buildPanelToolDefs() {
|
|
|
5643
5703
|
.describe("New property value (string/number/boolean/null). For the rgthree Fast Groups Bypasser, matchTitle is a title substring/regex filter."),
|
|
5644
5704
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_node_property", node_id: args.node_id, name: args.name, value: args.value })),
|
|
5645
5705
|
def("panel_edit_node", "Atomically edit one node, or apply the same edit to several nodes. Pass exactly one of node_id or node_ids, plus at least one field. In one Ctrl+Z step you can move (pos), resize (size — including Note/MarkdownNote), retitle, recolor, change shape, collapse, pin, or set execution mode. Widget values, LiteGraph properties, links, and slot order stay on their dedicated tools. For a multi-node call, position/size/title/mode apply the same value to every target. Color fields accept #RGB, #RGBA, #RRGGBB, or #RRGGBBAA; null clears a color. Bypassing a subgraph retains panel_set_node_mode's unsafe-boundary guard; force:true is required to override it. Undoable with Ctrl+Z.", {
|
|
5646
|
-
node_id:
|
|
5647
|
-
node_ids: z.array(
|
|
5706
|
+
node_id: nodeId().optional().describe("One node id from panel_graph_outline / panel_query_graph. Provide this OR node_ids, not both."),
|
|
5707
|
+
node_ids: z.array(nodeId()).min(1).optional().describe("Several node ids that receive the same presentation edit. Provide this OR node_id, not both."),
|
|
5648
5708
|
pos: xy().optional().describe("New canvas [x, y]."),
|
|
5649
5709
|
size: nodeSize().optional().describe("New [width, height] in canvas px. Uses the node's setSize so DOM-widget nodes reflow and minimum sizes are honored."),
|
|
5650
5710
|
title: z.string().optional().describe("New header title."),
|
|
@@ -5680,8 +5740,8 @@ export function buildPanelToolDefs() {
|
|
|
5680
5740
|
// Keep legacy bridge commands behind compatibility tool names. graph_edit_node
|
|
5681
5741
|
// is newer than several installed panels, while current panels adapt these
|
|
5682
5742
|
// commands into the same atomic implementation.
|
|
5683
|
-
def("panel_move_node", "Compatibility wrapper for panel_edit_node(pos).", { node_id:
|
|
5684
|
-
def("panel_resize_node", "Compatibility wrapper for panel_edit_node(size).", { node_id:
|
|
5743
|
+
def("panel_move_node", "Compatibility wrapper for panel_edit_node(pos).", { node_id: nodeId(), pos: xy() }, async (args, ctx) => ctx.call({ cmd: "graph_move_node", node_id: args.node_id, pos: args.pos })),
|
|
5744
|
+
def("panel_resize_node", "Compatibility wrapper for panel_edit_node(size).", { node_id: nodeId(), size: nodeSize() }, async (args, ctx) => ctx.call({ cmd: "graph_resize_node", node_id: args.node_id, size: args.size })),
|
|
5685
5745
|
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).", {
|
|
5686
5746
|
node_ids: z
|
|
5687
5747
|
.array(z.number().int())
|
|
@@ -5712,18 +5772,45 @@ export function buildPanelToolDefs() {
|
|
|
5712
5772
|
}, 15000)),
|
|
5713
5773
|
def("panel_canvas", "MOVE the user's viewport: '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. It changes what they are looking AT and returns nothing about the graph — to find out what is ON the canvas, use panel_graph_outline. View-only.", {
|
|
5714
5774
|
action: z.enum(["fit", "center_on_node", "pan", "zoom"]),
|
|
5715
|
-
node_id:
|
|
5775
|
+
node_id: nodeId().optional().describe("Required for center_on_node."),
|
|
5716
5776
|
dx: z.number().optional().describe("Pan delta x."),
|
|
5717
5777
|
dy: z.number().optional().describe("Pan delta y."),
|
|
5718
5778
|
scale: z.number().optional().describe("Absolute zoom for 'zoom' (0.05–4, 1 = 100%)."),
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5779
|
+
// #845 — the tool used two names for one concept: `action:"zoom"` requires
|
|
5780
|
+
// `scale`, and a `zoom` argument was simply not in the schema, so passing
|
|
5781
|
+
// it was dropped without a word. Accept it as the alias it obviously is.
|
|
5782
|
+
zoom: z.number().optional().describe("Alias for `scale`."),
|
|
5783
|
+
}, async (args, ctx) => {
|
|
5784
|
+
const scale = args.scale ?? args.zoom;
|
|
5785
|
+
const res = await ctx.call({
|
|
5786
|
+
cmd: "graph_canvas",
|
|
5787
|
+
action: args.action,
|
|
5788
|
+
node_id: args.node_id,
|
|
5789
|
+
dx: args.dx,
|
|
5790
|
+
dy: args.dy,
|
|
5791
|
+
scale,
|
|
5792
|
+
});
|
|
5793
|
+
// #845 — an argument this action does not consume was SILENTLY dropped.
|
|
5794
|
+
// The reporter passed zoom:0.55 to center_on_node, got scale 0.067 back,
|
|
5795
|
+
// and had no way to tell the zoom had been ignored rather than applied
|
|
5796
|
+
// and overridden. Only `zoom` applies a scale — the panel's
|
|
5797
|
+
// center_on_node sets the offset alone — so say which arguments this
|
|
5798
|
+
// action actually used.
|
|
5799
|
+
const ignored = ignoredCanvasArgs(String(args.action), {
|
|
5800
|
+
node_id: args.node_id,
|
|
5801
|
+
dx: args.dx,
|
|
5802
|
+
dy: args.dy,
|
|
5803
|
+
scale,
|
|
5804
|
+
});
|
|
5805
|
+
if (!ignored.length || res.isError)
|
|
5806
|
+
return res;
|
|
5807
|
+
return appendNote(res, `Note: action:"${args.action}" does not use ${ignored.join(", ")} — ` +
|
|
5808
|
+
`${ignored.length === 1 ? "it was" : "they were"} ignored, not applied. ` +
|
|
5809
|
+
(ignored.includes("scale/zoom")
|
|
5810
|
+
? `To change the zoom, call panel_canvas again with action:"zoom" and scale. `
|
|
5811
|
+
: "") +
|
|
5812
|
+
`Any values in this result are the canvas's actual state.`);
|
|
5813
|
+
}),
|
|
5727
5814
|
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). On success it confirms the run was queued; if ComfyUI REFUSES the prompt (validation failure on either channel — per-node node_errors OR a top-level error like a missing node type) it returns a FAILURE with that rejection detail, never a false 'queued'. 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. The output node may be NESTED inside a subgraph — just pass its id (resolved in the scope you're currently viewing, then anywhere in the workflow); the tool builds the nested execution path for you. Omit it to run the whole graph. DUPLICATE FENCE (#862): if a render this session cannot account for is already in flight (after a reconnect this is usually YOUR earlier render still running — the queue record does not survive a restart), the run is REFUSED before anything is queued and the in-flight prompt is named; inspect queue (action:'list') first, or pass allow_duplicate:true only to deliberately stack behind it. Use this so the render runs on THEIR canvas and they see the result.", {
|
|
5728
5815
|
batch_count: z
|
|
5729
5816
|
.number()
|
|
@@ -6366,8 +6453,13 @@ export function buildPanelToolDefs() {
|
|
|
6366
6453
|
items: z
|
|
6367
6454
|
.array(z.object({
|
|
6368
6455
|
text: z.string().describe("Short step description (a few words)."),
|
|
6456
|
+
// #1018 — accept the synonyms agents actually produce (in_progress,
|
|
6457
|
+
// completed, …) and normalize them in the handler. The DESCRIPTION
|
|
6458
|
+
// still teaches only the canonical trio: one vocabulary to learn,
|
|
6459
|
+
// and no round trip lost to a rejected first call over a spelling
|
|
6460
|
+
// whose intent was never in doubt.
|
|
6369
6461
|
status: z
|
|
6370
|
-
.enum(
|
|
6462
|
+
.enum(TODO_STATUS_INPUTS)
|
|
6371
6463
|
.optional()
|
|
6372
6464
|
.describe("Step state (default 'pending'). Mark the one you're on 'active'."),
|
|
6373
6465
|
}))
|
|
@@ -6382,6 +6474,10 @@ export function buildPanelToolDefs() {
|
|
|
6382
6474
|
// bound tab is a headless (mobile/remote) client, resolve the live desktop
|
|
6383
6475
|
// canvas tab instead of dispatching at the headless client — which would
|
|
6384
6476
|
// reject with the misleading "mobile client has no open canvas".
|
|
6477
|
+
// #1018 — canonicalize ONCE, here, before anything reads the list. The
|
|
6478
|
+
// panel, recordTodo's snapshot (#977) and the completion-directive logic
|
|
6479
|
+
// all keep seeing only pending/active/done.
|
|
6480
|
+
const items = normalizeTodoItems(args.items);
|
|
6385
6481
|
const redirect = desktopCanvasRedirect(ctx, "panel_set_todo");
|
|
6386
6482
|
if (redirect?.error)
|
|
6387
6483
|
return fail(redirect.error);
|
|
@@ -6389,14 +6485,14 @@ export function buildPanelToolDefs() {
|
|
|
6389
6485
|
// #977 — the desktop redirect writes the checklist to ANOTHER tab, so
|
|
6390
6486
|
// record it against that one. Keying it on ctx.tabId would leave the
|
|
6391
6487
|
// tab that actually holds the plan looking planless.
|
|
6392
|
-
recordTodo(redirect.tabId,
|
|
6393
|
-
return dispatchToTab(ctx, redirect.tabId, { cmd: "set_todo", items
|
|
6488
|
+
recordTodo(redirect.tabId, items);
|
|
6489
|
+
return dispatchToTab(ctx, redirect.tabId, { cmd: "set_todo", items }, 15000, () => reResolveDesktopTab(ctx, "panel_set_todo"));
|
|
6394
6490
|
}
|
|
6395
6491
|
// #977 — retain what the agent DECLARED, so a later render completion can
|
|
6396
6492
|
// tell "you are mid-sweep" from "that was the last thing you were doing".
|
|
6397
6493
|
// The panel keeps the UI copy; this is the orchestrator's own record.
|
|
6398
|
-
recordTodo(ctx.tabId,
|
|
6399
|
-
return ctx.call({ cmd: "set_todo", items
|
|
6494
|
+
recordTodo(ctx.tabId, items);
|
|
6495
|
+
return ctx.call({ cmd: "set_todo", items }, 15000);
|
|
6400
6496
|
}),
|
|
6401
6497
|
def("panel_open_civitai", "Open the in-panel CivitAI browser for the user, pre-seeded with a search term and suggested filters, so they can VISUALLY browse and pick a model / LoRA / checkpoint / workflow / image. When the user asks about — or you're recommending — specific CivitAI models/LoRAs/checkpoints (e.g. 'what's a good relight LoRA?'), PREFER opening this docked browser and highlighting your picks over a text-only answer: it docks beside the chat (dock defaults true) so chat and results stay visible together, and it lets the user SEE the actual cards instead of reading a table. Typical show-don't-tell flow: panel_open_civitai (docked) → panel_civitai_search to refine → panel_civitai_results to READ the metadata + URLs → panel_civitai_highlight the one(s) you recommend, with a brief text summary of why. Set a helpful query + filters matched to their goal (including the browsing level). Their selection comes back to you as a normal chat message — UNLESS the panel is muted, in which case they download it directly themselves. Prefer this over guessing a specific model or asking them to paste a URL.", {
|
|
6402
6498
|
query: z
|
|
@@ -6923,7 +7019,53 @@ export function buildPanelToolDefs() {
|
|
|
6923
7019
|
note: hint + rebindNote + (fence?.note ?? ""),
|
|
6924
7020
|
});
|
|
6925
7021
|
}),
|
|
6926
|
-
def("panel_new_workflow", "Open a brand-new BLANK workflow in a NEW TAB. Use this whenever the user wants a 'new workflow' / 'fresh canvas' / 'start over for a new project'. This does NOT touch their current workflow — it opens a separate tab. NEVER use panel_clear for a new workflow (panel_clear wipes the CURRENT graph and is only for 'clear/reset this canvas').", {}, async (_args, ctx) =>
|
|
7022
|
+
def("panel_new_workflow", "Open a brand-new BLANK workflow in a NEW TAB. Use this whenever the user wants a 'new workflow' / 'fresh canvas' / 'start over for a new project'. This does NOT touch their current workflow — it opens a separate tab. NEVER use panel_clear for a new workflow (panel_clear wipes the CURRENT graph and is only for 'clear/reset this canvas').", {}, async (_args, ctx) => {
|
|
7023
|
+
const res = await ctx.call({ cmd: "workflow_new" }, 15000);
|
|
7024
|
+
if (res.isError)
|
|
7025
|
+
return res;
|
|
7026
|
+
// #932 (recurrence on 0.50.6) — a NEW canvas needs a NEW fence.
|
|
7027
|
+
//
|
|
7028
|
+
// workflow_new authoritatively re-points the active workflow, exactly as
|
|
7029
|
+
// workflow_open does — but only the open path re-derived the command
|
|
7030
|
+
// fence afterwards (openWorkflowWithVerify). So this session kept the
|
|
7031
|
+
// PREVIOUS workflow's instance stamp while the user was now looking at a
|
|
7032
|
+
// brand-new blank canvas, and every stamped command after it failed with
|
|
7033
|
+
// "workflow instance mismatch". The reporter created a workflow and could
|
|
7034
|
+
// not add a single node to it.
|
|
7035
|
+
//
|
|
7036
|
+
// Refresh from the panel's own live active record, the same way the open
|
|
7037
|
+
// path does. The panel mints the new canvas's identity EAGERLY at
|
|
7038
|
+
// creation ("so the key exists BEFORE the first edit"), so it is readable
|
|
7039
|
+
// by the time this runs — it is simply not carried on the workflow_new
|
|
7040
|
+
// reply, which returns key/routing_key but no workflow_uuid.
|
|
7041
|
+
//
|
|
7042
|
+
// NEVER fails the call on a rebind miss: the workflow WAS created, and
|
|
7043
|
+
// retracting that would be the worse lie. Disclose instead, so the agent
|
|
7044
|
+
// learns the graph tools are not yet usable here rather than discovering
|
|
7045
|
+
// it one confusing mismatch at a time.
|
|
7046
|
+
const fenceRebind = await rebindWorkflowFence(ctx);
|
|
7047
|
+
let canMutateNow;
|
|
7048
|
+
let refusalCause;
|
|
7049
|
+
try {
|
|
7050
|
+
if (ctx.tabGraphMutationCapability) {
|
|
7051
|
+
const cap = ctx.tabGraphMutationCapability();
|
|
7052
|
+
canMutateNow = cap.known ? cap.canMutate : undefined;
|
|
7053
|
+
if (cap.known && !cap.canMutate)
|
|
7054
|
+
refusalCause = cap.because;
|
|
7055
|
+
}
|
|
7056
|
+
else {
|
|
7057
|
+
canMutateNow = ctx.tabCanMutateGraph?.();
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
catch {
|
|
7061
|
+
canMutateNow = undefined; // a guard that can throw is not a guard
|
|
7062
|
+
refusalCause = undefined;
|
|
7063
|
+
}
|
|
7064
|
+
const fence = describeFenceRebind(fenceRebind, canMutateNow, refusalCause);
|
|
7065
|
+
if (!fence || fence.binding === "bound")
|
|
7066
|
+
return res;
|
|
7067
|
+
return appendNote(res, `The blank workflow WAS created.${fence.note}`);
|
|
7068
|
+
}),
|
|
6927
7069
|
def("panel_open_workflow", "Open / switch to a workflow by path or filename (from panel_list_workflows). Switches the active tab to it. If the workflow was ALREADY open and its .json changed on disk out-of-band, the result carries stale:true (or stale:\"unknown\" when staleness couldn't be verified) with a stale_hint — the canvas still shows the version this tab loaded (it is NOT auto-reloaded); call panel_load_workflow to load the on-disk version.", { path: z.string().describe("Workflow path, filename, or key from panel_list_workflows.") },
|
|
6928
7070
|
// Verify-after-timeout (#215/#319/#496): a backgrounded/frozen or already-open
|
|
6929
7071
|
// tab can be slow to ack workflow_open even though the switch succeeded. On an
|
|
@@ -6938,8 +7080,8 @@ export function buildPanelToolDefs() {
|
|
|
6938
7080
|
path: z.string().optional().describe("Which workflow to close; omit for the active one."),
|
|
6939
7081
|
force: z.boolean().optional().describe("Close even with unsaved changes (discards them). Default false."),
|
|
6940
7082
|
}, async (args, ctx) => ctx.call({ cmd: "workflow_close", path: args.path, force: args.force }, 15000)),
|
|
6941
|
-
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(
|
|
6942
|
-
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(
|
|
7083
|
+
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(nodeId()).describe("Node ids to select.") }, async (args, ctx) => ctx.call({ cmd: "graph_select_nodes", node_ids: args.node_ids })),
|
|
7084
|
+
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(nodeId()).describe("Node ids to group into a subgraph.") }, async (args, ctx) => ctx.call({ cmd: "graph_create_subgraph", node_ids: args.node_ids }, 15000)),
|
|
6943
7085
|
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.)", {
|
|
6944
7086
|
group: z
|
|
6945
7087
|
.union([z.string(), z.number()])
|
|
@@ -6959,7 +7101,7 @@ export function buildPanelToolDefs() {
|
|
|
6959
7101
|
.describe("Reconnect pasted nodes' inputs to existing nodes where they line up (default false)."),
|
|
6960
7102
|
}, async (args, ctx) => ctx.call({ cmd: "graph_paste_nodes", pos: args.pos, connect_inputs: args.connect_inputs }, 15000)),
|
|
6961
7103
|
def("panel_save_subgraph", "Save a SUBGRAPH node to the user's reusable blueprint LIBRARY (publish), so it can be dropped into any workflow later. Pass node_id to pick the subgraph node (else a single selected subgraph node is used) and name to title the blueprint (defaults to the node's title). Runs programmatically — NO save dialog pops. The blueprint becomes the addable type 'SubgraphBlueprint.<name>' (use panel_add_subgraph or panel_list_subgraphs). Returns {saved: {name, type}}.", {
|
|
6962
|
-
node_id:
|
|
7104
|
+
node_id: nodeId().optional().describe("Subgraph node id to publish (is_subgraph=true). Omit to use the selected subgraph node."),
|
|
6963
7105
|
name: z.string().optional().describe("Blueprint name. Defaults to the subgraph node's title."),
|
|
6964
7106
|
}, async (args, ctx) => ctx.call({ cmd: "graph_save_subgraph", node_id: args.node_id, name: args.name }, 20000)),
|
|
6965
7107
|
def("panel_list_subgraphs", "List the saved subgraph BLUEPRINTS in the user's library (from panel_save_subgraph, plus any global/bundled ones). Each entry has {name, type, display_name, description, is_global} — use name/type with panel_add_subgraph to drop it onto the canvas. Read-only.\n\nTOKEN-BOUNDED like the other reads (panel#690). The reply's count field is always the LIBRARY TOTAL, so compare it against the returned array. When entries are withheld the reply carries truncated:true, a returned field, and a note — an absent entry in a TRUNCATED list is NOT evidence the blueprint does not exist, so narrow with `filter` (or raise `limit`, up to 500) before concluding anything. When a filter is applied the reply's matched field reports how many the filter selected, distinct from the count field, so matched:0 against a non-zero count means the filter missed, not that the library is empty.", {
|
|
@@ -7017,14 +7159,14 @@ export function buildPanelToolDefs() {
|
|
|
7017
7159
|
bounds: args.bounds,
|
|
7018
7160
|
}, 15000)),
|
|
7019
7161
|
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)),
|
|
7020
|
-
def("panel_set_node_title", "Compatibility wrapper for panel_edit_node(title).", { node_id:
|
|
7021
|
-
def("panel_set_node_collapsed", "Compatibility wrapper for panel_edit_node(collapsed).", { node_id:
|
|
7162
|
+
def("panel_set_node_title", "Compatibility wrapper for panel_edit_node(title).", { node_id: nodeId(), title: z.string() }, async (args, ctx) => ctx.call({ cmd: "graph_set_title", node_id: args.node_id, title: args.title }, 15000)),
|
|
7163
|
+
def("panel_set_node_collapsed", "Compatibility wrapper for panel_edit_node(collapsed).", { node_id: nodeId(), collapsed: z.boolean().optional() }, async (args, ctx) => ctx.call({ cmd: "graph_set_node_collapsed", node_id: args.node_id, collapsed: args.collapsed ?? true })),
|
|
7022
7164
|
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" +
|
|
7023
7165
|
"• 'active' — normal: the node executes.\n" +
|
|
7024
7166
|
"• '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" +
|
|
7025
7167
|
"• 'mute' — the node AND everything DOWNSTREAM of it do NOT execute (no pass-through). Use to fully switch off a branch/output.\n" +
|
|
7026
7168
|
"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. UNSAFE-BYPASS GUARD: bypassing a SUBGRAPH node whose boundary inputs are ordered differently from its outputs is REJECTED — ComfyUI forwards each output from the input at the SAME index, so e.g. an IMAGE output backed by a BBOX_DETECTOR input would silently feed the wrong type downstream. Re-order the boundary inputs or add an explicit ImpactSwitch to choose the passthrough; pass force:true only if you truly intend the positional forward. Undoable with Ctrl+Z.", {
|
|
7027
|
-
node_id:
|
|
7169
|
+
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
7028
7170
|
mode: z
|
|
7029
7171
|
.enum(["active", "bypass", "mute"])
|
|
7030
7172
|
.describe("'active' = runs normally; 'bypass' = skipped, passes input through (downstream still runs); 'mute' = node and everything downstream do not execute."),
|
|
@@ -7034,7 +7176,7 @@ export function buildPanelToolDefs() {
|
|
|
7034
7176
|
.describe("Override the unsafe-bypass guard on a subgraph node (proceed with a positional boundary forward even when input/output types don't line up by index). Omit for normal safe behaviour."),
|
|
7035
7177
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_node_mode", node_id: args.node_id, mode: args.mode, force: args.force })),
|
|
7036
7178
|
def("panel_set_node_color", "Legacy color compatibility wrapper. Unlike panel_edit_node, color and bgcolor accept any CSS color string; when preset is supplied it wins over explicit colors, preserving the historical bridge behavior.", {
|
|
7037
|
-
node_id:
|
|
7179
|
+
node_id: nodeId(),
|
|
7038
7180
|
preset: z.enum(["red", "brown", "green", "blue", "pale_blue", "cyan", "purple", "yellow", "black"]).nullable().optional(),
|
|
7039
7181
|
color: z.string().nullable().optional(),
|
|
7040
7182
|
bgcolor: z.string().nullable().optional(),
|
|
@@ -7071,19 +7213,19 @@ export function buildPanelToolDefs() {
|
|
|
7071
7213
|
return fail(err);
|
|
7072
7214
|
}
|
|
7073
7215
|
}),
|
|
7074
|
-
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:
|
|
7216
|
+
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: nodeId().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => ctx.call({ cmd: "graph_enter_subgraph", node_id: args.node_id }, 15000)),
|
|
7075
7217
|
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)),
|
|
7076
7218
|
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'.", {
|
|
7077
7219
|
rail: z.enum(["input", "output"]).describe("Which boundary rail to move."),
|
|
7078
7220
|
pos: xy().describe("New top-left [x, y] (two numbers)."),
|
|
7079
7221
|
}, async (args, ctx) => ctx.call({ cmd: "graph_move_rail", rail: args.rail, pos: args.pos })),
|
|
7080
7222
|
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.", {
|
|
7081
|
-
node_id:
|
|
7223
|
+
node_id: nodeId().describe("Inner node id (from panel_query_graph while inside the subgraph)."),
|
|
7082
7224
|
widget: z.string().describe("Name of the widget on that node to promote (e.g. 'seed', 'steps', 'text')."),
|
|
7083
7225
|
demote: z.boolean().optional().describe("Set true to UN-promote (remove the widget from the parent node)."),
|
|
7084
7226
|
}, async (args, ctx) => ctx.call({ cmd: "graph_promote_widget", node_id: args.node_id, widget: args.widget, demote: args.demote }, 15000)),
|
|
7085
7227
|
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.", {
|
|
7086
|
-
from_node_id:
|
|
7228
|
+
from_node_id: nodeId().describe("Interior (inner) node id whose output to expose (from panel_query_graph while inside the subgraph)."),
|
|
7087
7229
|
from_output: slotRef.describe("Output slot name (e.g. 'IMAGE', 'LATENT') or numeric index on that node."),
|
|
7088
7230
|
name: z.string().optional().describe("Optional name for the new subgraph output (boundary slot). Defaults from the source slot."),
|
|
7089
7231
|
}, async (args, ctx) => ctx.call({
|
|
@@ -7093,7 +7235,7 @@ export function buildPanelToolDefs() {
|
|
|
7093
7235
|
name: args.name,
|
|
7094
7236
|
}, 15000)),
|
|
7095
7237
|
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.", {
|
|
7096
|
-
to_node_id:
|
|
7238
|
+
to_node_id: nodeId().describe("Interior (inner) node id whose input to expose (from panel_query_graph while inside the subgraph)."),
|
|
7097
7239
|
to_input: slotRef.describe("Input slot name (e.g. 'model', 'pixels') or numeric index on that node."),
|
|
7098
7240
|
name: z.string().optional().describe("Optional name for the new subgraph input (boundary slot). Defaults from the target slot."),
|
|
7099
7241
|
}, async (args, ctx) => ctx.call({
|
|
@@ -7102,7 +7244,7 @@ export function buildPanelToolDefs() {
|
|
|
7102
7244
|
to_input: args.to_input,
|
|
7103
7245
|
name: args.name,
|
|
7104
7246
|
}, 15000)),
|
|
7105
|
-
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:
|
|
7247
|
+
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: nodeId().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)),
|
|
7106
7248
|
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)),
|
|
7107
7249
|
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)),
|
|
7108
7250
|
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 search result whose `id` IS a git URL (legacy/repository-style entries) is auto-routed to a from-source 'nightly' install — 'latest' cannot resolve for those. 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. " +
|