@fre4x/comfyui 1.0.65 → 1.1.0-beta.2

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.
Files changed (3) hide show
  1. package/README.md +43 -5
  2. package/dist/index.js +1020 -112
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -30637,8 +30637,9 @@ function normalizeWidgetValue(value, typeName, extraInfo) {
30637
30637
  }
30638
30638
  return value;
30639
30639
  }
30640
- function convertWebUIToAPI(webUIJson, objectInfo) {
30640
+ function convertWebUIToAPIWithMetadata(webUIJson, objectInfo) {
30641
30641
  const prompt = {};
30642
+ const widgetPathMap = {};
30642
30643
  const links = webUIJson.links || [];
30643
30644
  const nodes = webUIJson.nodes || [];
30644
30645
  const serverNodeIds = /* @__PURE__ */ new Set();
@@ -30703,14 +30704,16 @@ function convertWebUIToAPI(webUIJson, objectInfo) {
30703
30704
  }
30704
30705
  continue;
30705
30706
  }
30707
+ const widgetSourceIndex = widgetIndex;
30706
30708
  const value = normalizeWidgetValue(
30707
- widgets[widgetIndex],
30709
+ widgets[widgetSourceIndex],
30708
30710
  typeName,
30709
30711
  inputExtraInfo
30710
30712
  );
30711
30713
  widgetIndex++;
30712
30714
  if (!isConnectedWidget) {
30713
30715
  promptNode.inputs[inputName] = value;
30716
+ widgetPathMap[`${nodeIdStr}.widgets[${widgetSourceIndex}]`] = `${nodeIdStr}.inputs.${inputName}`;
30714
30717
  }
30715
30718
  }
30716
30719
  };
@@ -30725,7 +30728,10 @@ function convertWebUIToAPI(webUIJson, objectInfo) {
30725
30728
  }
30726
30729
  prompt[nodeIdStr] = promptNode;
30727
30730
  }
30728
- return prompt;
30731
+ return {
30732
+ prompt,
30733
+ widgetPathMap
30734
+ };
30729
30735
  }
30730
30736
 
30731
30737
  // src/index.ts
@@ -30749,6 +30755,20 @@ var WorkflowInputError = class extends Error {
30749
30755
  this.name = "WorkflowInputError";
30750
30756
  }
30751
30757
  };
30758
+ var INPUT_SEGMENT = "inputs";
30759
+ var OverrideRecordSchema = z3.record(z3.string(), z3.unknown());
30760
+ var OverridesSchema = z3.union([OverrideRecordSchema, z3.string()]).describe(
30761
+ 'Dot-notation overrides as an object or JSON string. e.g. {"6.seed": 7}'
30762
+ );
30763
+ var WorkflowTimeoutError = class extends Error {
30764
+ constructor(promptId, timeoutSeconds, queueSnapshot) {
30765
+ super(`Timeout waiting for workflow ${promptId}`);
30766
+ this.promptId = promptId;
30767
+ this.timeoutSeconds = timeoutSeconds;
30768
+ this.queueSnapshot = queueSnapshot;
30769
+ this.name = "WorkflowTimeoutError";
30770
+ }
30771
+ };
30752
30772
  function isRecord2(value) {
30753
30773
  return typeof value === "object" && value !== null;
30754
30774
  }
@@ -30961,17 +30981,31 @@ ${detailSummary}` : error48.message
30961
30981
  isError: true
30962
30982
  };
30963
30983
  }
30964
- const errorMessage = error48 instanceof Error ? error48.message : String(error48);
30965
- if (errorMessage.includes("Timeout waiting for workflow")) {
30984
+ if (error48 instanceof WorkflowTimeoutError) {
30985
+ const queueSummary = formatQueueSnapshotText(error48.queueSnapshot);
30986
+ const promptVisibility = getPromptQueueVisibility(
30987
+ error48.promptId,
30988
+ error48.queueSnapshot
30989
+ );
30990
+ const promptVisibilityText = promptVisibility === true ? "The prompt is still visible in the ComfyUI queue snapshot." : promptVisibility === false ? "The prompt is not visible in the ComfyUI queue snapshot; inspect ComfyUI history or server logs if it never completes." : "ComfyUI queue state was unavailable at timeout.";
30966
30991
  return {
30967
30992
  content: [
30968
30993
  {
30969
30994
  type: "text",
30970
- text: `${errorMessage}.
30971
-
30972
- The workflow is likely still running on the server. You can re-attach to it and wait for completion by calling 'comfyui_wait_for_workflow' again with the same prompt_id.`
30995
+ text: [
30996
+ `${error48.message}.`,
30997
+ queueSummary,
30998
+ promptVisibilityText,
30999
+ `You can re-attach by calling 'comfyui_wait_for_workflow' again with prompt_id '${error48.promptId}'.`
31000
+ ].filter((line) => line && line.length > 0).join("\n")
30973
31001
  }
30974
31002
  ],
31003
+ structuredContent: {
31004
+ prompt_id: error48.promptId,
31005
+ timeout_seconds: error48.timeoutSeconds,
31006
+ queue: error48.queueSnapshot ?? null,
31007
+ prompt_visible_in_queue: promptVisibility
31008
+ },
30975
31009
  isError: true
30976
31010
  };
30977
31011
  }
@@ -30994,7 +31028,9 @@ function getWorkflowDirectories(extraPath) {
30994
31028
  if (extraPath) {
30995
31029
  directories.push(extraPath);
30996
31030
  }
30997
- return directories;
31031
+ return [
31032
+ ...new Set(directories.map((directory) => path.resolve(directory)))
31033
+ ];
30998
31034
  }
30999
31035
  function getWorkflowNameCandidates(name) {
31000
31036
  const trimmed = name.trim();
@@ -31022,6 +31058,22 @@ function isConnectionValue(value) {
31022
31058
  async function getObjectInfo() {
31023
31059
  return IS_MOCK ? MOCK_FIXTURES.object_info : await fetchComfyJson("/object_info");
31024
31060
  }
31061
+ function summarizeErrorMessage(error48) {
31062
+ return error48 instanceof Error ? error48.message : String(error48);
31063
+ }
31064
+ async function tryGetObjectInfo() {
31065
+ try {
31066
+ return {
31067
+ objectInfo: await getObjectInfo()
31068
+ };
31069
+ } catch (error48) {
31070
+ return {
31071
+ warning: `Node schema details unavailable from ComfyUI: ${summarizeErrorMessage(
31072
+ error48
31073
+ )}`
31074
+ };
31075
+ }
31076
+ }
31025
31077
  async function resolveWorkflowReference(args) {
31026
31078
  if (args.workflow_file_path) {
31027
31079
  const targetPath = args.workflow_file_path;
@@ -31067,6 +31119,7 @@ async function resolveWorkflowReference(args) {
31067
31119
  "workflow_id"
31068
31120
  );
31069
31121
  }
31122
+ const objectInfoAvailability = await tryGetObjectInfo();
31070
31123
  for (const dir of getWorkflowDirectories()) {
31071
31124
  try {
31072
31125
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -31075,11 +31128,17 @@ async function resolveWorkflowReference(args) {
31075
31128
  continue;
31076
31129
  }
31077
31130
  const fullPath = path.join(dir, entry.name);
31078
- const defaultId = getWorkflowId(fullPath);
31079
- if (requestedId === defaultId || requestedId === entry.name.replace(/\.json$/, "")) {
31131
+ const inspected = await inspectWorkflowFile(
31132
+ fullPath,
31133
+ objectInfoAvailability.objectInfo
31134
+ );
31135
+ if (!inspected) {
31136
+ continue;
31137
+ }
31138
+ if (requestedId === inspected.id || requestedId === inspected.name || requestedId === entry.name.replace(/\.json$/, "")) {
31080
31139
  return {
31081
31140
  targetPath: fullPath,
31082
- content: await fs.readFile(fullPath, "utf8")
31141
+ content: inspected.content
31083
31142
  };
31084
31143
  }
31085
31144
  }
@@ -31097,7 +31156,7 @@ async function resolveWorkflowReference(args) {
31097
31156
  "workflow_id"
31098
31157
  );
31099
31158
  }
31100
- async function normalizeWorkflowPrompt(workflow) {
31159
+ async function normalizeWorkflowPrompt(workflow, objectInfo) {
31101
31160
  if (!isRecord2(workflow)) {
31102
31161
  throw new WorkflowInputError(
31103
31162
  "Workflow must be a JSON object.",
@@ -31105,13 +31164,22 @@ async function normalizeWorkflowPrompt(workflow) {
31105
31164
  );
31106
31165
  }
31107
31166
  let sourceFormat = "api";
31167
+ let widgetPathMap = {};
31108
31168
  let normalizedWorkflow = workflow;
31109
31169
  if (isWebUIFormat(normalizedWorkflow)) {
31170
+ if (!objectInfo) {
31171
+ throw new WorkflowInputError(
31172
+ "Web UI workflow normalization requires ComfyUI node definitions. Start the ComfyUI server or save the workflow in API format first.",
31173
+ "workflow"
31174
+ );
31175
+ }
31110
31176
  sourceFormat = "webui";
31111
- normalizedWorkflow = convertWebUIToAPI(
31177
+ const converted = convertWebUIToAPIWithMetadata(
31112
31178
  normalizedWorkflow,
31113
- await getObjectInfo()
31179
+ objectInfo
31114
31180
  );
31181
+ normalizedWorkflow = converted.prompt;
31182
+ widgetPathMap = converted.widgetPathMap;
31115
31183
  }
31116
31184
  const promptCandidate = isRecord2(normalizedWorkflow.prompt) && normalizedWorkflow.prompt ? normalizedWorkflow.prompt : normalizedWorkflow;
31117
31185
  if (!isRecord2(promptCandidate)) {
@@ -31122,10 +31190,11 @@ async function normalizeWorkflowPrompt(workflow) {
31122
31190
  }
31123
31191
  return {
31124
31192
  prompt: extractRunnablePromptNodes(promptCandidate),
31125
- sourceFormat
31193
+ sourceFormat,
31194
+ widgetPathMap
31126
31195
  };
31127
31196
  }
31128
- async function loadWorkflow(args) {
31197
+ async function loadWorkflow(args, objectInfo) {
31129
31198
  const { targetPath, content } = await resolveWorkflowReference(args);
31130
31199
  let parsed;
31131
31200
  try {
@@ -31136,13 +31205,83 @@ async function loadWorkflow(args) {
31136
31205
  args.workflow_file_path ? "workflow_file_path" : "workflow_id"
31137
31206
  );
31138
31207
  }
31139
- const { prompt, sourceFormat } = await normalizeWorkflowPrompt(parsed);
31208
+ const { prompt, sourceFormat, widgetPathMap } = await normalizeWorkflowPrompt(parsed, objectInfo);
31140
31209
  return {
31141
31210
  targetPath,
31142
31211
  prompt,
31143
- sourceFormat
31212
+ sourceFormat,
31213
+ widgetPathMap
31144
31214
  };
31145
31215
  }
31216
+ async function inspectWorkflowFile(fullPath, objectInfo) {
31217
+ const content = await fs.readFile(fullPath, "utf8");
31218
+ const defaultName = path.basename(fullPath, ".json");
31219
+ const defaultId = getWorkflowId(fullPath);
31220
+ let parsed;
31221
+ try {
31222
+ parsed = JSON.parse(content);
31223
+ } catch {
31224
+ return void 0;
31225
+ }
31226
+ let name = defaultName;
31227
+ if (isRecord2(parsed)) {
31228
+ if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
31229
+ name = parsed.name;
31230
+ } else if (isRecord2(parsed.workflow) && typeof parsed.workflow.name === "string" && parsed.workflow.name.trim().length > 0) {
31231
+ name = parsed.workflow.name;
31232
+ }
31233
+ }
31234
+ if (isWebUIFormat(parsed)) {
31235
+ if (!objectInfo) {
31236
+ return {
31237
+ id: defaultId,
31238
+ name,
31239
+ path: fullPath,
31240
+ source_format: "webui",
31241
+ validation_status: "conversion_requires_node_schemas",
31242
+ content,
31243
+ isWebUI: true
31244
+ };
31245
+ }
31246
+ try {
31247
+ await normalizeWorkflowPrompt(parsed, objectInfo);
31248
+ return {
31249
+ id: defaultId,
31250
+ name,
31251
+ path: fullPath,
31252
+ source_format: "webui",
31253
+ validation_status: "conversion_validated",
31254
+ content,
31255
+ isWebUI: true
31256
+ };
31257
+ } catch (error48) {
31258
+ return {
31259
+ id: defaultId,
31260
+ name,
31261
+ path: fullPath,
31262
+ source_format: "webui",
31263
+ validation_status: "conversion_failed",
31264
+ content,
31265
+ error: summarizeErrorMessage(error48),
31266
+ isWebUI: true
31267
+ };
31268
+ }
31269
+ }
31270
+ try {
31271
+ await normalizeWorkflowPrompt(parsed);
31272
+ return {
31273
+ id: defaultId,
31274
+ name,
31275
+ path: fullPath,
31276
+ source_format: "api",
31277
+ validation_status: "ready",
31278
+ content,
31279
+ isWebUI: false
31280
+ };
31281
+ } catch {
31282
+ return void 0;
31283
+ }
31284
+ }
31146
31285
  function inferEditableRole(nodeId, classType, inputName, consumerMap) {
31147
31286
  if (classType === "CLIPTextEncode" && inputName === "text") {
31148
31287
  const consumers = consumerMap.get(nodeId) ?? [];
@@ -31154,6 +31293,28 @@ function inferEditableRole(nodeId, classType, inputName, consumerMap) {
31154
31293
  }
31155
31294
  return "prompt_text";
31156
31295
  }
31296
+ if (inputName === "value") {
31297
+ const consumers = consumerMap.get(nodeId) ?? [];
31298
+ const textConsumers = consumers.filter(
31299
+ (consumer) => consumer.classType === "CLIPTextEncode" && consumer.inputName === "text"
31300
+ );
31301
+ if (textConsumers.length > 0) {
31302
+ for (const consumer of textConsumers) {
31303
+ const clipConsumers = consumerMap.get(consumer.nodeId) ?? [];
31304
+ if (clipConsumers.some(
31305
+ (clipConsumer) => clipConsumer.inputName === "positive"
31306
+ )) {
31307
+ return "positive_prompt";
31308
+ }
31309
+ if (clipConsumers.some(
31310
+ (clipConsumer) => clipConsumer.inputName === "negative"
31311
+ )) {
31312
+ return "negative_prompt";
31313
+ }
31314
+ }
31315
+ return "prompt_text";
31316
+ }
31317
+ }
31157
31318
  switch (inputName) {
31158
31319
  case "seed":
31159
31320
  return "seed";
@@ -31197,7 +31358,428 @@ function formatValuePreview(value) {
31197
31358
  return String(value);
31198
31359
  }
31199
31360
  }
31200
- function analyzeEditableInputs(prompt) {
31361
+ function getInputDefinition(objectInfo, prompt, nodeId, inputName) {
31362
+ if (!objectInfo) {
31363
+ return void 0;
31364
+ }
31365
+ const node = prompt[nodeId];
31366
+ if (!node) {
31367
+ return void 0;
31368
+ }
31369
+ const nodeDefinition = objectInfo[node.class_type];
31370
+ if (!nodeDefinition) {
31371
+ return void 0;
31372
+ }
31373
+ return nodeDefinition.input?.required?.[inputName] ?? nodeDefinition.input?.optional?.[inputName];
31374
+ }
31375
+ function getExpectedTypeName(inputDefinition) {
31376
+ if (!inputDefinition) {
31377
+ return void 0;
31378
+ }
31379
+ const [typeData] = inputDefinition;
31380
+ return Array.isArray(typeData) ? "COMBO" : String(typeData).toUpperCase();
31381
+ }
31382
+ function getInputOptionsSummary(inputDefinition, previewLimit = 8) {
31383
+ if (!inputDefinition) {
31384
+ return void 0;
31385
+ }
31386
+ const [typeData] = inputDefinition;
31387
+ if (!Array.isArray(typeData)) {
31388
+ return void 0;
31389
+ }
31390
+ return {
31391
+ options_count: typeData.length,
31392
+ options_preview: typeData.slice(0, previewLimit),
31393
+ options_truncated: typeData.length > previewLimit
31394
+ };
31395
+ }
31396
+ function isPlainObject3(value) {
31397
+ return isRecord2(value) && !Array.isArray(value);
31398
+ }
31399
+ function getValueTypeName(value) {
31400
+ if (Array.isArray(value)) {
31401
+ return "array";
31402
+ }
31403
+ if (value === null) {
31404
+ return "null";
31405
+ }
31406
+ return typeof value;
31407
+ }
31408
+ function validateOverrideValueType(path2, value, inputDefinition) {
31409
+ if (!inputDefinition) {
31410
+ return;
31411
+ }
31412
+ const [typeData] = inputDefinition;
31413
+ if (Array.isArray(typeData)) {
31414
+ if (!typeData.some((option) => option === value)) {
31415
+ throw new WorkflowInputError(
31416
+ `Invalid override for '${path2}': expected one of ${typeData.map((option) => JSON.stringify(option)).join(", ")}, received ${JSON.stringify(value)}.`,
31417
+ "overrides"
31418
+ );
31419
+ }
31420
+ return;
31421
+ }
31422
+ const typeName = String(typeData).toUpperCase();
31423
+ switch (typeName) {
31424
+ case "INT":
31425
+ if (typeof value !== "number" || !Number.isInteger(value)) {
31426
+ throw new WorkflowInputError(
31427
+ `Invalid override for '${path2}': expected INT, received ${getValueTypeName(
31428
+ value
31429
+ )}.`,
31430
+ "overrides"
31431
+ );
31432
+ }
31433
+ return;
31434
+ case "FLOAT":
31435
+ if (typeof value !== "number") {
31436
+ throw new WorkflowInputError(
31437
+ `Invalid override for '${path2}': expected FLOAT, received ${getValueTypeName(
31438
+ value
31439
+ )}.`,
31440
+ "overrides"
31441
+ );
31442
+ }
31443
+ return;
31444
+ case "STRING":
31445
+ if (typeof value !== "string") {
31446
+ throw new WorkflowInputError(
31447
+ `Invalid override for '${path2}': expected STRING, received ${getValueTypeName(
31448
+ value
31449
+ )}.`,
31450
+ "overrides"
31451
+ );
31452
+ }
31453
+ return;
31454
+ case "BOOLEAN":
31455
+ if (typeof value !== "boolean") {
31456
+ throw new WorkflowInputError(
31457
+ `Invalid override for '${path2}': expected BOOLEAN, received ${getValueTypeName(
31458
+ value
31459
+ )}.`,
31460
+ "overrides"
31461
+ );
31462
+ }
31463
+ return;
31464
+ default:
31465
+ return;
31466
+ }
31467
+ }
31468
+ function getEditableAliases(canonicalPath, nodeId, inputName, role, widgetPathMap) {
31469
+ const aliases = /* @__PURE__ */ new Set([`${nodeId}.${inputName}`]);
31470
+ if (role) {
31471
+ aliases.add(`${nodeId}.${role}`);
31472
+ }
31473
+ for (const [widgetPath, resolvedPath] of Object.entries(widgetPathMap)) {
31474
+ if (resolvedPath === canonicalPath) {
31475
+ aliases.add(widgetPath);
31476
+ }
31477
+ }
31478
+ aliases.delete(canonicalPath);
31479
+ return [...aliases];
31480
+ }
31481
+ function buildEditableInputIndex(editableInputs) {
31482
+ const entriesByPath = /* @__PURE__ */ new Map();
31483
+ const aliasToPath = /* @__PURE__ */ new Map();
31484
+ const ambiguousAliases = /* @__PURE__ */ new Map();
31485
+ const pathsByNode = /* @__PURE__ */ new Map();
31486
+ for (const entry of editableInputs) {
31487
+ entriesByPath.set(entry.path, entry);
31488
+ const nodeEntries = pathsByNode.get(entry.node_id) ?? [];
31489
+ nodeEntries.push(entry);
31490
+ pathsByNode.set(entry.node_id, nodeEntries);
31491
+ for (const alias of [entry.path, ...entry.aliases]) {
31492
+ const existing = aliasToPath.get(alias);
31493
+ if (!existing) {
31494
+ aliasToPath.set(alias, entry.path);
31495
+ continue;
31496
+ }
31497
+ if (existing === entry.path) {
31498
+ continue;
31499
+ }
31500
+ const ambiguous = new Set(
31501
+ ambiguousAliases.get(alias) ?? [existing]
31502
+ );
31503
+ ambiguous.add(entry.path);
31504
+ ambiguousAliases.set(alias, [...ambiguous].sort());
31505
+ aliasToPath.delete(alias);
31506
+ }
31507
+ }
31508
+ return {
31509
+ entriesByPath,
31510
+ aliasToPath,
31511
+ ambiguousAliases,
31512
+ pathsByNode
31513
+ };
31514
+ }
31515
+ function getPathSegmentHelp(prompt, path2) {
31516
+ const parts = path2.split(".");
31517
+ if (parts.length === 0 || parts[0].length === 0) {
31518
+ return void 0;
31519
+ }
31520
+ const nodeId = parts[0];
31521
+ return {
31522
+ nodeId,
31523
+ nodeExists: isRecord2(prompt[nodeId])
31524
+ };
31525
+ }
31526
+ function describeAvailableNodePaths(editableIndex, nodeId) {
31527
+ if (!editableIndex) {
31528
+ return "";
31529
+ }
31530
+ const entries = editableIndex.pathsByNode.get(nodeId) ?? [];
31531
+ if (entries.length === 0) {
31532
+ return "";
31533
+ }
31534
+ return entries.map((entry) => {
31535
+ const aliasSuffix = entry.aliases.length > 0 ? ` (aliases: ${entry.aliases.join(", ")})` : "";
31536
+ return `${entry.path}${aliasSuffix}`;
31537
+ }).join("\n");
31538
+ }
31539
+ function getPreferredOverridePath(entry) {
31540
+ const roleAlias = entry.role ? `${entry.node_id}.${entry.role}` : void 0;
31541
+ if (roleAlias && entry.aliases.includes(roleAlias)) {
31542
+ return roleAlias;
31543
+ }
31544
+ const inputAlias = `${entry.node_id}.${entry.input_name}`;
31545
+ if (entry.aliases.includes(inputAlias)) {
31546
+ return inputAlias;
31547
+ }
31548
+ const nonWidgetAlias = entry.aliases.find(
31549
+ (alias) => !alias.includes(".widgets[")
31550
+ );
31551
+ return nonWidgetAlias ?? entry.path;
31552
+ }
31553
+ function formatEditableInputOptionsText(entry) {
31554
+ if (entry.options_count === void 0 || entry.options_preview === void 0 || entry.options_preview.length === 0) {
31555
+ return "";
31556
+ }
31557
+ const preview = entry.options_preview.map((option) => formatValuePreview(option)).join(", ");
31558
+ const truncatedSuffix = entry.options_truncated ? ", ..." : "";
31559
+ return ` | options: ${preview}${truncatedSuffix} (${entry.options_count} total)`;
31560
+ }
31561
+ function getEditableInputGroup(entry) {
31562
+ if (entry.role === "positive_prompt" || entry.role === "negative_prompt" || entry.role === "prompt_text") {
31563
+ return "prompts";
31564
+ }
31565
+ if (entry.role === "checkpoint" || entry.input_name === "ckpt_name" || entry.input_name === "model_name" || entry.input_name === "vae_name" || entry.class_type.includes("Checkpoint") || entry.class_type.includes("VAE")) {
31566
+ return "models";
31567
+ }
31568
+ if (entry.role === "seed" || entry.role === "steps" || entry.role === "cfg" || entry.role === "sampler" || entry.role === "scheduler" || entry.role === "denoise" || entry.class_type.includes("Sampler")) {
31569
+ return "sampling";
31570
+ }
31571
+ if (entry.role === "width" || entry.role === "height" || entry.role === "batch_size") {
31572
+ return "image";
31573
+ }
31574
+ if (entry.input_name === "lora_name" || entry.input_name.startsWith("strength_") || entry.class_type.includes("Lora")) {
31575
+ return "loras";
31576
+ }
31577
+ if (entry.class_type.includes("ControlNet") || entry.class_type.includes("IPAdapter") || entry.class_type.includes("FreeU") || entry.class_type.includes("FaceDetailer")) {
31578
+ return "control";
31579
+ }
31580
+ if (entry.role === "filename_prefix" || entry.input_name === "filename" || entry.input_name === "path" || entry.input_name === "extension" || entry.class_type.includes("Save") || entry.class_type.includes("PromptSaver")) {
31581
+ return "output";
31582
+ }
31583
+ return "other";
31584
+ }
31585
+ function getEditableInputPriority(entry) {
31586
+ const rolePriority = {
31587
+ positive_prompt: 300,
31588
+ negative_prompt: 295,
31589
+ prompt_text: 290,
31590
+ checkpoint: 260,
31591
+ seed: 250,
31592
+ steps: 240,
31593
+ cfg: 235,
31594
+ sampler: 230,
31595
+ scheduler: 225,
31596
+ width: 220,
31597
+ height: 219,
31598
+ batch_size: 218,
31599
+ denoise: 215,
31600
+ filename_prefix: 190,
31601
+ text: 180
31602
+ };
31603
+ let priority = rolePriority[entry.role ?? ""] ?? 0;
31604
+ if (entry.input_name === "lora_name") {
31605
+ priority = Math.max(priority, 170);
31606
+ }
31607
+ if (entry.input_name.startsWith("strength_")) {
31608
+ priority = Math.max(priority, 160);
31609
+ }
31610
+ if (entry.class_type.includes("PromptSaver") || entry.class_type === "SaveImage") {
31611
+ priority -= 70;
31612
+ }
31613
+ if (entry.class_type === "PrimitiveStringMultiline" && (entry.role === "positive_prompt" || entry.role === "negative_prompt")) {
31614
+ priority += 30;
31615
+ }
31616
+ return priority;
31617
+ }
31618
+ function getHighSignalEditableInputs(editableInputs, limit = 12) {
31619
+ const seen = /* @__PURE__ */ new Set();
31620
+ const prioritized = [...editableInputs].sort((left, right) => {
31621
+ const priorityDiff = getEditableInputPriority(right) - getEditableInputPriority(left);
31622
+ if (priorityDiff !== 0) {
31623
+ return priorityDiff;
31624
+ }
31625
+ return getPreferredOverridePath(left).localeCompare(
31626
+ getPreferredOverridePath(right),
31627
+ void 0,
31628
+ { numeric: true }
31629
+ );
31630
+ }).filter((entry) => {
31631
+ const preferredPath = getPreferredOverridePath(entry);
31632
+ if (seen.has(preferredPath)) {
31633
+ return false;
31634
+ }
31635
+ seen.add(preferredPath);
31636
+ return true;
31637
+ });
31638
+ return prioritized.slice(0, limit);
31639
+ }
31640
+ function summarizeEditableInputGroups(editableInputs) {
31641
+ const labels = {
31642
+ prompts: "Prompts",
31643
+ models: "Models and checkpoints",
31644
+ sampling: "Sampling",
31645
+ image: "Image size and batching",
31646
+ loras: "LoRAs and strengths",
31647
+ control: "Conditioning and adapters",
31648
+ output: "Output and persistence",
31649
+ other: "Other"
31650
+ };
31651
+ const summaries = /* @__PURE__ */ new Map();
31652
+ for (const entry of editableInputs) {
31653
+ const group = getEditableInputGroup(entry);
31654
+ const existing = summaries.get(group) ?? {
31655
+ group,
31656
+ label: labels[group],
31657
+ count: 0,
31658
+ sample_paths: []
31659
+ };
31660
+ existing.count += 1;
31661
+ if (existing.sample_paths.length < 3) {
31662
+ existing.sample_paths.push(getPreferredOverridePath(entry));
31663
+ }
31664
+ summaries.set(group, existing);
31665
+ }
31666
+ const order = [
31667
+ "prompts",
31668
+ "models",
31669
+ "sampling",
31670
+ "image",
31671
+ "loras",
31672
+ "control",
31673
+ "output",
31674
+ "other"
31675
+ ];
31676
+ return order.map((group) => summaries.get(group)).filter(
31677
+ (summary) => Boolean(summary)
31678
+ );
31679
+ }
31680
+ function formatEditableInputGroupsText(groupSummaries) {
31681
+ if (groupSummaries.length === 0) {
31682
+ return "No editable input groups were detected.";
31683
+ }
31684
+ return formatListItems(groupSummaries, (summary) => {
31685
+ const examples = summary.sample_paths.length > 0 ? ` | examples: ${summary.sample_paths.join(", ")}` : "";
31686
+ return `${summary.label}: ${summary.count}${examples}`;
31687
+ });
31688
+ }
31689
+ function buildOverrideExamples(editableInputs, limit = 5) {
31690
+ const examples = {};
31691
+ const preferredPaths = [];
31692
+ for (const entry of getHighSignalEditableInputs(editableInputs, limit)) {
31693
+ const preferredPath = getPreferredOverridePath(entry);
31694
+ examples[preferredPath] = entry.value;
31695
+ preferredPaths.push(preferredPath);
31696
+ }
31697
+ return {
31698
+ examples,
31699
+ preferred_paths: preferredPaths
31700
+ };
31701
+ }
31702
+ function formatOverrideExamplesText(examples) {
31703
+ return JSON.stringify(examples, null, 2);
31704
+ }
31705
+ function resolveOverridePath(prompt, rawPath, editableIndex) {
31706
+ if (!editableIndex) {
31707
+ return rawPath;
31708
+ }
31709
+ if (editableIndex.entriesByPath.has(rawPath)) {
31710
+ return rawPath;
31711
+ }
31712
+ const ambiguous = editableIndex.ambiguousAliases.get(rawPath);
31713
+ if (ambiguous) {
31714
+ throw new WorkflowInputError(
31715
+ `Ambiguous override path '${rawPath}'. It could refer to: ${ambiguous.join(
31716
+ ", "
31717
+ )}. Use a full '<node>.inputs.<field>' path instead.`,
31718
+ "overrides"
31719
+ );
31720
+ }
31721
+ const resolved = editableIndex.aliasToPath.get(rawPath);
31722
+ if (resolved) {
31723
+ return resolved;
31724
+ }
31725
+ const rawParts = rawPath.split(".");
31726
+ if (rawParts.length >= 3 && rawParts[1] === INPUT_SEGMENT) {
31727
+ const pathHelp2 = getPathSegmentHelp(prompt, rawPath);
31728
+ if (pathHelp2?.nodeId && pathHelp2.nodeExists) {
31729
+ const validPaths = describeAvailableNodePaths(
31730
+ editableIndex,
31731
+ pathHelp2.nodeId
31732
+ );
31733
+ if (validPaths.length > 0) {
31734
+ throw new WorkflowInputError(
31735
+ `Unsupported override path '${rawPath}'. Only detected editable inputs can be overridden. Valid editable paths for node ${pathHelp2.nodeId}:
31736
+ ${validPaths}`,
31737
+ "overrides"
31738
+ );
31739
+ }
31740
+ }
31741
+ }
31742
+ const pathHelp = getPathSegmentHelp(prompt, rawPath);
31743
+ if (pathHelp?.nodeId && pathHelp.nodeExists) {
31744
+ const validPaths = describeAvailableNodePaths(
31745
+ editableIndex,
31746
+ pathHelp.nodeId
31747
+ );
31748
+ if (validPaths.length > 0) {
31749
+ throw new WorkflowInputError(
31750
+ `Unknown override path '${rawPath}'. Valid editable paths for node ${pathHelp.nodeId}:
31751
+ ${validPaths}`,
31752
+ "overrides"
31753
+ );
31754
+ }
31755
+ }
31756
+ return rawPath;
31757
+ }
31758
+ function parseOverrides(rawOverrides) {
31759
+ if (rawOverrides === void 0) {
31760
+ return void 0;
31761
+ }
31762
+ if (typeof rawOverrides !== "string") {
31763
+ return rawOverrides;
31764
+ }
31765
+ let parsed;
31766
+ try {
31767
+ parsed = JSON.parse(rawOverrides);
31768
+ } catch (error48) {
31769
+ throw new WorkflowInputError(
31770
+ `Overrides must be a valid JSON object string: ${error48 instanceof Error ? error48.message : String(error48)}`,
31771
+ "overrides"
31772
+ );
31773
+ }
31774
+ if (!isPlainObject3(parsed)) {
31775
+ throw new WorkflowInputError(
31776
+ "Overrides JSON string must decode to an object.",
31777
+ "overrides"
31778
+ );
31779
+ }
31780
+ return parsed;
31781
+ }
31782
+ function analyzeEditableInputs(prompt, objectInfo, widgetPathMap = {}) {
31201
31783
  const consumerMap = /* @__PURE__ */ new Map();
31202
31784
  for (const [nodeId, node] of Object.entries(prompt)) {
31203
31785
  for (const [inputName, value] of Object.entries(node.inputs)) {
@@ -31232,23 +31814,186 @@ function analyzeEditableInputs(prompt) {
31232
31814
  node.class_type,
31233
31815
  inputName,
31234
31816
  consumerMap
31817
+ ),
31818
+ aliases: [],
31819
+ expected_type: getExpectedTypeName(
31820
+ getInputDefinition(objectInfo, prompt, nodeId, inputName)
31821
+ ),
31822
+ ...getInputOptionsSummary(
31823
+ getInputDefinition(objectInfo, prompt, nodeId, inputName)
31235
31824
  )
31236
31825
  });
31237
31826
  }
31238
31827
  }
31239
- return editableInputs.sort(
31828
+ return editableInputs.map((entry) => ({
31829
+ ...entry,
31830
+ aliases: getEditableAliases(
31831
+ entry.path,
31832
+ entry.node_id,
31833
+ entry.input_name,
31834
+ entry.role,
31835
+ widgetPathMap
31836
+ )
31837
+ })).sort(
31240
31838
  (left, right) => left.path.localeCompare(right.path, void 0, { numeric: true })
31241
31839
  );
31242
31840
  }
31243
- function formatEditableInputsText(editableInputs) {
31841
+ function formatEditableInputsText(editableInputs, options = {}) {
31244
31842
  if (editableInputs.length === 0) {
31245
31843
  return "No literal editable inputs were detected.";
31246
31844
  }
31247
31845
  return formatListItems(editableInputs, (entry) => {
31248
31846
  const role = entry.role ? ` [${entry.role}]` : "";
31249
31847
  const classType = entry.class_type ? ` (${entry.class_type})` : "";
31250
- return `${entry.path}${role}${classType}: ${entry.value_preview}`;
31848
+ const expectedType = entry.expected_type ? ` <${entry.expected_type}>` : "";
31849
+ const aliasSuffix = entry.aliases.length > 0 ? ` | aliases: ${entry.aliases.join(", ")}` : "";
31850
+ const optionsSuffix = options.includeOptions ? formatEditableInputOptionsText(entry) : "";
31851
+ return `${entry.path}${role}${classType}${expectedType}: ${entry.value_preview}${aliasSuffix}${optionsSuffix}`;
31852
+ });
31853
+ }
31854
+ function validatePromptPreflight(prompt, objectInfo, widgetPathMap = {}) {
31855
+ if (!objectInfo) {
31856
+ return {
31857
+ checked_inputs: 0,
31858
+ combo_inputs: 0
31859
+ };
31860
+ }
31861
+ const editableInputs = analyzeEditableInputs(
31862
+ prompt,
31863
+ objectInfo,
31864
+ widgetPathMap
31865
+ );
31866
+ const issues = [];
31867
+ let checkedInputs = 0;
31868
+ let comboInputs = 0;
31869
+ for (const entry of editableInputs) {
31870
+ const inputDefinition = getInputDefinition(
31871
+ objectInfo,
31872
+ prompt,
31873
+ entry.node_id,
31874
+ entry.input_name
31875
+ );
31876
+ if (!inputDefinition) {
31877
+ continue;
31878
+ }
31879
+ checkedInputs += 1;
31880
+ if (Array.isArray(inputDefinition[0])) {
31881
+ comboInputs += 1;
31882
+ }
31883
+ try {
31884
+ validateOverrideValueType(entry.path, entry.value, inputDefinition);
31885
+ } catch (error48) {
31886
+ if (!(error48 instanceof WorkflowInputError)) {
31887
+ throw error48;
31888
+ }
31889
+ const preferredPath = getPreferredOverridePath(entry);
31890
+ const comboHint = formatEditableInputOptionsText(entry);
31891
+ const issueMessage = entry.expected_type === "COMBO" && entry.options_count !== void 0 ? `${formatValuePreview(entry.value)} is not in the server-reported options for ${preferredPath}.` : error48.message;
31892
+ issues.push(`- ${preferredPath}: ${issueMessage}${comboHint}`);
31893
+ }
31894
+ }
31895
+ if (issues.length > 0) {
31896
+ throw new WorkflowInputError(
31897
+ `Workflow preflight failed before submission.
31898
+ ${issues.join("\n")}`,
31899
+ "workflow"
31900
+ );
31901
+ }
31902
+ return {
31903
+ checked_inputs: checkedInputs,
31904
+ combo_inputs: comboInputs
31905
+ };
31906
+ }
31907
+ function formatInputDefinitionSummary(inputName, inputDefinition) {
31908
+ const [typeData, extraInfo] = inputDefinition;
31909
+ const typeLabel = Array.isArray(typeData) ? `COMBO (${typeData.length} options)` : String(typeData).toUpperCase();
31910
+ const details = [];
31911
+ if (Array.isArray(typeData)) {
31912
+ details.push(
31913
+ `options: ${typeData.slice(0, 5).map((option) => JSON.stringify(option)).join(", ")}${typeData.length > 5 ? ", ..." : ""}`
31914
+ );
31915
+ }
31916
+ if (isRecord2(extraInfo)) {
31917
+ if (extraInfo.default !== void 0) {
31918
+ details.push(`default: ${formatValuePreview(extraInfo.default)}`);
31919
+ }
31920
+ if (typeof extraInfo.min === "number") {
31921
+ details.push(`min: ${extraInfo.min}`);
31922
+ }
31923
+ if (typeof extraInfo.max === "number") {
31924
+ details.push(`max: ${extraInfo.max}`);
31925
+ }
31926
+ }
31927
+ return `${inputName} <${typeLabel}>${details.length > 0 ? ` | ${details.join(" | ")}` : ""}`;
31928
+ }
31929
+ function formatNodeDefinitionText(nodeClass, node) {
31930
+ const requiredInputs = Object.entries(node.input.required ?? {});
31931
+ const optionalInputs = Object.entries(node.input.optional ?? {});
31932
+ const lines = [
31933
+ `Node: ${nodeClass}`,
31934
+ `Display name: ${node.display_name || node.name || nodeClass}`,
31935
+ `Category: ${node.category || "uncategorized"}`
31936
+ ];
31937
+ if (typeof node.description === "string" && node.description.trim().length > 0) {
31938
+ lines.push(`Description: ${node.description.trim()}`);
31939
+ }
31940
+ lines.push(
31941
+ `Required inputs (${requiredInputs.length}): ${requiredInputs.length > 0 ? requiredInputs.map(
31942
+ ([inputName, inputDefinition]) => formatInputDefinitionSummary(
31943
+ inputName,
31944
+ inputDefinition
31945
+ )
31946
+ ).join("\n") : "None."}`
31947
+ );
31948
+ lines.push(
31949
+ `Optional inputs (${optionalInputs.length}): ${optionalInputs.length > 0 ? optionalInputs.map(
31950
+ ([inputName, inputDefinition]) => formatInputDefinitionSummary(
31951
+ inputName,
31952
+ inputDefinition
31953
+ )
31954
+ ).join("\n") : "None."}`
31955
+ );
31956
+ lines.push(`Outputs: ${node.output.join(", ") || "None."}`);
31957
+ return lines.join("\n");
31958
+ }
31959
+ function formatNodeListText(classes, rawInfo, pagination) {
31960
+ const text = `Available nodes (${pagination.total} total):
31961
+ ` + formatListItems(classes, (nodeClass) => {
31962
+ const node = rawInfo[nodeClass];
31963
+ const displayName = node.display_name && node.display_name !== nodeClass ? ` \u2014 ${node.display_name}` : "";
31964
+ const category = node.category ? ` | category: ${node.category}` : "";
31965
+ const description = typeof node.description === "string" && node.description.trim().length > 0 ? ` | ${node.description.trim()}` : "";
31966
+ return `${nodeClass}${displayName}${category}${description}`;
31251
31967
  });
31968
+ if (!pagination.hasMore) {
31969
+ return text;
31970
+ }
31971
+ return `${text}
31972
+
31973
+ ${formatPaginationFooter(
31974
+ pagination.offset,
31975
+ pagination.limit,
31976
+ pagination.total
31977
+ )}`;
31978
+ }
31979
+ async function getQueueSnapshot() {
31980
+ try {
31981
+ return IS_MOCK ? MOCK_FIXTURES.queue : await fetchComfyJson("/queue");
31982
+ } catch {
31983
+ return void 0;
31984
+ }
31985
+ }
31986
+ function formatQueueSnapshotText(queueSnapshot) {
31987
+ if (!queueSnapshot) {
31988
+ return "";
31989
+ }
31990
+ return `Queue snapshot: ${queueSnapshot.queue_running.length} running, ${queueSnapshot.queue_pending.length} pending.`;
31991
+ }
31992
+ function getPromptQueueVisibility(promptId, queueSnapshot) {
31993
+ if (!queueSnapshot) {
31994
+ return null;
31995
+ }
31996
+ return JSON.stringify(queueSnapshot).includes(promptId);
31252
31997
  }
31253
31998
  function deriveSavedWorkflowFileName(sourcePath, requestedName) {
31254
31999
  if (requestedName) {
@@ -31259,21 +32004,39 @@ function deriveSavedWorkflowFileName(sourcePath, requestedName) {
31259
32004
  }
31260
32005
  return `workflow-${Date.now()}.json`;
31261
32006
  }
31262
- function applyOverrides(prompt, overrides) {
31263
- for (const [key, value] of Object.entries(overrides)) {
32007
+ function applyOverrides(prompt, overrides, options = {}) {
32008
+ for (const [rawKey, value] of Object.entries(overrides)) {
32009
+ const key = resolveOverridePath(prompt, rawKey, options.editableIndex);
31264
32010
  const parts = key.split(".");
31265
32011
  let current = prompt;
31266
32012
  for (let i = 0; i < parts.length - 1; i++) {
31267
32013
  const part = parts[i];
31268
32014
  const next = current[part];
31269
32015
  if (!isRecord2(next)) {
32016
+ const pathHelp = getPathSegmentHelp(prompt, key);
32017
+ const validPaths = pathHelp?.nodeId && pathHelp.nodeExists ? describeAvailableNodePaths(
32018
+ options.editableIndex,
32019
+ pathHelp.nodeId
32020
+ ) : "";
32021
+ const validPathSuffix = validPaths.length > 0 ? `
32022
+ Valid editable paths for node ${pathHelp?.nodeId}:
32023
+ ${validPaths}` : "";
31270
32024
  throw new WorkflowInputError(
31271
- `Invalid override path: '${key}'. The segment '${part}' does not exist or is not an object. Please check the editable inputs using 'comfyui_get_workflow' to find the correct path.`,
32025
+ `Invalid override path: '${rawKey}'. The resolved path '${key}' failed at segment '${part}'.${validPathSuffix}`,
31272
32026
  "overrides"
31273
32027
  );
31274
32028
  }
31275
32029
  current = next;
31276
32030
  }
32031
+ if (parts.length === 3 && parts[1] === INPUT_SEGMENT && typeof parts[0] === "string" && typeof parts[2] === "string") {
32032
+ const inputDefinition = getInputDefinition(
32033
+ options.objectInfo,
32034
+ prompt,
32035
+ parts[0],
32036
+ parts[2]
32037
+ );
32038
+ validateOverrideValueType(key, value, inputDefinition);
32039
+ }
31277
32040
  current[parts[parts.length - 1]] = value;
31278
32041
  }
31279
32042
  }
@@ -31289,13 +32052,12 @@ var DiscoverWorkflowsSchema = paginationSchema.extend({
31289
32052
  directory_path: z3.string().optional().describe("Custom directory path to scan for workflows.")
31290
32053
  });
31291
32054
  var WorkflowRunSchema = z3.object({
32055
+ workflow: z3.unknown().optional().describe("Workflow JSON object to execute immediately."),
31292
32056
  workflow_file_path: z3.string().optional().describe(
31293
32057
  "Path or filename of workflow JSON. Scans default dirs if simple name provided."
31294
32058
  ),
31295
32059
  workflow_id: z3.string().optional().describe("Workflow identifier returned by discover_workflows."),
31296
- overrides: z3.record(z3.string(), z3.unknown()).optional().describe(
31297
- 'Key-value pairs to override using dot-notation. e.g. {"6.inputs.text": "dragon"}'
31298
- ),
32060
+ overrides: OverridesSchema.optional(),
31299
32061
  await: z3.boolean().default(true).describe("Wait for completion and return native images."),
31300
32062
  timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
31301
32063
  });
@@ -31308,7 +32070,7 @@ var SaveWorkflowSchema = z3.object({
31308
32070
  workflow: z3.unknown().optional().describe("Workflow JSON object to save locally."),
31309
32071
  workflow_file_path: z3.string().optional().describe("Existing workflow path or filename to save."),
31310
32072
  workflow_id: z3.string().optional().describe("Existing workflow identifier to save."),
31311
- overrides: z3.record(z3.string(), z3.unknown()).optional().describe("Dot-notation overrides to persist before saving."),
32073
+ overrides: OverridesSchema.optional(),
31312
32074
  output_file_name: z3.string().optional().describe("Filename to write locally. (default: anchored to ./)"),
31313
32075
  overwrite: z3.boolean().default(false).describe("Replace an existing local file when true."),
31314
32076
  include_prompt: z3.boolean().default(false).describe("Include saved API JSON in structuredContent.")
@@ -31329,7 +32091,10 @@ async function handleInspectNode(args) {
31329
32091
  }
31330
32092
  return {
31331
32093
  content: [
31332
- { type: "text", text: JSON.stringify(node, null, 2) }
32094
+ {
32095
+ type: "text",
32096
+ text: formatNodeDefinitionText(args.node_class, node)
32097
+ }
31333
32098
  ],
31334
32099
  structuredContent: { ...node }
31335
32100
  };
@@ -31348,16 +32113,18 @@ async function handleInspectNode(args) {
31348
32113
  for (const c of paginated.items) {
31349
32114
  resultNodes[c] = rawInfo[c];
31350
32115
  }
31351
- const text = `Available nodes (${paginated.total} total):
31352
- ` + paginated.items.join(", ") + (paginated.hasMore ? `
31353
-
31354
- ${formatPaginationFooter(
31355
- paginated.offset,
31356
- paginated.limit,
31357
- paginated.total
31358
- )}` : "");
31359
32116
  return {
31360
- content: [{ type: "text", text }],
32117
+ content: [
32118
+ {
32119
+ type: "text",
32120
+ text: formatNodeListText(paginated.items, rawInfo, {
32121
+ total: paginated.total,
32122
+ offset: paginated.offset,
32123
+ limit: paginated.limit,
32124
+ hasMore: paginated.hasMore
32125
+ })
32126
+ }
32127
+ ],
31361
32128
  structuredContent: {
31362
32129
  nodes: resultNodes,
31363
32130
  pagination: {
@@ -31377,42 +32144,27 @@ function getWorkflowId(absolutePath) {
31377
32144
  }
31378
32145
  async function handleDiscoverWorkflows(args) {
31379
32146
  const pathsToScan = getWorkflowDirectories(args.directory_path);
32147
+ const objectInfoAvailability = await tryGetObjectInfo();
31380
32148
  const foundWorkflows = [];
32149
+ let skippedNonWorkflowFiles = 0;
31381
32150
  for (const dir of pathsToScan) {
31382
32151
  try {
31383
32152
  const entries = await fs.readdir(dir, { withFileTypes: true });
31384
32153
  for (const entry of entries) {
31385
32154
  if (entry.isFile() && entry.name.endsWith(".json")) {
31386
32155
  const fullPath = path.join(dir, entry.name);
31387
- const defaultId = getWorkflowId(fullPath);
31388
- const defaultName = entry.name.replace(/\.json$/, "");
31389
32156
  try {
31390
- const content = await fs.readFile(fullPath, "utf8");
31391
- const json2 = JSON.parse(content);
31392
- const id = defaultId;
31393
- let name = defaultName;
31394
- let isWebUI = false;
31395
- if (typeof json2 === "object" && json2 !== null) {
31396
- if (Array.isArray(json2.nodes) && Array.isArray(json2.links)) {
31397
- isWebUI = true;
31398
- }
31399
- if (typeof json2.name === "string") name = json2.name;
31400
- else if (json2.workflow && typeof json2.workflow.name === "string")
31401
- name = json2.workflow.name;
32157
+ const inspected = await inspectWorkflowFile(
32158
+ fullPath,
32159
+ objectInfoAvailability.objectInfo
32160
+ );
32161
+ if (inspected) {
32162
+ foundWorkflows.push(inspected);
32163
+ } else {
32164
+ skippedNonWorkflowFiles += 1;
31402
32165
  }
31403
- foundWorkflows.push({
31404
- id,
31405
- name,
31406
- path: fullPath,
31407
- isWebUI
31408
- });
31409
- } catch (e) {
31410
- foundWorkflows.push({
31411
- id: defaultId,
31412
- name: defaultName,
31413
- path: fullPath,
31414
- error: String(e)
31415
- });
32166
+ } catch {
32167
+ skippedNonWorkflowFiles += 1;
31416
32168
  }
31417
32169
  }
31418
32170
  }
@@ -31420,14 +32172,14 @@ async function handleDiscoverWorkflows(args) {
31420
32172
  }
31421
32173
  }
31422
32174
  if (foundWorkflows.length === 0) {
31423
- const text2 = `No workflows found in searched directories.
32175
+ const text = `No workflows found in searched directories.
31424
32176
 
31425
32177
  Searched paths:
31426
32178
  ${pathsToScan.map((p) => `- ${p}`).join("\n")}
31427
32179
 
31428
32180
  Hint: If your workflows are stored elsewhere, provide a custom path using the 'directory_path' parameter.`;
31429
32181
  return {
31430
- content: [{ type: "text", text: text2 }],
32182
+ content: [{ type: "text", text }],
31431
32183
  structuredContent: {
31432
32184
  workflows: [],
31433
32185
  searched_paths: pathsToScan
@@ -31435,23 +32187,47 @@ Hint: If your workflows are stored elsewhere, provide a custom path using the 'd
31435
32187
  };
31436
32188
  }
31437
32189
  const paginated = applyPagination(foundWorkflows, args);
31438
- const text = `Found ${paginated.total} workflow files:
31439
- ` + formatListItems(paginated.items, (w) => {
31440
- const formatTag = w.isWebUI ? "\u{1F504} Web UI (Auto-converted on run)" : "\u2705 API Format";
31441
- let line = `[ID: ${w.id}] [${formatTag}] Name: "${w.name}"`;
31442
- if (w.error) line += ` (Error: ${w.error})`;
31443
- return line;
31444
- }) + (paginated.hasMore ? `
31445
-
31446
- ${formatPaginationFooter(
31447
- paginated.offset,
31448
- paginated.limit,
31449
- paginated.total
31450
- )}` : "");
32190
+ const textSections = [
32191
+ `Found ${paginated.total} workflow files:`,
32192
+ formatListItems(paginated.items, (workflow) => {
32193
+ const formatTag = workflow.source_format === "webui" ? workflow.validation_status === "conversion_validated" ? "\u26A0\uFE0F Web UI graph (conversion validated)" : workflow.validation_status === "conversion_requires_node_schemas" ? "\u26A0\uFE0F Web UI graph (needs node schemas to validate)" : "\u274C Web UI graph (conversion failed)" : "\u2705 API workflow";
32194
+ let line = `[ID: ${workflow.id}] [${formatTag}] Name: "${workflow.name}"`;
32195
+ if (workflow.error) {
32196
+ line += ` (Validation error: ${workflow.error})`;
32197
+ }
32198
+ return line;
32199
+ })
32200
+ ];
32201
+ if (skippedNonWorkflowFiles > 0) {
32202
+ textSections.push(
32203
+ `Skipped ${skippedNonWorkflowFiles} non-workflow JSON file(s).`
32204
+ );
32205
+ }
32206
+ if (objectInfoAvailability.warning) {
32207
+ textSections.push(objectInfoAvailability.warning);
32208
+ }
32209
+ if (paginated.hasMore) {
32210
+ textSections.push(
32211
+ formatPaginationFooter(
32212
+ paginated.offset,
32213
+ paginated.limit,
32214
+ paginated.total
32215
+ )
32216
+ );
32217
+ }
31451
32218
  return {
31452
- content: [{ type: "text", text }],
32219
+ content: [{ type: "text", text: textSections.join("\n\n") }],
31453
32220
  structuredContent: {
31454
- workflows: paginated.items,
32221
+ workflows: paginated.items.map((workflow) => ({
32222
+ id: workflow.id,
32223
+ name: workflow.name,
32224
+ path: workflow.path,
32225
+ isWebUI: workflow.isWebUI,
32226
+ source_format: workflow.source_format,
32227
+ validation_status: workflow.validation_status,
32228
+ ...workflow.error ? { error: workflow.error } : {}
32229
+ })),
32230
+ skipped_non_workflow_files: skippedNonWorkflowFiles,
31455
32231
  pagination: {
31456
32232
  total: paginated.total,
31457
32233
  offset: paginated.offset,
@@ -31480,7 +32256,8 @@ async function waitForWorkflowResult(prompt_id, timeout_seconds) {
31480
32256
  }
31481
32257
  await new Promise((r) => setTimeout(r, 1e3));
31482
32258
  }
31483
- throw new Error(`Timeout waiting for workflow ${prompt_id}`);
32259
+ const queueSnapshot = await getQueueSnapshot();
32260
+ throw new WorkflowTimeoutError(prompt_id, timeout_seconds, queueSnapshot);
31484
32261
  }
31485
32262
  async function handleWaitForWorkflow(args) {
31486
32263
  try {
@@ -31552,21 +32329,53 @@ async function handleWaitForWorkflow(args) {
31552
32329
  }
31553
32330
  async function handleGetWorkflow(args) {
31554
32331
  try {
31555
- const loadedWorkflow = await loadWorkflow(args);
31556
- const editableInputs = analyzeEditableInputs(loadedWorkflow.prompt);
32332
+ const objectInfoAvailability = await tryGetObjectInfo();
32333
+ const loadedWorkflow = await loadWorkflow(
32334
+ args,
32335
+ objectInfoAvailability.objectInfo
32336
+ );
32337
+ const editableInputs = analyzeEditableInputs(
32338
+ loadedWorkflow.prompt,
32339
+ objectInfoAvailability.objectInfo,
32340
+ loadedWorkflow.widgetPathMap
32341
+ );
32342
+ const highSignalInputs = getHighSignalEditableInputs(editableInputs);
32343
+ const editableInputGroups = summarizeEditableInputGroups(editableInputs);
32344
+ const overrideExamples = buildOverrideExamples(editableInputs);
31557
32345
  const workflowId = getWorkflowId(loadedWorkflow.targetPath);
31558
- const text = [
32346
+ const textSections = [
31559
32347
  `Workflow ${workflowId} loaded from ${loadedWorkflow.targetPath}.`,
31560
- loadedWorkflow.sourceFormat === "webui" ? "Source format: Web UI (normalized to API format)." : "Source format: API format.",
31561
- `Editable inputs (${editableInputs.length}):`,
31562
- formatEditableInputsText(editableInputs)
31563
- ].join("\n");
32348
+ loadedWorkflow.sourceFormat === "webui" ? "Source format: Web UI (normalized to API format)." : "Source format: API format."
32349
+ ];
32350
+ if (objectInfoAvailability.warning) {
32351
+ textSections.push(objectInfoAvailability.warning);
32352
+ }
32353
+ textSections.push(
32354
+ `High-signal overrides (${highSignalInputs.length}):`,
32355
+ formatEditableInputsText(highSignalInputs, {
32356
+ includeOptions: true
32357
+ }),
32358
+ "Editable input groups:",
32359
+ formatEditableInputGroupsText(editableInputGroups),
32360
+ `Editable inputs (${editableInputs.length} total):`,
32361
+ formatEditableInputsText(editableInputs),
32362
+ "Copy-ready override example:",
32363
+ formatOverrideExamplesText(overrideExamples.examples)
32364
+ );
32365
+ const text = textSections.join("\n");
31564
32366
  const structuredContent = {
31565
32367
  workflow_id: workflowId,
31566
32368
  path: loadedWorkflow.targetPath,
31567
32369
  source_format: loadedWorkflow.sourceFormat,
31568
- editable_inputs: editableInputs
32370
+ editable_inputs: editableInputs,
32371
+ high_signal_inputs: highSignalInputs,
32372
+ input_groups: editableInputGroups,
32373
+ override_examples: overrideExamples.examples,
32374
+ preferred_override_paths: overrideExamples.preferred_paths
31569
32375
  };
32376
+ if (objectInfoAvailability.warning) {
32377
+ structuredContent.warnings = [objectInfoAvailability.warning];
32378
+ }
31570
32379
  if (args.include_prompt) {
31571
32380
  structuredContent.prompt = loadedWorkflow.prompt;
31572
32381
  }
@@ -31581,10 +32390,17 @@ async function handleGetWorkflow(args) {
31581
32390
  async function handleSaveWorkflow(args) {
31582
32391
  try {
31583
32392
  let prompt;
32393
+ let widgetPathMap = {};
31584
32394
  let sourcePath;
32395
+ const parsedOverrides = parseOverrides(args.overrides);
32396
+ const objectInfoAvailability = await tryGetObjectInfo();
31585
32397
  if (args.workflow !== void 0) {
31586
- const normalized = await normalizeWorkflowPrompt(args.workflow);
32398
+ const normalized = await normalizeWorkflowPrompt(
32399
+ args.workflow,
32400
+ objectInfoAvailability.objectInfo
32401
+ );
31587
32402
  prompt = normalized.prompt;
32403
+ widgetPathMap = normalized.widgetPathMap;
31588
32404
  } else {
31589
32405
  if (!args.workflow_file_path && !args.workflow_id) {
31590
32406
  return createValidationError(
@@ -31592,12 +32408,25 @@ async function handleSaveWorkflow(args) {
31592
32408
  "Provide workflow JSON or an existing workflow reference."
31593
32409
  );
31594
32410
  }
31595
- const loadedWorkflow = await loadWorkflow(args);
32411
+ const loadedWorkflow = await loadWorkflow(
32412
+ args,
32413
+ objectInfoAvailability.objectInfo
32414
+ );
31596
32415
  prompt = loadedWorkflow.prompt;
31597
32416
  sourcePath = loadedWorkflow.targetPath;
32417
+ widgetPathMap = loadedWorkflow.widgetPathMap;
31598
32418
  }
31599
- if (args.overrides) {
31600
- applyOverrides(prompt, args.overrides);
32419
+ const editableInputs = analyzeEditableInputs(
32420
+ prompt,
32421
+ objectInfoAvailability.objectInfo,
32422
+ widgetPathMap
32423
+ );
32424
+ const editableIndex = buildEditableInputIndex(editableInputs);
32425
+ if (parsedOverrides) {
32426
+ applyOverrides(prompt, parsedOverrides, {
32427
+ editableIndex,
32428
+ objectInfo: objectInfoAvailability.objectInfo
32429
+ });
31601
32430
  }
31602
32431
  const outputFileName = deriveSavedWorkflowFileName(
31603
32432
  sourcePath,
@@ -31630,25 +32459,57 @@ async function handleSaveWorkflow(args) {
31630
32459
  `,
31631
32460
  "utf8"
31632
32461
  );
31633
- const editableInputs = analyzeEditableInputs(prompt);
32462
+ const updatedEditableInputs = analyzeEditableInputs(
32463
+ prompt,
32464
+ objectInfoAvailability.objectInfo,
32465
+ widgetPathMap
32466
+ );
32467
+ const highSignalInputs = getHighSignalEditableInputs(
32468
+ updatedEditableInputs
32469
+ );
32470
+ const editableInputGroups = summarizeEditableInputGroups(
32471
+ updatedEditableInputs
32472
+ );
32473
+ const overrideExamples = buildOverrideExamples(updatedEditableInputs);
31634
32474
  const workflowId = getWorkflowId(outputPath);
31635
32475
  const structuredContent = {
31636
32476
  workflow_id: workflowId,
31637
32477
  path: outputPath,
31638
32478
  source_path: sourcePath,
31639
32479
  overwritten: existedBeforeSave,
31640
- editable_inputs: editableInputs
32480
+ editable_inputs: updatedEditableInputs,
32481
+ high_signal_inputs: highSignalInputs,
32482
+ input_groups: editableInputGroups,
32483
+ override_examples: overrideExamples.examples,
32484
+ preferred_override_paths: overrideExamples.preferred_paths
31641
32485
  };
32486
+ if (objectInfoAvailability.warning) {
32487
+ structuredContent.warnings = [objectInfoAvailability.warning];
32488
+ }
31642
32489
  if (args.include_prompt) {
31643
32490
  structuredContent.prompt = prompt;
31644
32491
  }
31645
- const persistedChanges = args.overrides && Object.keys(args.overrides).length > 0 ? `Persisted ${Object.keys(args.overrides).length} override(s).` : "No overrides were applied.";
31646
- const text = [
32492
+ const persistedChanges = parsedOverrides && Object.keys(parsedOverrides).length > 0 ? `Persisted ${Object.keys(parsedOverrides).length} override(s).` : "No overrides were applied.";
32493
+ const textSections = [
31647
32494
  `Saved workflow ${workflowId} to ${outputPath}.`,
31648
- persistedChanges,
31649
- `Editable inputs (${editableInputs.length}):`,
31650
- formatEditableInputsText(editableInputs)
31651
- ].join("\n");
32495
+ persistedChanges
32496
+ ];
32497
+ if (objectInfoAvailability.warning) {
32498
+ textSections.push(objectInfoAvailability.warning);
32499
+ }
32500
+ textSections.push(
32501
+ `High-signal overrides (${highSignalInputs.length}):`,
32502
+ formatEditableInputsText(highSignalInputs, {
32503
+ includeOptions: true
32504
+ }),
32505
+ "Editable input groups:",
32506
+ formatEditableInputGroupsText(editableInputGroups),
32507
+ `Editable inputs (${updatedEditableInputs.length} total):`,
32508
+ formatEditableInputsText(updatedEditableInputs),
32509
+ "Copy-ready override example:",
32510
+ formatOverrideExamplesText(overrideExamples.examples)
32511
+ );
32512
+ const text = textSections.join("\n");
31652
32513
  return {
31653
32514
  content: [{ type: "text", text }],
31654
32515
  structuredContent
@@ -31659,11 +32520,49 @@ async function handleSaveWorkflow(args) {
31659
32520
  }
31660
32521
  async function handleWorkflowRun(args) {
31661
32522
  try {
31662
- const { prompt } = await loadWorkflow(args);
32523
+ let loadedWorkflow;
32524
+ let objectInfo;
32525
+ if (args.workflow !== void 0) {
32526
+ objectInfo = await getObjectInfo();
32527
+ const normalized = await normalizeWorkflowPrompt(
32528
+ args.workflow,
32529
+ objectInfo
32530
+ );
32531
+ loadedWorkflow = {
32532
+ targetPath: "[inline workflow]",
32533
+ prompt: normalized.prompt,
32534
+ sourceFormat: normalized.sourceFormat,
32535
+ widgetPathMap: normalized.widgetPathMap
32536
+ };
32537
+ } else {
32538
+ if (!args.workflow_file_path && !args.workflow_id) {
32539
+ return createValidationError(
32540
+ "workflow",
32541
+ "Provide workflow JSON or an existing workflow reference."
32542
+ );
32543
+ }
32544
+ objectInfo = await getObjectInfo();
32545
+ loadedWorkflow = await loadWorkflow(args, objectInfo);
32546
+ }
32547
+ const { prompt } = loadedWorkflow;
31663
32548
  const promptObj = { ...prompt };
31664
- if (args.overrides) {
31665
- applyOverrides(promptObj, args.overrides);
32549
+ const parsedOverrides = parseOverrides(args.overrides);
32550
+ if (parsedOverrides) {
32551
+ const editableInputs = analyzeEditableInputs(
32552
+ prompt,
32553
+ objectInfo,
32554
+ loadedWorkflow.widgetPathMap
32555
+ );
32556
+ applyOverrides(promptObj, parsedOverrides, {
32557
+ editableIndex: buildEditableInputIndex(editableInputs),
32558
+ objectInfo
32559
+ });
31666
32560
  }
32561
+ const preflight = validatePromptPreflight(
32562
+ promptObj,
32563
+ objectInfo,
32564
+ loadedWorkflow.widgetPathMap
32565
+ );
31667
32566
  const clientId = `mcp-${Date.now()}`;
31668
32567
  const result = IS_MOCK ? MOCK_FIXTURES.prompt_response : await fetchComfyJson("/prompt", {
31669
32568
  method: "POST",
@@ -31690,16 +32589,25 @@ async function handleWorkflowRun(args) {
31690
32589
  timeout: args.timeout
31691
32590
  });
31692
32591
  }
32592
+ const queueSnapshot = await getQueueSnapshot();
31693
32593
  return {
31694
32594
  content: [
31695
32595
  {
31696
32596
  type: "text",
31697
- text: `Workflow submitted. prompt_id: ${result.prompt_id}, queue: ${result.number}.
31698
-
31699
- To retrieve the final results or check its status later, use the 'comfyui_wait_for_workflow' tool with this prompt_id.`
32597
+ text: [
32598
+ `Workflow submitted. prompt_id: ${result.prompt_id}.`,
32599
+ `ComfyUI queue number: ${result.number} (queue number is not an ETA).`,
32600
+ `Preflight checked ${preflight.checked_inputs} literal inputs (${preflight.combo_inputs} combo-backed).`,
32601
+ formatQueueSnapshotText(queueSnapshot),
32602
+ `To retrieve the final results or check its status later, use the 'comfyui_wait_for_workflow' tool with this prompt_id.`
32603
+ ].filter((line) => line && line.length > 0).join("\n")
31700
32604
  }
31701
32605
  ],
31702
- structuredContent: { ...result }
32606
+ structuredContent: {
32607
+ ...result,
32608
+ preflight,
32609
+ ...queueSnapshot ? { queue: queueSnapshot } : {}
32610
+ }
31703
32611
  };
31704
32612
  } catch (error48) {
31705
32613
  return handleComfyError(error48);