@fre4x/comfyui 1.1.7 → 1.1.9

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 +70 -33
  2. package/dist/index.js +1706 -1015
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6891,8 +6891,8 @@ var require_dist = __commonJS({
6891
6891
  });
6892
6892
 
6893
6893
  // src/index.ts
6894
- import * as fs3 from "node:fs/promises";
6895
6894
  import { realpathSync } from "node:fs";
6895
+ import * as fs3 from "node:fs/promises";
6896
6896
  import path3 from "node:path";
6897
6897
  import { fileURLToPath } from "node:url";
6898
6898
 
@@ -6932,7 +6932,7 @@ function createInternalError(error48) {
6932
6932
  type: "text",
6933
6933
  text: `Internal Error: ${message}
6934
6934
 
6935
- Suggestion: If the problem persists, check server logs.`
6935
+ Suggestion: Use the error details above to correct tool arguments and retry the tool call.`
6936
6936
  }
6937
6937
  ]
6938
6938
  };
@@ -6944,6 +6944,11 @@ function formatListItems(items, renderer, emptyMessage = "_No results found._")
6944
6944
  return emptyMessage;
6945
6945
  return items.map((item, i) => renderer(item, i)).join("\n\n");
6946
6946
  }
6947
+ function truncateToLimit(text, maxChars = 12e3) {
6948
+ if (text.length <= maxChars)
6949
+ return text;
6950
+ return text.slice(0, maxChars) + "\n\n_...response truncated to fit context window. Use pagination to retrieve more results._";
6951
+ }
6947
6952
  function formatPaginationFooter(offset, limit, total) {
6948
6953
  const start = offset + 1;
6949
6954
  const end = Math.min(offset + limit, total);
@@ -30516,6 +30521,10 @@ var StdioServerTransport = class {
30516
30521
  }
30517
30522
  };
30518
30523
 
30524
+ // src/api.ts
30525
+ import * as fs from "node:fs/promises";
30526
+ import path from "node:path";
30527
+
30519
30528
  // src/errors.ts
30520
30529
  var ComfyApiError = class extends Error {
30521
30530
  constructor(message, statusCode, details) {
@@ -30539,7 +30548,9 @@ var WorkflowInputError = class extends Error {
30539
30548
  };
30540
30549
  var WorkflowTimeoutError = class extends Error {
30541
30550
  constructor(promptId, timeoutSeconds, queueSnapshot) {
30542
- super(`Timeout waiting for workflow ${promptId}`);
30551
+ super(
30552
+ `Timed out waiting for prompt_id '${promptId}'. Call comfyui_wait_for_workflow again with this prompt_id`
30553
+ );
30543
30554
  this.promptId = promptId;
30544
30555
  this.timeoutSeconds = timeoutSeconds;
30545
30556
  this.queueSnapshot = queueSnapshot;
@@ -30550,10 +30561,6 @@ var WorkflowTimeoutError = class extends Error {
30550
30561
  queueSnapshot;
30551
30562
  };
30552
30563
 
30553
- // src/api.ts
30554
- import * as fs from "node:fs/promises";
30555
- import path from "node:path";
30556
-
30557
30564
  // src/mock.ts
30558
30565
  var IS_MOCK = process.env.MOCK === "true";
30559
30566
  var MOCK_FIXTURES = {
@@ -30701,25 +30708,77 @@ var MOCK_FIXTURES = {
30701
30708
  messages: []
30702
30709
  }
30703
30710
  }
30711
+ },
30712
+ models: {
30713
+ checkpoint: [
30714
+ "v1-5-pruned-emaonly.ckpt",
30715
+ "sd_xl_base_1.0.safetensors",
30716
+ "cyberrealisticPony_v160.safetensors"
30717
+ ],
30718
+ lora: ["mock_style.safetensors", "mock_character.safetensors"],
30719
+ vae: ["ae.safetensors", "sdxl_vae.safetensors"],
30720
+ controlnet: ["control_v11p_sd15_canny.pth"],
30721
+ unet: ["flux1-dev.safetensors"],
30722
+ clip: ["clip_l.safetensors", "t5xxl_fp16.safetensors"],
30723
+ upscale: ["RealESRGAN_x4plus.pth"]
30704
30724
  }
30705
30725
  };
30706
30726
 
30707
30727
  // src/api.ts
30708
- var COMFYUI_SERVER_URL = process.env.COMFYUI_SERVER_URL || "http://localhost:8188";
30728
+ var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
30729
+ var MODEL_FOLDER_BY_TYPE = {
30730
+ checkpoint: ["checkpoints"],
30731
+ lora: ["loras"],
30732
+ vae: ["vae"],
30733
+ controlnet: ["controlnet"],
30734
+ unet: ["diffusion_models", "unet"],
30735
+ clip: ["clip"],
30736
+ upscale: ["upscale_models"]
30737
+ };
30738
+ var MIME_BY_EXTENSION = {
30739
+ ".png": "image/png",
30740
+ ".jpg": "image/jpeg",
30741
+ ".jpeg": "image/jpeg",
30742
+ ".webp": "image/webp",
30743
+ ".gif": "image/gif",
30744
+ ".bmp": "image/bmp",
30745
+ ".tif": "image/tiff",
30746
+ ".tiff": "image/tiff"
30747
+ };
30748
+ function getComfyServerUrl() {
30749
+ return process.env.COMFYUI_SERVER_URL || "http://localhost:8188";
30750
+ }
30751
+ function buildComfyHeaders(extra) {
30752
+ const headers = new Headers();
30753
+ headers.set("Accept", "application/json");
30754
+ const apiKey = process.env.COMFY_API_KEY?.trim();
30755
+ if (apiKey) {
30756
+ const headerName = process.env.COMFY_API_HEADER?.trim() || "X-API-Key";
30757
+ const value = headerName.toLowerCase() === "authorization" && !/^Bearer\s+/i.test(apiKey) ? `Bearer ${apiKey}` : apiKey;
30758
+ headers.set(headerName, value);
30759
+ }
30760
+ if (extra) {
30761
+ new Headers(extra).forEach((value, key) => {
30762
+ headers.set(key, value);
30763
+ });
30764
+ }
30765
+ return headers;
30766
+ }
30767
+ function isStringArray(value) {
30768
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
30769
+ }
30709
30770
  async function fetchComfyJson(apiPath, options) {
30710
- const url2 = new URL(apiPath, COMFYUI_SERVER_URL).toString();
30771
+ const serverUrl = getComfyServerUrl();
30772
+ const url2 = new URL(apiPath, serverUrl).toString();
30711
30773
  let response;
30712
30774
  try {
30713
30775
  response = await fetch(url2, {
30714
30776
  ...options,
30715
- headers: {
30716
- Accept: "application/json",
30717
- ...options?.headers || {}
30718
- }
30777
+ headers: buildComfyHeaders(options?.headers)
30719
30778
  });
30720
30779
  } catch (error48) {
30721
30780
  throw new Error(
30722
- `Failed to connect to ComfyUI server at ${COMFYUI_SERVER_URL}: ${error48 instanceof Error ? error48.message : String(error48)}`
30781
+ `Failed to connect to ComfyUI server at ${serverUrl}: ${error48 instanceof Error ? error48.message : String(error48)}`
30723
30782
  );
30724
30783
  }
30725
30784
  if (response.status === 404) {
@@ -30750,9 +30809,11 @@ async function fetchImageAsset(filename, type, subfolder) {
30750
30809
  if (subfolder) params.set("subfolder", subfolder);
30751
30810
  const url2 = new URL(
30752
30811
  `/view?${params.toString()}`,
30753
- COMFYUI_SERVER_URL
30812
+ getComfyServerUrl()
30754
30813
  ).toString();
30755
- const response = await fetch(url2);
30814
+ const response = await fetch(url2, {
30815
+ headers: buildComfyHeaders()
30816
+ });
30756
30817
  if (!response.ok)
30757
30818
  throw new Error(`Failed to fetch image: ${response.statusText}`);
30758
30819
  const arrayBuffer = await response.arrayBuffer();
@@ -30779,27 +30840,27 @@ function sanitizePathSegment(value, fallback) {
30779
30840
  const normalized = value.trim().replace(/[\\/]+/g, "_").replace(/\.\.+/g, "_").replace(/_+/g, "_").replace(/[^A-Za-z0-9._-]/g, "_");
30780
30841
  return normalized.length > 0 ? normalized : fallback;
30781
30842
  }
30843
+ function getWorkspaceOutputRelativePath(promptId, image) {
30844
+ return path.join(
30845
+ "comfyui",
30846
+ sanitizePathSegment(promptId, "unknown-prompt"),
30847
+ ...getSafePathSegments(image.subfolder),
30848
+ path.basename(image.filename)
30849
+ );
30850
+ }
30782
30851
  async function saveToWorkspace(promptId, image, asset) {
30783
30852
  const workspaceDir = getSessionWorkspaceDir();
30784
30853
  if (!workspaceDir) {
30785
30854
  return void 0;
30786
30855
  }
30787
- const safePromptId = sanitizePathSegment(promptId, "unknown-prompt");
30788
- const safeFileName = path.basename(image.filename);
30789
- const safeSubfolderSegments = getSafePathSegments(image.subfolder);
30790
- const relativePath = path.join(
30791
- "comfyui",
30792
- safePromptId,
30793
- ...safeSubfolderSegments,
30794
- safeFileName
30795
- );
30856
+ const relativePath = getWorkspaceOutputRelativePath(promptId, image);
30796
30857
  const workspacePath = path.join(workspaceDir, relativePath);
30797
30858
  await fs.mkdir(path.dirname(workspacePath), { recursive: true });
30798
30859
  await fs.writeFile(workspacePath, asset.buffer);
30799
30860
  return {
30800
30861
  workspace_path: workspacePath,
30801
30862
  relative_path: relativePath,
30802
- filename: safeFileName,
30863
+ filename: path.basename(image.filename),
30803
30864
  mime_type: asset.mimeType,
30804
30865
  subfolder: image.subfolder
30805
30866
  };
@@ -30830,17 +30891,58 @@ async function getQueueSnapshot() {
30830
30891
  return void 0;
30831
30892
  }
30832
30893
  }
30894
+ function extractPromptIdsFromQueueEntries(entries) {
30895
+ const ids = [];
30896
+ for (const entry of entries) {
30897
+ if (!Array.isArray(entry) || entry.length === 0) {
30898
+ continue;
30899
+ }
30900
+ if (typeof entry[1] === "string" && entry[1].length > 0) {
30901
+ ids.push(entry[1]);
30902
+ continue;
30903
+ }
30904
+ if (typeof entry[0] === "string" && entry[0].length > 0) {
30905
+ ids.push(entry[0]);
30906
+ }
30907
+ }
30908
+ return ids;
30909
+ }
30910
+ function summarizeQueue(queueSnapshot) {
30911
+ if (!queueSnapshot) {
30912
+ return void 0;
30913
+ }
30914
+ return {
30915
+ running: queueSnapshot.queue_running.length,
30916
+ pending: queueSnapshot.queue_pending.length,
30917
+ running_prompt_ids: extractPromptIdsFromQueueEntries(
30918
+ queueSnapshot.queue_running
30919
+ ),
30920
+ pending_prompt_ids: extractPromptIdsFromQueueEntries(
30921
+ queueSnapshot.queue_pending
30922
+ )
30923
+ };
30924
+ }
30833
30925
  function formatQueueSnapshotText(queueSnapshot) {
30834
30926
  if (!queueSnapshot) {
30835
30927
  return "";
30836
30928
  }
30929
+ if ("running" in queueSnapshot && "pending" in queueSnapshot) {
30930
+ return `Queue snapshot: ${queueSnapshot.running} running, ${queueSnapshot.pending} pending.`;
30931
+ }
30837
30932
  return `Queue snapshot: ${queueSnapshot.queue_running.length} running, ${queueSnapshot.queue_pending.length} pending.`;
30838
30933
  }
30839
30934
  function getPromptQueueVisibility(promptId, queueSnapshot) {
30840
30935
  if (!queueSnapshot) {
30841
30936
  return null;
30842
30937
  }
30843
- return JSON.stringify(queueSnapshot).includes(promptId);
30938
+ if ("running_prompt_ids" in queueSnapshot) {
30939
+ return queueSnapshot.running_prompt_ids.includes(promptId) || queueSnapshot.pending_prompt_ids.includes(promptId);
30940
+ }
30941
+ const summary = summarizeQueue(queueSnapshot);
30942
+ if (!summary) {
30943
+ return null;
30944
+ }
30945
+ return summary.running_prompt_ids.includes(promptId) || summary.pending_prompt_ids.includes(promptId);
30844
30946
  }
30845
30947
  async function waitForWorkflowResult(prompt_id, timeout_seconds) {
30846
30948
  const start = Date.now();
@@ -30861,9 +30963,270 @@ async function waitForWorkflowResult(prompt_id, timeout_seconds) {
30861
30963
  }
30862
30964
  await new Promise((r) => setTimeout(r, 1e3));
30863
30965
  }
30864
- const queueSnapshot = await getQueueSnapshot();
30966
+ const queueSnapshot = summarizeQueue(await getQueueSnapshot());
30865
30967
  throw new WorkflowTimeoutError(prompt_id, timeout_seconds, queueSnapshot);
30866
30968
  }
30969
+ function parseInterruptedFlag(body) {
30970
+ if (!body || typeof body !== "object" || !("interrupted" in body)) {
30971
+ return void 0;
30972
+ }
30973
+ const interrupted = body.interrupted;
30974
+ return typeof interrupted === "boolean" ? interrupted : void 0;
30975
+ }
30976
+ async function interruptWorkflow(options) {
30977
+ let interrupted = true;
30978
+ if (!IS_MOCK) {
30979
+ const interruptBody = await fetchComfyJson("/interrupt", {
30980
+ method: "POST",
30981
+ ...options.promptId ? {
30982
+ body: JSON.stringify({ prompt_id: options.promptId }),
30983
+ headers: { "Content-Type": "application/json" }
30984
+ } : {}
30985
+ });
30986
+ const parsed = parseInterruptedFlag(interruptBody);
30987
+ if (parsed !== void 0) {
30988
+ interrupted = parsed;
30989
+ }
30990
+ if (options.clearQueue) {
30991
+ await fetchComfyJson("/queue", {
30992
+ method: "POST",
30993
+ body: JSON.stringify({ clear: true }),
30994
+ headers: { "Content-Type": "application/json" }
30995
+ });
30996
+ }
30997
+ }
30998
+ const queue = summarizeQueue(await getQueueSnapshot());
30999
+ return {
31000
+ interrupted,
31001
+ prompt_id: options.promptId,
31002
+ queue_cleared: options.clearQueue,
31003
+ ...queue ? { queue } : {}
31004
+ };
31005
+ }
31006
+ function mimeTypeFromFilename(filename) {
31007
+ const extension = path.extname(filename).toLowerCase();
31008
+ return MIME_BY_EXTENSION[extension] || "image/png";
31009
+ }
31010
+ async function getUploadRoots() {
31011
+ const candidates = [
31012
+ process.cwd(),
31013
+ process.env.MCP_WORKSPACE_DIR,
31014
+ process.env.COMFYUI_UPLOAD_ROOT
31015
+ ].filter((value) => Boolean(value));
31016
+ const roots = [];
31017
+ for (const candidate of candidates) {
31018
+ try {
31019
+ roots.push(await fs.realpath(candidate));
31020
+ } catch {
31021
+ roots.push(path.resolve(candidate));
31022
+ }
31023
+ }
31024
+ return [...new Set(roots)];
31025
+ }
31026
+ function isPathInsideRoot(target, root) {
31027
+ return target === root || target.startsWith(`${root}${path.sep}`);
31028
+ }
31029
+ async function resolveReadableUploadPath(filePath) {
31030
+ let resolved;
31031
+ try {
31032
+ resolved = await fs.realpath(filePath);
31033
+ } catch {
31034
+ throw new WorkflowInputError(
31035
+ `Image file not found: ${filePath}`,
31036
+ "file_path",
31037
+ "not_found"
31038
+ );
31039
+ }
31040
+ const roots = await getUploadRoots();
31041
+ if (!roots.some((root) => isPathInsideRoot(resolved, root))) {
31042
+ throw new WorkflowInputError(
31043
+ "file_path must be inside the workspace or COMFYUI_UPLOAD_ROOT.",
31044
+ "file_path"
31045
+ );
31046
+ }
31047
+ return resolved;
31048
+ }
31049
+ async function readPreviousRunImage(params) {
31050
+ if (IS_MOCK) {
31051
+ return createMockImageAsset();
31052
+ }
31053
+ const workspaceDir = getSessionWorkspaceDir();
31054
+ if (workspaceDir) {
31055
+ const candidate = path.join(
31056
+ workspaceDir,
31057
+ getWorkspaceOutputRelativePath(params.promptId, {
31058
+ filename: params.filename,
31059
+ subfolder: params.subfolder
31060
+ })
31061
+ );
31062
+ try {
31063
+ const resolved = await fs.realpath(candidate);
31064
+ let root;
31065
+ try {
31066
+ root = await fs.realpath(workspaceDir);
31067
+ } catch {
31068
+ root = path.resolve(workspaceDir);
31069
+ }
31070
+ if (isPathInsideRoot(resolved, root)) {
31071
+ return {
31072
+ buffer: await fs.readFile(resolved),
31073
+ mimeType: mimeTypeFromFilename(params.filename)
31074
+ };
31075
+ }
31076
+ } catch {
31077
+ }
31078
+ }
31079
+ return fetchImageAsset(
31080
+ params.filename,
31081
+ params.sourceType,
31082
+ params.subfolder
31083
+ );
31084
+ }
31085
+ async function readUploadImageSource(params) {
31086
+ const sources = [
31087
+ params.filePath ? "file_path" : void 0,
31088
+ params.promptId ? "prompt_id" : void 0
31089
+ ].filter((value) => Boolean(value));
31090
+ if (sources.length !== 1) {
31091
+ throw new WorkflowInputError(
31092
+ "Provide exactly one image source: file_path, or prompt_id + filename from a previous run.",
31093
+ sources[1] ?? "file_path"
31094
+ );
31095
+ }
31096
+ if (params.promptId) {
31097
+ if (!params.filename?.trim()) {
31098
+ throw new WorkflowInputError(
31099
+ "filename is required when uploading from a previous prompt_id.",
31100
+ "filename"
31101
+ );
31102
+ }
31103
+ const asset = await readPreviousRunImage({
31104
+ promptId: params.promptId,
31105
+ filename: params.filename,
31106
+ subfolder: params.subfolder,
31107
+ sourceType: params.sourceType ?? "output"
31108
+ });
31109
+ if (asset.buffer.byteLength > MAX_UPLOAD_BYTES) {
31110
+ throw new WorkflowInputError(
31111
+ `Image exceeds the ${MAX_UPLOAD_BYTES} byte upload limit.`,
31112
+ "prompt_id"
31113
+ );
31114
+ }
31115
+ const filename2 = sanitizePathSegment(
31116
+ params.filename,
31117
+ `upload-${Date.now()}.png`
31118
+ );
31119
+ return {
31120
+ buffer: asset.buffer,
31121
+ filename: filename2,
31122
+ mimeType: asset.mimeType
31123
+ };
31124
+ }
31125
+ if (!params.filePath) {
31126
+ throw new WorkflowInputError(
31127
+ "Provide exactly one image source: file_path, or prompt_id + filename from a previous run.",
31128
+ "file_path"
31129
+ );
31130
+ }
31131
+ const resolvedPath = await resolveReadableUploadPath(params.filePath);
31132
+ const buffer = await fs.readFile(resolvedPath);
31133
+ if (buffer.byteLength > MAX_UPLOAD_BYTES) {
31134
+ throw new WorkflowInputError(
31135
+ `Image exceeds the ${MAX_UPLOAD_BYTES} byte upload limit.`,
31136
+ "file_path"
31137
+ );
31138
+ }
31139
+ const filename = sanitizePathSegment(
31140
+ params.filename || path.basename(resolvedPath),
31141
+ `upload-${Date.now()}.png`
31142
+ );
31143
+ return {
31144
+ buffer,
31145
+ filename,
31146
+ mimeType: mimeTypeFromFilename(filename)
31147
+ };
31148
+ }
31149
+ async function uploadImage(params) {
31150
+ if (IS_MOCK) {
31151
+ return {
31152
+ name: params.filename,
31153
+ subfolder: "",
31154
+ type: params.imageType
31155
+ };
31156
+ }
31157
+ const formData = new FormData();
31158
+ const bytes = new Uint8Array(params.buffer);
31159
+ formData.append(
31160
+ "image",
31161
+ new Blob([bytes], { type: params.mimeType }),
31162
+ params.filename
31163
+ );
31164
+ formData.append("type", params.imageType);
31165
+ if (params.overwrite) {
31166
+ formData.append("overwrite", "true");
31167
+ }
31168
+ const uploaded = await fetchComfyJson("/upload/image", {
31169
+ method: "POST",
31170
+ body: formData
31171
+ });
31172
+ if (uploaded && typeof uploaded === "object" && "name" in uploaded && typeof uploaded.name === "string") {
31173
+ const record2 = uploaded;
31174
+ return {
31175
+ name: uploaded.name,
31176
+ subfolder: typeof record2.subfolder === "string" ? record2.subfolder : "",
31177
+ type: typeof record2.type === "string" ? record2.type : params.imageType
31178
+ };
31179
+ }
31180
+ return {
31181
+ name: params.filename,
31182
+ subfolder: "",
31183
+ type: params.imageType
31184
+ };
31185
+ }
31186
+ async function getModelList(modelType) {
31187
+ if (IS_MOCK) {
31188
+ return {
31189
+ models: MOCK_FIXTURES.models[modelType] ?? [],
31190
+ models_endpoint_available: true
31191
+ };
31192
+ }
31193
+ const models = /* @__PURE__ */ new Set();
31194
+ let sawSuccessfulFolder = false;
31195
+ for (const folder of MODEL_FOLDER_BY_TYPE[modelType]) {
31196
+ try {
31197
+ const listed = await fetchComfyJson(`/models/${folder}`);
31198
+ if (!isStringArray(listed)) {
31199
+ continue;
31200
+ }
31201
+ sawSuccessfulFolder = true;
31202
+ for (const item of listed) {
31203
+ models.add(item);
31204
+ }
31205
+ } catch (error48) {
31206
+ if (error48 instanceof ComfyApiError && error48.statusCode === 404) {
31207
+ continue;
31208
+ }
31209
+ throw error48;
31210
+ }
31211
+ }
31212
+ return {
31213
+ models: [...models].sort(
31214
+ (left, right) => left.localeCompare(right, void 0, { numeric: true })
31215
+ ),
31216
+ models_endpoint_available: sawSuccessfulFolder
31217
+ };
31218
+ }
31219
+
31220
+ // src/types.ts
31221
+ var COMFY_MODEL_TYPES = [
31222
+ "checkpoint",
31223
+ "lora",
31224
+ "vae",
31225
+ "controlnet",
31226
+ "unet",
31227
+ "clip",
31228
+ "upscale"
31229
+ ];
30867
31230
 
30868
31231
  // src/workflow/common.ts
30869
31232
  function isRecord(value) {
@@ -30988,9 +31351,6 @@ function getInputOptionsSummary(inputDefinition, previewLimit = 8) {
30988
31351
  options_truncated: typeData.length > previewLimit
30989
31352
  };
30990
31353
  }
30991
- function isPlainObject3(value) {
30992
- return isRecord(value) && !Array.isArray(value);
30993
- }
30994
31354
  function getValueTypeName(value) {
30995
31355
  if (Array.isArray(value)) {
30996
31356
  return "array";
@@ -31001,906 +31361,1014 @@ function getValueTypeName(value) {
31001
31361
  return typeof value;
31002
31362
  }
31003
31363
 
31004
- // src/workflow/load.ts
31005
- import * as fs2 from "node:fs/promises";
31006
- import * as crypto from "node:crypto";
31007
- import { homedir } from "node:os";
31008
- import path2 from "node:path";
31009
-
31010
- // src/converter.ts
31011
- var WIDGET_TYPES = /* @__PURE__ */ new Set(["INT", "FLOAT", "STRING", "BOOLEAN"]);
31012
- var UI_CONTROL_VALUES = /* @__PURE__ */ new Set([
31013
- "fixed",
31014
- "increment",
31015
- "decrement",
31016
- "randomize"
31017
- ]);
31018
- function isRecord2(value) {
31019
- return typeof value === "object" && value !== null;
31020
- }
31021
- function isWebUIFormat(json2) {
31022
- return isRecord2(json2) && Array.isArray(json2.nodes) && Array.isArray(json2.links);
31023
- }
31024
- function isLinkData(link) {
31025
- return Array.isArray(link) && link.length >= 6 && typeof link[0] === "number" && typeof link[1] === "number" && typeof link[2] === "number" && typeof link[3] === "number" && typeof link[4] === "number" && typeof link[5] === "string";
31364
+ // src/workflow/editable.ts
31365
+ function isConnectionValue(value) {
31366
+ return Array.isArray(value) && value.length >= 2 && (typeof value[0] === "string" || typeof value[0] === "number") && typeof value[1] === "number";
31026
31367
  }
31027
- function getOrderedInputEntries(group, orderedNames) {
31028
- if (!group) {
31029
- return [];
31368
+ function inferEditableRole(nodeId, classType, inputName, consumerMap) {
31369
+ if (classType === "CLIPTextEncode" && inputName === "text") {
31370
+ const consumers = consumerMap.get(nodeId) ?? [];
31371
+ if (consumers.some((consumer) => consumer.inputName === "positive")) {
31372
+ return "positive_prompt";
31373
+ }
31374
+ if (consumers.some((consumer) => consumer.inputName === "negative")) {
31375
+ return "negative_prompt";
31376
+ }
31377
+ return "prompt_text";
31030
31378
  }
31031
- const seen = /* @__PURE__ */ new Set();
31032
- const orderedEntries = [];
31033
- for (const inputName of orderedNames ?? []) {
31034
- const inputDefinition = group[inputName];
31035
- if (!inputDefinition) {
31036
- continue;
31379
+ if (inputName === "value") {
31380
+ const consumers = consumerMap.get(nodeId) ?? [];
31381
+ const textConsumers = consumers.filter(
31382
+ (consumer) => consumer.classType === "CLIPTextEncode" && consumer.inputName === "text"
31383
+ );
31384
+ if (textConsumers.length > 0) {
31385
+ for (const consumer of textConsumers) {
31386
+ const clipConsumers = consumerMap.get(consumer.nodeId) ?? [];
31387
+ if (clipConsumers.some(
31388
+ (clipConsumer) => clipConsumer.inputName === "positive"
31389
+ )) {
31390
+ return "positive_prompt";
31391
+ }
31392
+ if (clipConsumers.some(
31393
+ (clipConsumer) => clipConsumer.inputName === "negative"
31394
+ )) {
31395
+ return "negative_prompt";
31396
+ }
31397
+ }
31398
+ return "prompt_text";
31037
31399
  }
31038
- orderedEntries.push([inputName, inputDefinition]);
31039
- seen.add(inputName);
31040
31400
  }
31041
- for (const [inputName, inputDefinition] of Object.entries(group)) {
31042
- if (seen.has(inputName)) {
31401
+ switch (inputName) {
31402
+ case "seed":
31403
+ return "seed";
31404
+ case "steps":
31405
+ return "steps";
31406
+ case "cfg":
31407
+ return "cfg";
31408
+ case "sampler_name":
31409
+ return "sampler";
31410
+ case "scheduler":
31411
+ return "scheduler";
31412
+ case "denoise":
31413
+ return "denoise";
31414
+ case "width":
31415
+ return "width";
31416
+ case "height":
31417
+ return "height";
31418
+ case "batch_size":
31419
+ return "batch_size";
31420
+ case "ckpt_name":
31421
+ return "checkpoint";
31422
+ case "filename_prefix":
31423
+ return "filename_prefix";
31424
+ case "text":
31425
+ return "text";
31426
+ default:
31427
+ return void 0;
31428
+ }
31429
+ }
31430
+ var GLOBAL_OVERRIDE_ROLES = /* @__PURE__ */ new Set([
31431
+ "positive_prompt",
31432
+ "negative_prompt",
31433
+ "seed",
31434
+ "steps",
31435
+ "cfg",
31436
+ "checkpoint",
31437
+ "width",
31438
+ "height",
31439
+ "denoise",
31440
+ "sampler",
31441
+ "scheduler",
31442
+ "batch_size",
31443
+ "filename_prefix"
31444
+ ]);
31445
+ function collectRoleEntries(editableInputs) {
31446
+ const roleEntries = /* @__PURE__ */ new Map();
31447
+ for (const entry of editableInputs) {
31448
+ if (!entry.role || !GLOBAL_OVERRIDE_ROLES.has(entry.role)) {
31043
31449
  continue;
31044
31450
  }
31045
- orderedEntries.push([inputName, inputDefinition]);
31451
+ const entries = roleEntries.get(entry.role) ?? [];
31452
+ entries.push(entry);
31453
+ roleEntries.set(entry.role, entries);
31046
31454
  }
31047
- return orderedEntries;
31455
+ return roleEntries;
31048
31456
  }
31049
- function getWidgetTypeName(inputTypeData) {
31050
- return Array.isArray(inputTypeData) ? "COMBO" : String(inputTypeData).toUpperCase();
31457
+ function applyUniqueGlobalRoleAliases(editableInputs) {
31458
+ for (const [role, entries] of collectRoleEntries(editableInputs)) {
31459
+ if (entries.length !== 1) {
31460
+ continue;
31461
+ }
31462
+ const entry = entries[0];
31463
+ if (!entry.aliases.includes(role)) {
31464
+ entry.aliases.push(role);
31465
+ }
31466
+ }
31051
31467
  }
31052
- function isWidgetInput(inputTypeData) {
31053
- const typeName = getWidgetTypeName(inputTypeData);
31054
- return typeName === "COMBO" || WIDGET_TYPES.has(typeName);
31055
- }
31056
- function isSkippableUiArtifact(value, expectedTypeName) {
31057
- if (expectedTypeName !== "STRING" && typeof value === "string" && UI_CONTROL_VALUES.has(value)) {
31058
- return true;
31059
- }
31060
- return typeof value === "object" && value !== null && !Array.isArray(value);
31061
- }
31062
- function normalizeWidgetValue(value, typeName, extraInfo) {
31063
- const defaultValue = extraInfo?.default;
31064
- const shouldUseDefaultForNumeric = (typeName === "INT" || typeName === "FLOAT") && (value === "" || value === void 0 || value === null);
31065
- if (shouldUseDefaultForNumeric && defaultValue !== void 0) {
31066
- return defaultValue;
31067
- }
31068
- return value;
31069
- }
31070
- function convertWebUIToAPIWithMetadata(webUIJson, objectInfo) {
31071
- const prompt = {};
31072
- const widgetPathMap = {};
31073
- const links = webUIJson.links || [];
31074
- const nodes = webUIJson.nodes || [];
31075
- const serverNodeIds = /* @__PURE__ */ new Set();
31076
- for (const node of nodes) {
31077
- if (!node.id || !node.type || !objectInfo[node.type]) {
31078
- continue;
31079
- }
31080
- serverNodeIds.add(String(node.id));
31468
+ function getEditableAliases(canonicalPath, nodeId, inputName, role, widgetPathMap) {
31469
+ const aliases = /* @__PURE__ */ new Set([`${nodeId}.${inputName}`]);
31470
+ if (role) {
31471
+ aliases.add(`${nodeId}.${role}`);
31081
31472
  }
31082
- const linkMap = /* @__PURE__ */ new Map();
31083
- for (const link of links) {
31084
- if (isLinkData(link) && serverNodeIds.has(String(link[3]))) {
31085
- linkMap.set(link[0], link);
31473
+ for (const [widgetPath, resolvedPath] of Object.entries(widgetPathMap)) {
31474
+ if (resolvedPath === canonicalPath) {
31475
+ aliases.add(widgetPath);
31086
31476
  }
31087
31477
  }
31088
- for (const node of nodes) {
31089
- if (!node.id || !node.type) continue;
31090
- const objInfo = objectInfo[node.type];
31091
- if (!objInfo) continue;
31092
- const nodeIdStr = String(node.id);
31093
- const promptNode = {
31094
- class_type: node.type,
31095
- inputs: {}
31096
- };
31097
- const connectedInputNames = /* @__PURE__ */ new Set();
31098
- if (Array.isArray(node.inputs)) {
31099
- for (const input of node.inputs) {
31100
- if (input.name && input.link !== null && input.link !== void 0) {
31101
- const linkData = linkMap.get(input.link);
31102
- if (Array.isArray(linkData) && linkData.length >= 6 && String(linkData[3]) === nodeIdStr) {
31103
- const originNodeId = String(linkData[1]);
31104
- const originSlot = linkData[2];
31105
- promptNode.inputs[input.name] = [
31106
- originNodeId,
31107
- originSlot
31108
- ];
31109
- connectedInputNames.add(input.name);
31110
- }
31111
- }
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;
31112
31496
  }
31113
- }
31114
- if (objInfo) {
31115
- const widgets = Array.isArray(node.widgets_values) ? node.widgets_values : [];
31116
- let widgetIndex = 0;
31117
- const processInputGroup = (group, orderedNames) => {
31118
- for (const [inputName, inputDataRaw] of getOrderedInputEntries(
31119
- group,
31120
- orderedNames
31121
- )) {
31122
- const [inputTypeData, inputExtraInfo] = inputDataRaw;
31123
- if (!isWidgetInput(inputTypeData)) {
31124
- continue;
31125
- }
31126
- const typeName = getWidgetTypeName(inputTypeData);
31127
- while (widgetIndex < widgets.length && isSkippableUiArtifact(widgets[widgetIndex], typeName)) {
31128
- widgetIndex++;
31129
- }
31130
- const isConnectedWidget = connectedInputNames.has(inputName);
31131
- if (widgetIndex >= widgets.length) {
31132
- if (!isConnectedWidget && inputExtraInfo?.default !== void 0) {
31133
- promptNode.inputs[inputName] = inputExtraInfo.default;
31134
- }
31135
- continue;
31136
- }
31137
- const widgetSourceIndex = widgetIndex;
31138
- const value = normalizeWidgetValue(
31139
- widgets[widgetSourceIndex],
31140
- typeName,
31141
- inputExtraInfo
31142
- );
31143
- widgetIndex++;
31144
- if (!isConnectedWidget) {
31145
- promptNode.inputs[inputName] = value;
31146
- widgetPathMap[`${nodeIdStr}.widgets[${widgetSourceIndex}]`] = `${nodeIdStr}.inputs.${inputName}`;
31147
- }
31148
- }
31149
- };
31150
- processInputGroup(
31151
- objInfo.input?.required,
31152
- objInfo.input_order?.required
31153
- );
31154
- processInputGroup(
31155
- objInfo.input?.optional,
31156
- objInfo.input_order?.optional
31497
+ if (existing === entry.path) {
31498
+ continue;
31499
+ }
31500
+ const ambiguous = new Set(
31501
+ ambiguousAliases.get(alias) ?? [existing]
31157
31502
  );
31503
+ ambiguous.add(entry.path);
31504
+ ambiguousAliases.set(alias, [...ambiguous].sort());
31505
+ aliasToPath.delete(alias);
31158
31506
  }
31159
- prompt[nodeIdStr] = promptNode;
31507
+ }
31508
+ for (const [role, entries] of collectRoleEntries(editableInputs)) {
31509
+ if (entries.length <= 1) {
31510
+ continue;
31511
+ }
31512
+ const paths = [...new Set(entries.map((entry) => entry.path))].sort();
31513
+ ambiguousAliases.set(role, paths);
31514
+ aliasToPath.delete(role);
31160
31515
  }
31161
31516
  return {
31162
- prompt,
31163
- widgetPathMap
31517
+ entriesByPath,
31518
+ aliasToPath,
31519
+ ambiguousAliases,
31520
+ pathsByNode
31164
31521
  };
31165
31522
  }
31166
-
31167
- // src/workflow/load.ts
31168
- function isComfyPromptNode(value) {
31169
- return isRecord(value) && typeof value.class_type === "string" && isRecord(value.inputs);
31523
+ function getPathSegmentHelp(prompt, path4) {
31524
+ const parts = path4.split(".");
31525
+ if (parts.length === 0 || parts[0].length === 0) {
31526
+ return void 0;
31527
+ }
31528
+ const nodeId = parts[0];
31529
+ return {
31530
+ nodeId,
31531
+ nodeExists: isRecord(prompt[nodeId])
31532
+ };
31170
31533
  }
31171
- function isUiOnlyPromptNode(nodeId, node) {
31172
- return nodeId === "meta" || ["Note", "MarkdownNote", "PrimitiveNode"].includes(node.class_type);
31534
+ function describeAvailableNodePaths(editableIndex, nodeId) {
31535
+ if (!editableIndex) {
31536
+ return "";
31537
+ }
31538
+ const entries = editableIndex.pathsByNode.get(nodeId) ?? [];
31539
+ if (entries.length === 0) {
31540
+ return "";
31541
+ }
31542
+ return entries.map((entry) => {
31543
+ const aliasSuffix = entry.aliases.length > 0 ? ` (aliases: ${entry.aliases.join(", ")})` : "";
31544
+ return `${entry.path}${aliasSuffix}`;
31545
+ }).join("\n");
31173
31546
  }
31174
- function extractRunnablePromptNodes(prompt) {
31175
- const runnablePrompt = {};
31176
- for (const [nodeId, rawNode] of Object.entries(prompt)) {
31177
- if (!isComfyPromptNode(rawNode) || isUiOnlyPromptNode(nodeId, rawNode)) {
31178
- continue;
31179
- }
31180
- runnablePrompt[nodeId] = rawNode;
31547
+ function getPreferredOverridePath(entry) {
31548
+ if (entry.role && entry.aliases.includes(entry.role)) {
31549
+ return entry.role;
31181
31550
  }
31182
- if (Object.keys(runnablePrompt).length === 0) {
31183
- throw new WorkflowInputError(
31184
- "Workflow prompt does not contain any runnable ComfyUI nodes.",
31185
- "workflow"
31186
- );
31551
+ const roleAlias = entry.role ? `${entry.node_id}.${entry.role}` : void 0;
31552
+ if (roleAlias && entry.aliases.includes(roleAlias)) {
31553
+ return roleAlias;
31187
31554
  }
31188
- return runnablePrompt;
31555
+ const inputAlias = `${entry.node_id}.${entry.input_name}`;
31556
+ if (entry.aliases.includes(inputAlias)) {
31557
+ return inputAlias;
31558
+ }
31559
+ const nonWidgetAlias = entry.aliases.find(
31560
+ (alias) => !alias.includes(".widgets[")
31561
+ );
31562
+ return nonWidgetAlias ?? entry.path;
31189
31563
  }
31190
- function getWorkflowDirectories(extraPath) {
31191
- const directories = [
31192
- process.cwd(),
31193
- path2.join(process.cwd(), "workflows"),
31194
- path2.join(homedir(), ".fre4x-comfyui", "workflows"),
31195
- path2.join(
31196
- homedir(),
31197
- "comfy",
31198
- "ComfyUI",
31199
- "user",
31200
- "default",
31201
- "workflows"
31202
- )
31203
- ];
31204
- if (extraPath) {
31205
- directories.push(extraPath);
31564
+ function formatEditableInputOptionsText(entry) {
31565
+ if (entry.options_count === void 0 || entry.options_preview === void 0 || entry.options_preview.length === 0) {
31566
+ return "";
31206
31567
  }
31207
- return [
31208
- ...new Set(directories.map((directory) => path2.resolve(directory)))
31209
- ];
31568
+ const preview = entry.options_preview.map((option) => formatValuePreview(option)).join(", ");
31569
+ const truncatedSuffix = entry.options_truncated ? ", ..." : "";
31570
+ return ` | options: ${preview}${truncatedSuffix} (${entry.options_count} total)`;
31210
31571
  }
31211
- function getWorkflowNameCandidates(name) {
31212
- const trimmed = name.trim();
31213
- if (trimmed.length === 0) {
31214
- return [];
31572
+ function getEditableInputGroup(entry) {
31573
+ if (entry.role === "positive_prompt" || entry.role === "negative_prompt" || entry.role === "prompt_text") {
31574
+ return "prompts";
31215
31575
  }
31216
- if (trimmed.endsWith(".json")) {
31217
- return [trimmed];
31576
+ 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")) {
31577
+ return "models";
31218
31578
  }
31219
- return [trimmed, `${trimmed}.json`];
31220
- }
31221
- function normalizeWorkflowFileName(fileName) {
31222
- const trimmed = fileName.trim();
31223
- if (trimmed.length === 0) {
31224
- throw new WorkflowInputError(
31225
- "Filename cannot be empty.",
31226
- "output_file_name"
31227
- );
31579
+ if (entry.role === "seed" || entry.role === "steps" || entry.role === "cfg" || entry.role === "sampler" || entry.role === "scheduler" || entry.role === "denoise" || entry.class_type.includes("Sampler")) {
31580
+ return "sampling";
31228
31581
  }
31229
- if (path2.isAbsolute(trimmed)) {
31230
- throw new WorkflowInputError(
31231
- "Absolute paths are not allowed.",
31232
- "output_file_name"
31233
- );
31582
+ if (entry.role === "width" || entry.role === "height" || entry.role === "batch_size") {
31583
+ return "image";
31234
31584
  }
31235
- return trimmed.endsWith(".json") ? trimmed : `${trimmed}.json`;
31236
- }
31237
- async function resolveWorkflowReference(args) {
31238
- if (args.workflow_file_path) {
31239
- const targetPath = args.workflow_file_path;
31240
- try {
31241
- const resolvedPath = path2.resolve(targetPath);
31242
- const content = await fs2.readFile(resolvedPath, "utf8");
31243
- return {
31244
- targetPath: resolvedPath,
31245
- content
31246
- };
31247
- } catch {
31248
- if (path2.basename(targetPath) === targetPath) {
31249
- const candidates = getWorkflowNameCandidates(targetPath);
31250
- for (const dir of getWorkflowDirectories()) {
31251
- for (const candidate of candidates) {
31252
- const candidatePath = path2.join(dir, candidate);
31253
- try {
31254
- const content = await fs2.readFile(
31255
- candidatePath,
31256
- "utf8"
31257
- );
31258
- return {
31259
- targetPath: candidatePath,
31260
- content
31261
- };
31262
- } catch {
31263
- }
31264
- }
31265
- }
31266
- }
31267
- throw new WorkflowInputError(
31268
- `Workflow file not found: ${targetPath}`,
31269
- "workflow_file_path",
31270
- "not_found"
31271
- );
31272
- }
31585
+ if (entry.input_name === "lora_name" || entry.input_name.startsWith("strength_") || entry.class_type.includes("Lora")) {
31586
+ return "loras";
31273
31587
  }
31274
- if (args.workflow_id) {
31275
- const requestedId = args.workflow_id.trim();
31276
- if (requestedId.length === 0) {
31277
- throw new WorkflowInputError(
31278
- "Workflow ID cannot be empty.",
31279
- "workflow_id"
31280
- );
31281
- }
31282
- const objectInfoAvailability = await tryGetObjectInfo();
31283
- for (const dir of getWorkflowDirectories()) {
31284
- try {
31285
- const entries = await fs2.readdir(dir, { withFileTypes: true });
31286
- for (const entry of entries) {
31287
- if (!entry.isFile() || !entry.name.endsWith(".json")) {
31288
- continue;
31289
- }
31290
- const fullPath = path2.join(dir, entry.name);
31291
- const inspected = await inspectWorkflowFile(
31292
- fullPath,
31293
- objectInfoAvailability.objectInfo
31294
- );
31295
- if (!inspected) {
31296
- continue;
31297
- }
31298
- if (requestedId === inspected.id || requestedId === inspected.name || requestedId === entry.name.replace(/\.json$/, "")) {
31299
- return {
31300
- targetPath: fullPath,
31301
- content: inspected.content
31302
- };
31303
- }
31304
- }
31305
- } catch {
31306
- }
31307
- }
31308
- throw new WorkflowInputError(
31309
- `Workflow ID not found: ${args.workflow_id}`,
31310
- "workflow_id",
31311
- "not_found"
31312
- );
31588
+ if (entry.class_type.includes("ControlNet") || entry.class_type.includes("IPAdapter") || entry.class_type.includes("FreeU") || entry.class_type.includes("FaceDetailer")) {
31589
+ return "control";
31313
31590
  }
31314
- throw new WorkflowInputError(
31315
- "Must provide either workflow_file_path or workflow_id.",
31316
- "workflow_id"
31317
- );
31591
+ 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")) {
31592
+ return "output";
31593
+ }
31594
+ return "other";
31318
31595
  }
31319
- async function normalizeWorkflowPrompt(workflow, objectInfo) {
31320
- if (!isRecord(workflow)) {
31321
- throw new WorkflowInputError(
31322
- "Workflow must be a JSON object.",
31323
- "workflow"
31324
- );
31596
+ function getEditableInputPriority(entry) {
31597
+ const rolePriority = {
31598
+ positive_prompt: 300,
31599
+ negative_prompt: 295,
31600
+ prompt_text: 290,
31601
+ checkpoint: 260,
31602
+ seed: 250,
31603
+ steps: 240,
31604
+ cfg: 235,
31605
+ sampler: 230,
31606
+ scheduler: 225,
31607
+ width: 220,
31608
+ height: 219,
31609
+ batch_size: 218,
31610
+ denoise: 215,
31611
+ filename_prefix: 190,
31612
+ text: 180
31613
+ };
31614
+ let priority = rolePriority[entry.role ?? ""] ?? 0;
31615
+ if (entry.input_name === "lora_name") {
31616
+ priority = Math.max(priority, 170);
31325
31617
  }
31326
- let sourceFormat = "api";
31327
- let widgetPathMap = {};
31328
- let normalizedWorkflow = workflow;
31329
- if (isWebUIFormat(normalizedWorkflow)) {
31330
- if (!objectInfo) {
31331
- throw new WorkflowInputError(
31332
- "Web UI workflow normalization requires ComfyUI node definitions. Start the ComfyUI server or save the workflow in API format first.",
31333
- "workflow"
31334
- );
31618
+ if (entry.input_name.startsWith("strength_")) {
31619
+ priority = Math.max(priority, 160);
31620
+ }
31621
+ if (entry.class_type.includes("PromptSaver") || entry.class_type === "SaveImage") {
31622
+ priority -= 70;
31623
+ }
31624
+ if (entry.class_type === "PrimitiveStringMultiline" && (entry.role === "positive_prompt" || entry.role === "negative_prompt")) {
31625
+ priority += 30;
31626
+ }
31627
+ return priority;
31628
+ }
31629
+ function getHighSignalEditableInputs(editableInputs, limit = 12) {
31630
+ const seen = /* @__PURE__ */ new Set();
31631
+ const prioritized = [...editableInputs].sort((left, right) => {
31632
+ const priorityDiff = getEditableInputPriority(right) - getEditableInputPriority(left);
31633
+ if (priorityDiff !== 0) {
31634
+ return priorityDiff;
31335
31635
  }
31336
- sourceFormat = "webui";
31337
- const converted = convertWebUIToAPIWithMetadata(
31338
- normalizedWorkflow,
31339
- objectInfo
31636
+ return getPreferredOverridePath(left).localeCompare(
31637
+ getPreferredOverridePath(right),
31638
+ void 0,
31639
+ { numeric: true }
31340
31640
  );
31341
- normalizedWorkflow = converted.prompt;
31342
- widgetPathMap = converted.widgetPathMap;
31641
+ }).filter((entry) => {
31642
+ const preferredPath = getPreferredOverridePath(entry);
31643
+ if (seen.has(preferredPath)) {
31644
+ return false;
31645
+ }
31646
+ seen.add(preferredPath);
31647
+ return true;
31648
+ });
31649
+ return prioritized.slice(0, limit);
31650
+ }
31651
+ function summarizeEditableInputGroups(editableInputs) {
31652
+ const labels = {
31653
+ prompts: "Prompts",
31654
+ models: "Models and checkpoints",
31655
+ sampling: "Sampling",
31656
+ image: "Image size and batching",
31657
+ loras: "LoRAs and strengths",
31658
+ control: "Conditioning and adapters",
31659
+ output: "Output and persistence",
31660
+ other: "Other"
31661
+ };
31662
+ const summaries = /* @__PURE__ */ new Map();
31663
+ for (const entry of editableInputs) {
31664
+ const group = getEditableInputGroup(entry);
31665
+ const existing = summaries.get(group) ?? {
31666
+ group,
31667
+ label: labels[group],
31668
+ count: 0,
31669
+ sample_paths: []
31670
+ };
31671
+ existing.count += 1;
31672
+ if (existing.sample_paths.length < 3) {
31673
+ existing.sample_paths.push(getPreferredOverridePath(entry));
31674
+ }
31675
+ summaries.set(group, existing);
31343
31676
  }
31344
- const promptCandidate = isRecord(normalizedWorkflow.prompt) && normalizedWorkflow.prompt ? normalizedWorkflow.prompt : normalizedWorkflow;
31345
- if (!isRecord(promptCandidate)) {
31346
- throw new WorkflowInputError(
31347
- "Workflow prompt must be a JSON object.",
31348
- "workflow"
31349
- );
31677
+ const order = [
31678
+ "prompts",
31679
+ "models",
31680
+ "sampling",
31681
+ "image",
31682
+ "loras",
31683
+ "control",
31684
+ "output",
31685
+ "other"
31686
+ ];
31687
+ return order.map((group) => summaries.get(group)).filter(
31688
+ (summary) => Boolean(summary)
31689
+ );
31690
+ }
31691
+ function formatEditableInputGroupsText(groupSummaries) {
31692
+ if (groupSummaries.length === 0) {
31693
+ return "No editable input groups were detected.";
31350
31694
  }
31351
- return {
31352
- prompt: extractRunnablePromptNodes(promptCandidate),
31353
- sourceFormat,
31354
- widgetPathMap
31355
- };
31695
+ return formatListItems(groupSummaries, (summary) => {
31696
+ const examples = summary.sample_paths.length > 0 ? ` | examples: ${summary.sample_paths.join(", ")}` : "";
31697
+ return `${summary.label}: ${summary.count}${examples}`;
31698
+ });
31356
31699
  }
31357
- async function loadWorkflow(args, objectInfo) {
31358
- const { targetPath, content } = await resolveWorkflowReference(args);
31359
- let parsed;
31360
- try {
31361
- parsed = JSON.parse(content);
31362
- } catch (error48) {
31363
- throw new WorkflowInputError(
31364
- `Invalid JSON in ${targetPath}: ${error48 instanceof Error ? error48.message : String(error48)}`,
31365
- args.workflow_file_path ? "workflow_file_path" : "workflow_id"
31366
- );
31700
+ function buildOverrideExamples(editableInputs, limit = 5, options = {}) {
31701
+ const valueMode = options.valueMode ?? "full";
31702
+ const examples = {};
31703
+ const preferredPaths = [];
31704
+ for (const entry of getHighSignalEditableInputs(editableInputs, limit)) {
31705
+ const preferredPath = getPreferredOverridePath(entry);
31706
+ preferredPaths.push(preferredPath);
31707
+ if (valueMode === "keys") {
31708
+ continue;
31709
+ }
31710
+ examples[preferredPath] = valueMode === "preview" ? entry.value_preview : entry.value;
31367
31711
  }
31368
- const { prompt, sourceFormat, widgetPathMap } = await normalizeWorkflowPrompt(parsed, objectInfo);
31369
31712
  return {
31370
- targetPath,
31371
- prompt,
31372
- sourceFormat,
31373
- widgetPathMap
31713
+ examples,
31714
+ preferred_paths: preferredPaths
31374
31715
  };
31375
31716
  }
31376
- async function inspectWorkflowFile(fullPath, objectInfo) {
31377
- const content = await fs2.readFile(fullPath, "utf8");
31378
- const defaultName = path2.basename(fullPath, ".json");
31379
- const defaultId = getWorkflowId(fullPath);
31380
- let parsed;
31381
- try {
31382
- parsed = JSON.parse(content);
31383
- } catch {
31384
- return void 0;
31717
+ function formatOverrideExamplesText(examples) {
31718
+ return JSON.stringify(examples, null, 2);
31719
+ }
31720
+ function analyzeEditableInputs(prompt, objectInfo, widgetPathMap = {}) {
31721
+ const consumerMap = /* @__PURE__ */ new Map();
31722
+ for (const [nodeId, node] of Object.entries(prompt)) {
31723
+ for (const [inputName, value] of Object.entries(node.inputs)) {
31724
+ if (!isConnectionValue(value)) {
31725
+ continue;
31726
+ }
31727
+ const sourceNodeId = String(value[0]);
31728
+ const consumers = consumerMap.get(sourceNodeId) ?? [];
31729
+ consumers.push({
31730
+ inputName,
31731
+ classType: node.class_type,
31732
+ nodeId
31733
+ });
31734
+ consumerMap.set(sourceNodeId, consumers);
31735
+ }
31385
31736
  }
31386
- let name = defaultName;
31387
- if (isRecord(parsed)) {
31388
- if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
31389
- name = parsed.name;
31390
- } else if (isRecord(parsed.workflow) && typeof parsed.workflow.name === "string" && parsed.workflow.name.trim().length > 0) {
31391
- name = parsed.workflow.name;
31737
+ const editableInputs = [];
31738
+ for (const [nodeId, node] of Object.entries(prompt)) {
31739
+ for (const [inputName, value] of Object.entries(node.inputs)) {
31740
+ if (isConnectionValue(value)) {
31741
+ continue;
31742
+ }
31743
+ editableInputs.push({
31744
+ path: `${nodeId}.inputs.${inputName}`,
31745
+ node_id: nodeId,
31746
+ class_type: node.class_type,
31747
+ input_name: inputName,
31748
+ value,
31749
+ value_preview: formatValuePreview(value),
31750
+ role: inferEditableRole(
31751
+ nodeId,
31752
+ node.class_type,
31753
+ inputName,
31754
+ consumerMap
31755
+ ),
31756
+ aliases: [],
31757
+ expected_type: getExpectedTypeName(
31758
+ getInputDefinition(objectInfo, prompt, nodeId, inputName)
31759
+ ),
31760
+ ...getInputOptionsSummary(
31761
+ getInputDefinition(objectInfo, prompt, nodeId, inputName)
31762
+ )
31763
+ });
31392
31764
  }
31393
31765
  }
31394
- if (isWebUIFormat(parsed)) {
31395
- if (!objectInfo) {
31396
- return {
31397
- id: defaultId,
31398
- name,
31399
- path: fullPath,
31400
- source_format: "webui",
31401
- validation_status: "conversion_requires_node_schemas",
31402
- content,
31403
- isWebUI: true
31404
- };
31405
- }
31406
- try {
31407
- await normalizeWorkflowPrompt(parsed, objectInfo);
31408
- return {
31409
- id: defaultId,
31410
- name,
31411
- path: fullPath,
31412
- source_format: "webui",
31413
- validation_status: "conversion_validated",
31414
- content,
31415
- isWebUI: true
31416
- };
31417
- } catch (error48) {
31418
- return {
31419
- id: defaultId,
31420
- name,
31421
- path: fullPath,
31422
- source_format: "webui",
31423
- validation_status: "conversion_failed",
31424
- content,
31425
- error: summarizeErrorMessage(error48),
31426
- isWebUI: true
31427
- };
31428
- }
31429
- }
31430
- try {
31431
- await normalizeWorkflowPrompt(parsed);
31432
- return {
31433
- id: defaultId,
31434
- name,
31435
- path: fullPath,
31436
- source_format: "api",
31437
- validation_status: "ready",
31438
- content,
31439
- isWebUI: false
31440
- };
31441
- } catch {
31442
- return void 0;
31443
- }
31766
+ const withAliases = editableInputs.map((entry) => ({
31767
+ ...entry,
31768
+ aliases: getEditableAliases(
31769
+ entry.path,
31770
+ entry.node_id,
31771
+ entry.input_name,
31772
+ entry.role,
31773
+ widgetPathMap
31774
+ )
31775
+ }));
31776
+ applyUniqueGlobalRoleAliases(withAliases);
31777
+ return withAliases.sort(
31778
+ (left, right) => left.path.localeCompare(right.path, void 0, { numeric: true })
31779
+ );
31444
31780
  }
31445
- function deriveSavedWorkflowFileName(sourcePath, requestedName) {
31446
- if (requestedName) {
31447
- return normalizeWorkflowFileName(requestedName);
31781
+ function formatWorkflowInspectionText(params) {
31782
+ const textSections = [...params.introLines];
31783
+ if (params.warning) {
31784
+ textSections.push(params.warning);
31448
31785
  }
31449
- if (sourcePath) {
31450
- return normalizeWorkflowFileName(path2.basename(sourcePath));
31786
+ textSections.push(
31787
+ `High-signal overrides (${params.highSignalInputs.length}):`,
31788
+ formatEditableInputsText(params.highSignalInputs, {
31789
+ includeOptions: true
31790
+ }),
31791
+ "Editable input groups:",
31792
+ formatEditableInputGroupsText(params.editableInputGroups)
31793
+ );
31794
+ if (params.verbose) {
31795
+ textSections.push(
31796
+ `Editable inputs (${params.editableInputs.length} total):`,
31797
+ formatEditableInputsText(params.editableInputs)
31798
+ );
31799
+ } else if (params.editableInputs.length > params.highSignalInputs.length) {
31800
+ textSections.push(
31801
+ `Pass verbose=true to list all ${params.editableInputs.length} editable inputs.`
31802
+ );
31451
31803
  }
31452
- return `workflow-${Date.now()}.json`;
31453
- }
31454
- function getWorkflowId(absolutePath) {
31455
- return crypto.createHash("sha256").update(absolutePath).digest("hex").substring(0, 8);
31804
+ textSections.push(
31805
+ `Copy-ready override keys: ${params.preferredOverridePaths.length > 0 ? params.preferredOverridePaths.join(", ") : "(none)"}`,
31806
+ "Example values (truncated):",
31807
+ formatOverrideExamplesText(params.overrideExamples)
31808
+ );
31809
+ return textSections.join("\n");
31456
31810
  }
31457
-
31458
- // src/workflow/editable.ts
31459
- function isConnectionValue(value) {
31460
- return Array.isArray(value) && value.length >= 2 && (typeof value[0] === "string" || typeof value[0] === "number") && typeof value[1] === "number";
31811
+ function formatEditableInputsText(editableInputs, options = {}) {
31812
+ if (editableInputs.length === 0) {
31813
+ return "No literal editable inputs were detected.";
31814
+ }
31815
+ return formatListItems(editableInputs, (entry) => {
31816
+ const role = entry.role ? ` [${entry.role}]` : "";
31817
+ const classType = entry.class_type ? ` (${entry.class_type})` : "";
31818
+ const expectedType = entry.expected_type ? ` <${entry.expected_type}>` : "";
31819
+ const aliasSuffix = entry.aliases.length > 0 ? ` | aliases: ${entry.aliases.join(", ")}` : "";
31820
+ const optionsSuffix = options.includeOptions ? formatEditableInputOptionsText(entry) : "";
31821
+ return `${entry.path}${role}${classType}${expectedType}: ${entry.value_preview}${aliasSuffix}${optionsSuffix}`;
31822
+ });
31461
31823
  }
31462
- function inferEditableRole(nodeId, classType, inputName, consumerMap) {
31463
- if (classType === "CLIPTextEncode" && inputName === "text") {
31464
- const consumers = consumerMap.get(nodeId) ?? [];
31465
- if (consumers.some((consumer) => consumer.inputName === "positive")) {
31466
- return "positive_prompt";
31824
+ function formatInputDefinitionSummary(inputName, inputDefinition) {
31825
+ const [typeData, extraInfo] = inputDefinition;
31826
+ const typeLabel = Array.isArray(typeData) ? `COMBO (${typeData.length} options)` : String(typeData).toUpperCase();
31827
+ const details = [];
31828
+ if (Array.isArray(typeData)) {
31829
+ details.push(
31830
+ `options: ${typeData.slice(0, 5).map((option) => JSON.stringify(option)).join(", ")}${typeData.length > 5 ? ", ..." : ""}`
31831
+ );
31832
+ }
31833
+ if (isRecord(extraInfo)) {
31834
+ if (extraInfo.default !== void 0) {
31835
+ details.push(`default: ${formatValuePreview(extraInfo.default)}`);
31467
31836
  }
31468
- if (consumers.some((consumer) => consumer.inputName === "negative")) {
31469
- return "negative_prompt";
31837
+ if (typeof extraInfo.min === "number") {
31838
+ details.push(`min: ${extraInfo.min}`);
31470
31839
  }
31471
- return "prompt_text";
31472
- }
31473
- if (inputName === "value") {
31474
- const consumers = consumerMap.get(nodeId) ?? [];
31475
- const textConsumers = consumers.filter(
31476
- (consumer) => consumer.classType === "CLIPTextEncode" && consumer.inputName === "text"
31477
- );
31478
- if (textConsumers.length > 0) {
31479
- for (const consumer of textConsumers) {
31480
- const clipConsumers = consumerMap.get(consumer.nodeId) ?? [];
31481
- if (clipConsumers.some(
31482
- (clipConsumer) => clipConsumer.inputName === "positive"
31483
- )) {
31484
- return "positive_prompt";
31485
- }
31486
- if (clipConsumers.some(
31487
- (clipConsumer) => clipConsumer.inputName === "negative"
31488
- )) {
31489
- return "negative_prompt";
31490
- }
31491
- }
31492
- return "prompt_text";
31840
+ if (typeof extraInfo.max === "number") {
31841
+ details.push(`max: ${extraInfo.max}`);
31493
31842
  }
31494
31843
  }
31495
- switch (inputName) {
31496
- case "seed":
31497
- return "seed";
31498
- case "steps":
31499
- return "steps";
31500
- case "cfg":
31501
- return "cfg";
31502
- case "sampler_name":
31503
- return "sampler";
31504
- case "scheduler":
31505
- return "scheduler";
31506
- case "denoise":
31507
- return "denoise";
31508
- case "width":
31509
- return "width";
31510
- case "height":
31511
- return "height";
31512
- case "batch_size":
31513
- return "batch_size";
31514
- case "ckpt_name":
31515
- return "checkpoint";
31516
- case "filename_prefix":
31517
- return "filename_prefix";
31518
- case "text":
31519
- return "text";
31520
- default:
31521
- return void 0;
31522
- }
31844
+ return `${inputName} <${typeLabel}>${details.length > 0 ? ` | ${details.join(" | ")}` : ""}`;
31523
31845
  }
31524
- function getEditableAliases(canonicalPath, nodeId, inputName, role, widgetPathMap) {
31525
- const aliases = /* @__PURE__ */ new Set([`${nodeId}.${inputName}`]);
31526
- if (role) {
31527
- aliases.add(`${nodeId}.${role}`);
31528
- }
31529
- for (const [widgetPath, resolvedPath] of Object.entries(widgetPathMap)) {
31530
- if (resolvedPath === canonicalPath) {
31531
- aliases.add(widgetPath);
31532
- }
31846
+ function formatNodeDefinitionText(nodeClass, node) {
31847
+ const requiredInputs = Object.entries(node.input.required ?? {});
31848
+ const optionalInputs = Object.entries(node.input.optional ?? {});
31849
+ const lines = [
31850
+ `Node: ${nodeClass}`,
31851
+ `Display name: ${node.display_name || node.name || nodeClass}`,
31852
+ `Category: ${node.category || "uncategorized"}`
31853
+ ];
31854
+ if (typeof node.description === "string" && node.description.trim().length > 0) {
31855
+ lines.push(`Description: ${node.description.trim()}`);
31533
31856
  }
31534
- aliases.delete(canonicalPath);
31535
- return [...aliases];
31857
+ lines.push(
31858
+ `Required inputs (${requiredInputs.length}): ${requiredInputs.length > 0 ? requiredInputs.map(
31859
+ ([inputName, inputDefinition]) => formatInputDefinitionSummary(
31860
+ inputName,
31861
+ inputDefinition
31862
+ )
31863
+ ).join("\n") : "None."}`
31864
+ );
31865
+ lines.push(
31866
+ `Optional inputs (${optionalInputs.length}): ${optionalInputs.length > 0 ? optionalInputs.map(
31867
+ ([inputName, inputDefinition]) => formatInputDefinitionSummary(
31868
+ inputName,
31869
+ inputDefinition
31870
+ )
31871
+ ).join("\n") : "None."}`
31872
+ );
31873
+ lines.push(`Outputs: ${node.output.join(", ") || "None."}`);
31874
+ return lines.join("\n");
31536
31875
  }
31537
- function buildEditableInputIndex(editableInputs) {
31538
- const entriesByPath = /* @__PURE__ */ new Map();
31539
- const aliasToPath = /* @__PURE__ */ new Map();
31540
- const ambiguousAliases = /* @__PURE__ */ new Map();
31541
- const pathsByNode = /* @__PURE__ */ new Map();
31542
- for (const entry of editableInputs) {
31543
- entriesByPath.set(entry.path, entry);
31544
- const nodeEntries = pathsByNode.get(entry.node_id) ?? [];
31545
- nodeEntries.push(entry);
31546
- pathsByNode.set(entry.node_id, nodeEntries);
31547
- for (const alias of [entry.path, ...entry.aliases]) {
31548
- const existing = aliasToPath.get(alias);
31549
- if (!existing) {
31550
- aliasToPath.set(alias, entry.path);
31551
- continue;
31552
- }
31553
- if (existing === entry.path) {
31554
- continue;
31555
- }
31556
- const ambiguous = new Set(
31557
- ambiguousAliases.get(alias) ?? [existing]
31558
- );
31559
- ambiguous.add(entry.path);
31560
- ambiguousAliases.set(alias, [...ambiguous].sort());
31561
- aliasToPath.delete(alias);
31562
- }
31876
+ function formatNodeListText(classes, rawInfo, pagination) {
31877
+ const text = `Available nodes (${pagination.total} total):
31878
+ ` + formatListItems(classes, (nodeClass) => {
31879
+ const node = rawInfo[nodeClass];
31880
+ const displayName = node.display_name && node.display_name !== nodeClass ? ` \u2014 ${node.display_name}` : "";
31881
+ const category = node.category ? ` | category: ${node.category}` : "";
31882
+ const description = typeof node.description === "string" && node.description.trim().length > 0 ? ` | ${node.description.trim()}` : "";
31883
+ return `${nodeClass}${displayName}${category}${description}`;
31884
+ });
31885
+ if (!pagination.hasMore) {
31886
+ return text;
31563
31887
  }
31564
- return {
31565
- entriesByPath,
31566
- aliasToPath,
31567
- ambiguousAliases,
31568
- pathsByNode
31569
- };
31888
+ return `${text}
31889
+
31890
+ ${formatPaginationFooter(
31891
+ pagination.offset,
31892
+ pagination.limit,
31893
+ pagination.total
31894
+ )}`;
31570
31895
  }
31571
- function getPathSegmentHelp(prompt, path4) {
31572
- const parts = path4.split(".");
31573
- if (parts.length === 0 || parts[0].length === 0) {
31574
- return void 0;
31575
- }
31576
- const nodeId = parts[0];
31577
- return {
31578
- nodeId,
31579
- nodeExists: isRecord(prompt[nodeId])
31580
- };
31581
- }
31582
- function describeAvailableNodePaths(editableIndex, nodeId) {
31583
- if (!editableIndex) {
31584
- return "";
31585
- }
31586
- const entries = editableIndex.pathsByNode.get(nodeId) ?? [];
31587
- if (entries.length === 0) {
31588
- return "";
31589
- }
31590
- return entries.map((entry) => {
31591
- const aliasSuffix = entry.aliases.length > 0 ? ` (aliases: ${entry.aliases.join(", ")})` : "";
31592
- return `${entry.path}${aliasSuffix}`;
31593
- }).join("\n");
31896
+
31897
+ // src/workflow/load.ts
31898
+ import * as crypto from "node:crypto";
31899
+ import * as fs2 from "node:fs/promises";
31900
+ import { homedir } from "node:os";
31901
+ import path2 from "node:path";
31902
+
31903
+ // src/converter.ts
31904
+ var WIDGET_TYPES = /* @__PURE__ */ new Set(["INT", "FLOAT", "STRING", "BOOLEAN"]);
31905
+ var UI_CONTROL_VALUES = /* @__PURE__ */ new Set([
31906
+ "fixed",
31907
+ "increment",
31908
+ "decrement",
31909
+ "randomize"
31910
+ ]);
31911
+ function isRecord2(value) {
31912
+ return typeof value === "object" && value !== null;
31594
31913
  }
31595
- function getPreferredOverridePath(entry) {
31596
- const roleAlias = entry.role ? `${entry.node_id}.${entry.role}` : void 0;
31597
- if (roleAlias && entry.aliases.includes(roleAlias)) {
31598
- return roleAlias;
31599
- }
31600
- const inputAlias = `${entry.node_id}.${entry.input_name}`;
31601
- if (entry.aliases.includes(inputAlias)) {
31602
- return inputAlias;
31603
- }
31604
- const nonWidgetAlias = entry.aliases.find(
31605
- (alias) => !alias.includes(".widgets[")
31606
- );
31607
- return nonWidgetAlias ?? entry.path;
31914
+ function isWebUIFormat(json2) {
31915
+ return isRecord2(json2) && Array.isArray(json2.nodes) && Array.isArray(json2.links);
31608
31916
  }
31609
- function formatEditableInputOptionsText(entry) {
31610
- if (entry.options_count === void 0 || entry.options_preview === void 0 || entry.options_preview.length === 0) {
31611
- return "";
31612
- }
31613
- const preview = entry.options_preview.map((option) => formatValuePreview(option)).join(", ");
31614
- const truncatedSuffix = entry.options_truncated ? ", ..." : "";
31615
- return ` | options: ${preview}${truncatedSuffix} (${entry.options_count} total)`;
31917
+ function isLinkData(link) {
31918
+ return Array.isArray(link) && link.length >= 6 && typeof link[0] === "number" && typeof link[1] === "number" && typeof link[2] === "number" && typeof link[3] === "number" && typeof link[4] === "number" && typeof link[5] === "string";
31616
31919
  }
31617
- function getEditableInputGroup(entry) {
31618
- if (entry.role === "positive_prompt" || entry.role === "negative_prompt" || entry.role === "prompt_text") {
31619
- return "prompts";
31620
- }
31621
- 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")) {
31622
- return "models";
31623
- }
31624
- if (entry.role === "seed" || entry.role === "steps" || entry.role === "cfg" || entry.role === "sampler" || entry.role === "scheduler" || entry.role === "denoise" || entry.class_type.includes("Sampler")) {
31625
- return "sampling";
31626
- }
31627
- if (entry.role === "width" || entry.role === "height" || entry.role === "batch_size") {
31628
- return "image";
31629
- }
31630
- if (entry.input_name === "lora_name" || entry.input_name.startsWith("strength_") || entry.class_type.includes("Lora")) {
31631
- return "loras";
31920
+ function getOrderedInputEntries(group, orderedNames) {
31921
+ if (!group) {
31922
+ return [];
31632
31923
  }
31633
- if (entry.class_type.includes("ControlNet") || entry.class_type.includes("IPAdapter") || entry.class_type.includes("FreeU") || entry.class_type.includes("FaceDetailer")) {
31634
- return "control";
31924
+ const seen = /* @__PURE__ */ new Set();
31925
+ const orderedEntries = [];
31926
+ for (const inputName of orderedNames ?? []) {
31927
+ const inputDefinition = group[inputName];
31928
+ if (!inputDefinition) {
31929
+ continue;
31930
+ }
31931
+ orderedEntries.push([inputName, inputDefinition]);
31932
+ seen.add(inputName);
31635
31933
  }
31636
- 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")) {
31637
- return "output";
31934
+ for (const [inputName, inputDefinition] of Object.entries(group)) {
31935
+ if (seen.has(inputName)) {
31936
+ continue;
31937
+ }
31938
+ orderedEntries.push([inputName, inputDefinition]);
31638
31939
  }
31639
- return "other";
31940
+ return orderedEntries;
31640
31941
  }
31641
- function getEditableInputPriority(entry) {
31642
- const rolePriority = {
31643
- positive_prompt: 300,
31644
- negative_prompt: 295,
31645
- prompt_text: 290,
31646
- checkpoint: 260,
31647
- seed: 250,
31648
- steps: 240,
31649
- cfg: 235,
31650
- sampler: 230,
31651
- scheduler: 225,
31652
- width: 220,
31653
- height: 219,
31654
- batch_size: 218,
31655
- denoise: 215,
31656
- filename_prefix: 190,
31657
- text: 180
31658
- };
31659
- let priority = rolePriority[entry.role ?? ""] ?? 0;
31660
- if (entry.input_name === "lora_name") {
31661
- priority = Math.max(priority, 170);
31942
+ function getWidgetTypeName(inputTypeData) {
31943
+ return Array.isArray(inputTypeData) ? "COMBO" : String(inputTypeData).toUpperCase();
31944
+ }
31945
+ function isWidgetInput(inputTypeData) {
31946
+ const typeName = getWidgetTypeName(inputTypeData);
31947
+ return typeName === "COMBO" || WIDGET_TYPES.has(typeName);
31948
+ }
31949
+ function isSkippableUiArtifact(value, expectedTypeName) {
31950
+ if (expectedTypeName !== "STRING" && typeof value === "string" && UI_CONTROL_VALUES.has(value)) {
31951
+ return true;
31662
31952
  }
31663
- if (entry.input_name.startsWith("strength_")) {
31664
- priority = Math.max(priority, 160);
31953
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31954
+ }
31955
+ function normalizeWidgetValue(value, typeName, extraInfo) {
31956
+ const defaultValue = extraInfo?.default;
31957
+ const shouldUseDefaultForNumeric = (typeName === "INT" || typeName === "FLOAT") && (value === "" || value === void 0 || value === null);
31958
+ if (shouldUseDefaultForNumeric && defaultValue !== void 0) {
31959
+ return defaultValue;
31665
31960
  }
31666
- if (entry.class_type.includes("PromptSaver") || entry.class_type === "SaveImage") {
31667
- priority -= 70;
31961
+ return value;
31962
+ }
31963
+ function convertWebUIToAPIWithMetadata(webUIJson, objectInfo) {
31964
+ const prompt = {};
31965
+ const widgetPathMap = {};
31966
+ const links = webUIJson.links || [];
31967
+ const nodes = webUIJson.nodes || [];
31968
+ const serverNodeIds = /* @__PURE__ */ new Set();
31969
+ for (const node of nodes) {
31970
+ if (!node.id || !node.type || !objectInfo[node.type]) {
31971
+ continue;
31972
+ }
31973
+ serverNodeIds.add(String(node.id));
31668
31974
  }
31669
- if (entry.class_type === "PrimitiveStringMultiline" && (entry.role === "positive_prompt" || entry.role === "negative_prompt")) {
31670
- priority += 30;
31975
+ const linkMap = /* @__PURE__ */ new Map();
31976
+ for (const link of links) {
31977
+ if (isLinkData(link) && serverNodeIds.has(String(link[3]))) {
31978
+ linkMap.set(link[0], link);
31979
+ }
31671
31980
  }
31672
- return priority;
31673
- }
31674
- function getHighSignalEditableInputs(editableInputs, limit = 12) {
31675
- const seen = /* @__PURE__ */ new Set();
31676
- const prioritized = [...editableInputs].sort((left, right) => {
31677
- const priorityDiff = getEditableInputPriority(right) - getEditableInputPriority(left);
31678
- if (priorityDiff !== 0) {
31679
- return priorityDiff;
31981
+ for (const node of nodes) {
31982
+ if (!node.id || !node.type) continue;
31983
+ const objInfo = objectInfo[node.type];
31984
+ if (!objInfo) continue;
31985
+ const nodeIdStr = String(node.id);
31986
+ const promptNode = {
31987
+ class_type: node.type,
31988
+ inputs: {}
31989
+ };
31990
+ const connectedInputNames = /* @__PURE__ */ new Set();
31991
+ if (Array.isArray(node.inputs)) {
31992
+ for (const input of node.inputs) {
31993
+ if (input.name && input.link !== null && input.link !== void 0) {
31994
+ const linkData = linkMap.get(input.link);
31995
+ if (Array.isArray(linkData) && linkData.length >= 6 && String(linkData[3]) === nodeIdStr) {
31996
+ const originNodeId = String(linkData[1]);
31997
+ const originSlot = linkData[2];
31998
+ promptNode.inputs[input.name] = [
31999
+ originNodeId,
32000
+ originSlot
32001
+ ];
32002
+ connectedInputNames.add(input.name);
32003
+ }
32004
+ }
32005
+ }
31680
32006
  }
31681
- return getPreferredOverridePath(left).localeCompare(
31682
- getPreferredOverridePath(right),
31683
- void 0,
31684
- { numeric: true }
31685
- );
31686
- }).filter((entry) => {
31687
- const preferredPath = getPreferredOverridePath(entry);
31688
- if (seen.has(preferredPath)) {
31689
- return false;
32007
+ if (objInfo) {
32008
+ const widgets = Array.isArray(node.widgets_values) ? node.widgets_values : [];
32009
+ let widgetIndex = 0;
32010
+ const processInputGroup = (group, orderedNames) => {
32011
+ for (const [inputName, inputDataRaw] of getOrderedInputEntries(
32012
+ group,
32013
+ orderedNames
32014
+ )) {
32015
+ const [inputTypeData, inputExtraInfo] = inputDataRaw;
32016
+ if (!isWidgetInput(inputTypeData)) {
32017
+ continue;
32018
+ }
32019
+ const typeName = getWidgetTypeName(inputTypeData);
32020
+ while (widgetIndex < widgets.length && isSkippableUiArtifact(widgets[widgetIndex], typeName)) {
32021
+ widgetIndex++;
32022
+ }
32023
+ const isConnectedWidget = connectedInputNames.has(inputName);
32024
+ if (widgetIndex >= widgets.length) {
32025
+ if (!isConnectedWidget && inputExtraInfo?.default !== void 0) {
32026
+ promptNode.inputs[inputName] = inputExtraInfo.default;
32027
+ }
32028
+ continue;
32029
+ }
32030
+ const widgetSourceIndex = widgetIndex;
32031
+ const value = normalizeWidgetValue(
32032
+ widgets[widgetSourceIndex],
32033
+ typeName,
32034
+ inputExtraInfo
32035
+ );
32036
+ widgetIndex++;
32037
+ if (!isConnectedWidget) {
32038
+ promptNode.inputs[inputName] = value;
32039
+ widgetPathMap[`${nodeIdStr}.widgets[${widgetSourceIndex}]`] = `${nodeIdStr}.inputs.${inputName}`;
32040
+ }
32041
+ }
32042
+ };
32043
+ processInputGroup(
32044
+ objInfo.input?.required,
32045
+ objInfo.input_order?.required
32046
+ );
32047
+ processInputGroup(
32048
+ objInfo.input?.optional,
32049
+ objInfo.input_order?.optional
32050
+ );
31690
32051
  }
31691
- seen.add(preferredPath);
31692
- return true;
31693
- });
31694
- return prioritized.slice(0, limit);
31695
- }
31696
- function summarizeEditableInputGroups(editableInputs) {
31697
- const labels = {
31698
- prompts: "Prompts",
31699
- models: "Models and checkpoints",
31700
- sampling: "Sampling",
31701
- image: "Image size and batching",
31702
- loras: "LoRAs and strengths",
31703
- control: "Conditioning and adapters",
31704
- output: "Output and persistence",
31705
- other: "Other"
32052
+ prompt[nodeIdStr] = promptNode;
32053
+ }
32054
+ return {
32055
+ prompt,
32056
+ widgetPathMap
31706
32057
  };
31707
- const summaries = /* @__PURE__ */ new Map();
31708
- for (const entry of editableInputs) {
31709
- const group = getEditableInputGroup(entry);
31710
- const existing = summaries.get(group) ?? {
31711
- group,
31712
- label: labels[group],
31713
- count: 0,
31714
- sample_paths: []
31715
- };
31716
- existing.count += 1;
31717
- if (existing.sample_paths.length < 3) {
31718
- existing.sample_paths.push(getPreferredOverridePath(entry));
32058
+ }
32059
+
32060
+ // src/workflow/load.ts
32061
+ function isComfyPromptNode(value) {
32062
+ return isRecord(value) && typeof value.class_type === "string" && isRecord(value.inputs);
32063
+ }
32064
+ function isUiOnlyPromptNode(nodeId, node) {
32065
+ return nodeId === "meta" || ["Note", "MarkdownNote", "PrimitiveNode"].includes(node.class_type);
32066
+ }
32067
+ function extractRunnablePromptNodes(prompt) {
32068
+ const runnablePrompt = {};
32069
+ for (const [nodeId, rawNode] of Object.entries(prompt)) {
32070
+ if (!isComfyPromptNode(rawNode) || isUiOnlyPromptNode(nodeId, rawNode)) {
32071
+ continue;
31719
32072
  }
31720
- summaries.set(group, existing);
32073
+ runnablePrompt[nodeId] = rawNode;
31721
32074
  }
31722
- const order = [
31723
- "prompts",
31724
- "models",
31725
- "sampling",
31726
- "image",
31727
- "loras",
31728
- "control",
31729
- "output",
31730
- "other"
32075
+ if (Object.keys(runnablePrompt).length === 0) {
32076
+ throw new WorkflowInputError(
32077
+ "Workflow prompt does not contain any runnable ComfyUI nodes.",
32078
+ "workflow"
32079
+ );
32080
+ }
32081
+ return runnablePrompt;
32082
+ }
32083
+ function getWorkflowDirectories(extraPath) {
32084
+ const directories = [
32085
+ process.cwd(),
32086
+ path2.join(process.cwd(), "workflows"),
32087
+ path2.join(homedir(), ".fre4x-comfyui", "workflows"),
32088
+ path2.join(
32089
+ homedir(),
32090
+ "comfy",
32091
+ "ComfyUI",
32092
+ "user",
32093
+ "default",
32094
+ "workflows"
32095
+ )
32096
+ ];
32097
+ if (extraPath) {
32098
+ directories.push(extraPath);
32099
+ }
32100
+ return [
32101
+ ...new Set(directories.map((directory) => path2.resolve(directory)))
31731
32102
  ];
31732
- return order.map((group) => summaries.get(group)).filter(
31733
- (summary) => Boolean(summary)
31734
- );
31735
32103
  }
31736
- function formatEditableInputGroupsText(groupSummaries) {
31737
- if (groupSummaries.length === 0) {
31738
- return "No editable input groups were detected.";
32104
+ function getWorkflowNameCandidates(name) {
32105
+ const trimmed = name.trim();
32106
+ if (trimmed.length === 0) {
32107
+ return [];
31739
32108
  }
31740
- return formatListItems(groupSummaries, (summary) => {
31741
- const examples = summary.sample_paths.length > 0 ? ` | examples: ${summary.sample_paths.join(", ")}` : "";
31742
- return `${summary.label}: ${summary.count}${examples}`;
31743
- });
32109
+ if (trimmed.endsWith(".json")) {
32110
+ return [trimmed];
32111
+ }
32112
+ return [trimmed, `${trimmed}.json`];
31744
32113
  }
31745
- function buildOverrideExamples(editableInputs, limit = 5) {
31746
- const examples = {};
31747
- const preferredPaths = [];
31748
- for (const entry of getHighSignalEditableInputs(editableInputs, limit)) {
31749
- const preferredPath = getPreferredOverridePath(entry);
31750
- examples[preferredPath] = entry.value;
31751
- preferredPaths.push(preferredPath);
32114
+ function normalizeWorkflowFileName(fileName) {
32115
+ const trimmed = fileName.trim();
32116
+ if (trimmed.length === 0) {
32117
+ throw new WorkflowInputError(
32118
+ "Filename cannot be empty.",
32119
+ "output_file_name"
32120
+ );
31752
32121
  }
31753
- return {
31754
- examples,
31755
- preferred_paths: preferredPaths
31756
- };
32122
+ if (path2.isAbsolute(trimmed)) {
32123
+ throw new WorkflowInputError(
32124
+ "Absolute paths are not allowed.",
32125
+ "output_file_name"
32126
+ );
32127
+ }
32128
+ return trimmed.endsWith(".json") ? trimmed : `${trimmed}.json`;
31757
32129
  }
31758
- function formatOverrideExamplesText(examples) {
31759
- return JSON.stringify(examples, null, 2);
32130
+ function getWorkflowReference(args) {
32131
+ const explicitRefs = [
32132
+ args.workflow_ref?.trim(),
32133
+ args.workflow_file_path?.trim(),
32134
+ args.workflow_id?.trim()
32135
+ ].filter((value) => Boolean(value));
32136
+ const uniqueRefs = [...new Set(explicitRefs)];
32137
+ if (uniqueRefs.length > 1) {
32138
+ throw new WorkflowInputError(
32139
+ "Provide only one of workflow_ref, workflow_file_path, or workflow_id.",
32140
+ "workflow_ref"
32141
+ );
32142
+ }
32143
+ if (uniqueRefs.length === 1) {
32144
+ return uniqueRefs[0];
32145
+ }
32146
+ const defaultWorkflow = process.env.COMFYUI_DEFAULT_WORKFLOW?.trim();
32147
+ return defaultWorkflow && defaultWorkflow.length > 0 ? defaultWorkflow : void 0;
31760
32148
  }
31761
- function analyzeEditableInputs(prompt, objectInfo, widgetPathMap = {}) {
31762
- const consumerMap = /* @__PURE__ */ new Map();
31763
- for (const [nodeId, node] of Object.entries(prompt)) {
31764
- for (const [inputName, value] of Object.entries(node.inputs)) {
31765
- if (!isConnectionValue(value)) {
31766
- continue;
32149
+ async function resolveWorkflowReference(args) {
32150
+ const requested = getWorkflowReference(args);
32151
+ if (!requested) {
32152
+ throw new WorkflowInputError(
32153
+ "Provide workflow_ref, a stored workflow path/id, or set COMFYUI_DEFAULT_WORKFLOW.",
32154
+ "workflow_ref"
32155
+ );
32156
+ }
32157
+ try {
32158
+ const resolvedPath = path2.resolve(requested);
32159
+ const content = await fs2.readFile(resolvedPath, "utf8");
32160
+ return {
32161
+ targetPath: resolvedPath,
32162
+ content
32163
+ };
32164
+ } catch {
32165
+ }
32166
+ if (path2.basename(requested) === requested) {
32167
+ const candidates = getWorkflowNameCandidates(requested);
32168
+ for (const dir of getWorkflowDirectories()) {
32169
+ for (const candidate of candidates) {
32170
+ const candidatePath = path2.join(dir, candidate);
32171
+ try {
32172
+ const content = await fs2.readFile(candidatePath, "utf8");
32173
+ return {
32174
+ targetPath: candidatePath,
32175
+ content
32176
+ };
32177
+ } catch {
32178
+ }
31767
32179
  }
31768
- const sourceNodeId = String(value[0]);
31769
- const consumers = consumerMap.get(sourceNodeId) ?? [];
31770
- consumers.push({
31771
- inputName,
31772
- classType: node.class_type,
31773
- nodeId
31774
- });
31775
- consumerMap.set(sourceNodeId, consumers);
31776
32180
  }
31777
32181
  }
31778
- const editableInputs = [];
31779
- for (const [nodeId, node] of Object.entries(prompt)) {
31780
- for (const [inputName, value] of Object.entries(node.inputs)) {
31781
- if (isConnectionValue(value)) {
31782
- continue;
32182
+ const objectInfoAvailability = await tryGetObjectInfo();
32183
+ for (const dir of getWorkflowDirectories()) {
32184
+ try {
32185
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
32186
+ for (const entry of entries) {
32187
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
32188
+ continue;
32189
+ }
32190
+ const fullPath = path2.join(dir, entry.name);
32191
+ const inspected = await inspectWorkflowFile(
32192
+ fullPath,
32193
+ objectInfoAvailability.objectInfo
32194
+ );
32195
+ if (!inspected) {
32196
+ continue;
32197
+ }
32198
+ if (requested === inspected.id || requested === inspected.name || requested === entry.name.replace(/\.json$/, "")) {
32199
+ return {
32200
+ targetPath: fullPath,
32201
+ content: inspected.content
32202
+ };
32203
+ }
31783
32204
  }
31784
- editableInputs.push({
31785
- path: `${nodeId}.inputs.${inputName}`,
31786
- node_id: nodeId,
31787
- class_type: node.class_type,
31788
- input_name: inputName,
31789
- value,
31790
- value_preview: formatValuePreview(value),
31791
- role: inferEditableRole(
31792
- nodeId,
31793
- node.class_type,
31794
- inputName,
31795
- consumerMap
31796
- ),
31797
- aliases: [],
31798
- expected_type: getExpectedTypeName(
31799
- getInputDefinition(objectInfo, prompt, nodeId, inputName)
31800
- ),
31801
- ...getInputOptionsSummary(
31802
- getInputDefinition(objectInfo, prompt, nodeId, inputName)
31803
- )
31804
- });
32205
+ } catch {
31805
32206
  }
31806
32207
  }
31807
- return editableInputs.map((entry) => ({
31808
- ...entry,
31809
- aliases: getEditableAliases(
31810
- entry.path,
31811
- entry.node_id,
31812
- entry.input_name,
31813
- entry.role,
31814
- widgetPathMap
31815
- )
31816
- })).sort(
31817
- (left, right) => left.path.localeCompare(right.path, void 0, { numeric: true })
32208
+ throw new WorkflowInputError(
32209
+ path2.isAbsolute(requested) || requested.includes(path2.sep) ? `Workflow file not found: ${requested}` : `Workflow not found: ${requested}`,
32210
+ args.workflow_ref ? "workflow_ref" : args.workflow_file_path ? "workflow_file_path" : "workflow_id",
32211
+ "not_found"
31818
32212
  );
31819
32213
  }
31820
- function formatEditableInputsText(editableInputs, options = {}) {
31821
- if (editableInputs.length === 0) {
31822
- return "No literal editable inputs were detected.";
32214
+ async function normalizeWorkflowPrompt(workflow, objectInfo) {
32215
+ if (!isRecord(workflow)) {
32216
+ throw new WorkflowInputError(
32217
+ "Workflow must be a JSON object.",
32218
+ "workflow"
32219
+ );
31823
32220
  }
31824
- return formatListItems(editableInputs, (entry) => {
31825
- const role = entry.role ? ` [${entry.role}]` : "";
31826
- const classType = entry.class_type ? ` (${entry.class_type})` : "";
31827
- const expectedType = entry.expected_type ? ` <${entry.expected_type}>` : "";
31828
- const aliasSuffix = entry.aliases.length > 0 ? ` | aliases: ${entry.aliases.join(", ")}` : "";
31829
- const optionsSuffix = options.includeOptions ? formatEditableInputOptionsText(entry) : "";
31830
- return `${entry.path}${role}${classType}${expectedType}: ${entry.value_preview}${aliasSuffix}${optionsSuffix}`;
31831
- });
32221
+ let sourceFormat = "api";
32222
+ let widgetPathMap = {};
32223
+ let normalizedWorkflow = workflow;
32224
+ if (isWebUIFormat(normalizedWorkflow)) {
32225
+ if (!objectInfo) {
32226
+ throw new WorkflowInputError(
32227
+ "Web UI workflow normalization requires ComfyUI node definitions. Start the ComfyUI server or save the workflow in API format first.",
32228
+ "workflow"
32229
+ );
32230
+ }
32231
+ sourceFormat = "webui";
32232
+ const converted = convertWebUIToAPIWithMetadata(
32233
+ normalizedWorkflow,
32234
+ objectInfo
32235
+ );
32236
+ normalizedWorkflow = converted.prompt;
32237
+ widgetPathMap = converted.widgetPathMap;
32238
+ }
32239
+ const promptCandidate = isRecord(normalizedWorkflow.prompt) && normalizedWorkflow.prompt ? normalizedWorkflow.prompt : normalizedWorkflow;
32240
+ if (!isRecord(promptCandidate)) {
32241
+ throw new WorkflowInputError(
32242
+ "Workflow prompt must be a JSON object.",
32243
+ "workflow"
32244
+ );
32245
+ }
32246
+ return {
32247
+ prompt: extractRunnablePromptNodes(promptCandidate),
32248
+ sourceFormat,
32249
+ widgetPathMap
32250
+ };
31832
32251
  }
31833
- function formatInputDefinitionSummary(inputName, inputDefinition) {
31834
- const [typeData, extraInfo] = inputDefinition;
31835
- const typeLabel = Array.isArray(typeData) ? `COMBO (${typeData.length} options)` : String(typeData).toUpperCase();
31836
- const details = [];
31837
- if (Array.isArray(typeData)) {
31838
- details.push(
31839
- `options: ${typeData.slice(0, 5).map((option) => JSON.stringify(option)).join(", ")}${typeData.length > 5 ? ", ..." : ""}`
32252
+ async function loadWorkflow(args, objectInfo) {
32253
+ const { targetPath, content } = await resolveWorkflowReference(args);
32254
+ let parsed;
32255
+ try {
32256
+ parsed = JSON.parse(content);
32257
+ } catch (error48) {
32258
+ throw new WorkflowInputError(
32259
+ `Invalid JSON in ${targetPath}: ${error48 instanceof Error ? error48.message : String(error48)}`,
32260
+ args.workflow_file_path ? "workflow_file_path" : "workflow_id"
31840
32261
  );
31841
32262
  }
31842
- if (isRecord(extraInfo)) {
31843
- if (extraInfo.default !== void 0) {
31844
- details.push(`default: ${formatValuePreview(extraInfo.default)}`);
32263
+ const { prompt, sourceFormat, widgetPathMap } = await normalizeWorkflowPrompt(parsed, objectInfo);
32264
+ return {
32265
+ targetPath,
32266
+ prompt,
32267
+ sourceFormat,
32268
+ widgetPathMap
32269
+ };
32270
+ }
32271
+ function buildDiscoveredOverrideKeys(prompt, objectInfo, widgetPathMap = {}) {
32272
+ const editableInputs = analyzeEditableInputs(
32273
+ prompt,
32274
+ objectInfo,
32275
+ widgetPathMap
32276
+ );
32277
+ return buildOverrideExamples(editableInputs, 5, { valueMode: "keys" }).preferred_paths;
32278
+ }
32279
+ async function inspectWorkflowFile(fullPath, objectInfo) {
32280
+ const content = await fs2.readFile(fullPath, "utf8");
32281
+ const defaultName = path2.basename(fullPath, ".json");
32282
+ const defaultId = getWorkflowId(fullPath);
32283
+ let parsed;
32284
+ try {
32285
+ parsed = JSON.parse(content);
32286
+ } catch {
32287
+ return void 0;
32288
+ }
32289
+ let name = defaultName;
32290
+ if (isRecord(parsed)) {
32291
+ if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
32292
+ name = parsed.name;
32293
+ } else if (isRecord(parsed.workflow) && typeof parsed.workflow.name === "string" && parsed.workflow.name.trim().length > 0) {
32294
+ name = parsed.workflow.name;
31845
32295
  }
31846
- if (typeof extraInfo.min === "number") {
31847
- details.push(`min: ${extraInfo.min}`);
32296
+ }
32297
+ if (isWebUIFormat(parsed)) {
32298
+ if (!objectInfo) {
32299
+ return {
32300
+ id: defaultId,
32301
+ name,
32302
+ path: fullPath,
32303
+ source_format: "webui",
32304
+ validation_status: "conversion_requires_node_schemas",
32305
+ content,
32306
+ isWebUI: true
32307
+ };
31848
32308
  }
31849
- if (typeof extraInfo.max === "number") {
31850
- details.push(`max: ${extraInfo.max}`);
32309
+ try {
32310
+ const normalized = await normalizeWorkflowPrompt(
32311
+ parsed,
32312
+ objectInfo
32313
+ );
32314
+ return {
32315
+ id: defaultId,
32316
+ name,
32317
+ path: fullPath,
32318
+ source_format: "webui",
32319
+ validation_status: "conversion_validated",
32320
+ content,
32321
+ isWebUI: true,
32322
+ override_keys: buildDiscoveredOverrideKeys(
32323
+ normalized.prompt,
32324
+ objectInfo,
32325
+ normalized.widgetPathMap
32326
+ )
32327
+ };
32328
+ } catch (error48) {
32329
+ return {
32330
+ id: defaultId,
32331
+ name,
32332
+ path: fullPath,
32333
+ source_format: "webui",
32334
+ validation_status: "conversion_failed",
32335
+ content,
32336
+ error: summarizeErrorMessage(error48),
32337
+ isWebUI: true
32338
+ };
31851
32339
  }
31852
32340
  }
31853
- return `${inputName} <${typeLabel}>${details.length > 0 ? ` | ${details.join(" | ")}` : ""}`;
31854
- }
31855
- function formatNodeDefinitionText(nodeClass, node) {
31856
- const requiredInputs = Object.entries(node.input.required ?? {});
31857
- const optionalInputs = Object.entries(node.input.optional ?? {});
31858
- const lines = [
31859
- `Node: ${nodeClass}`,
31860
- `Display name: ${node.display_name || node.name || nodeClass}`,
31861
- `Category: ${node.category || "uncategorized"}`
31862
- ];
31863
- if (typeof node.description === "string" && node.description.trim().length > 0) {
31864
- lines.push(`Description: ${node.description.trim()}`);
31865
- }
31866
- lines.push(
31867
- `Required inputs (${requiredInputs.length}): ${requiredInputs.length > 0 ? requiredInputs.map(
31868
- ([inputName, inputDefinition]) => formatInputDefinitionSummary(
31869
- inputName,
31870
- inputDefinition
31871
- )
31872
- ).join("\n") : "None."}`
31873
- );
31874
- lines.push(
31875
- `Optional inputs (${optionalInputs.length}): ${optionalInputs.length > 0 ? optionalInputs.map(
31876
- ([inputName, inputDefinition]) => formatInputDefinitionSummary(
31877
- inputName,
31878
- inputDefinition
32341
+ try {
32342
+ const normalized = await normalizeWorkflowPrompt(parsed, objectInfo);
32343
+ return {
32344
+ id: defaultId,
32345
+ name,
32346
+ path: fullPath,
32347
+ source_format: "api",
32348
+ validation_status: "ready",
32349
+ content,
32350
+ isWebUI: false,
32351
+ override_keys: buildDiscoveredOverrideKeys(
32352
+ normalized.prompt,
32353
+ objectInfo,
32354
+ normalized.widgetPathMap
31879
32355
  )
31880
- ).join("\n") : "None."}`
31881
- );
31882
- lines.push(`Outputs: ${node.output.join(", ") || "None."}`);
31883
- return lines.join("\n");
32356
+ };
32357
+ } catch {
32358
+ return void 0;
32359
+ }
31884
32360
  }
31885
- function formatNodeListText(classes, rawInfo, pagination) {
31886
- const text = `Available nodes (${pagination.total} total):
31887
- ` + formatListItems(classes, (nodeClass) => {
31888
- const node = rawInfo[nodeClass];
31889
- const displayName = node.display_name && node.display_name !== nodeClass ? ` \u2014 ${node.display_name}` : "";
31890
- const category = node.category ? ` | category: ${node.category}` : "";
31891
- const description = typeof node.description === "string" && node.description.trim().length > 0 ? ` | ${node.description.trim()}` : "";
31892
- return `${nodeClass}${displayName}${category}${description}`;
31893
- });
31894
- if (!pagination.hasMore) {
31895
- return text;
32361
+ function deriveSavedWorkflowFileName(sourcePath, requestedName) {
32362
+ if (requestedName) {
32363
+ return normalizeWorkflowFileName(requestedName);
31896
32364
  }
31897
- return `${text}
31898
-
31899
- ${formatPaginationFooter(
31900
- pagination.offset,
31901
- pagination.limit,
31902
- pagination.total
31903
- )}`;
32365
+ if (sourcePath) {
32366
+ return normalizeWorkflowFileName(path2.basename(sourcePath));
32367
+ }
32368
+ return `workflow-${Date.now()}.json`;
32369
+ }
32370
+ function getWorkflowId(absolutePath) {
32371
+ return crypto.createHash("sha256").update(absolutePath).digest("hex").substring(0, 8);
31904
32372
  }
31905
32373
 
31906
32374
  // src/zod.ts
@@ -31909,9 +32377,8 @@ var z3 = zNamespace.z ?? zNamespace.default ?? zNamespace;
31909
32377
 
31910
32378
  // src/workflow/overrides.ts
31911
32379
  var INPUT_SEGMENT = "inputs";
31912
- var OverrideRecordSchema = z3.record(z3.string(), z3.unknown());
31913
- var OverridesSchema = z3.union([OverrideRecordSchema, z3.string()]).describe(
31914
- 'Dot-notation overrides as an object or JSON string. e.g. {"6.seed": 7}'
32380
+ var OverridesSchema = z3.record(z3.string(), z3.unknown()).describe(
32381
+ 'Small override object only. e.g. {"positive_prompt":"...","seed":7}. Never paste full workflow JSON.'
31915
32382
  );
31916
32383
  function validateOverrideValueType(path4, value, inputDefinition) {
31917
32384
  if (!inputDefinition) {
@@ -32013,25 +32480,7 @@ function parseOverrides(rawOverrides) {
32013
32480
  if (rawOverrides === void 0) {
32014
32481
  return void 0;
32015
32482
  }
32016
- if (typeof rawOverrides !== "string") {
32017
- return rawOverrides;
32018
- }
32019
- let parsed;
32020
- try {
32021
- parsed = JSON.parse(rawOverrides);
32022
- } catch (error48) {
32023
- throw new WorkflowInputError(
32024
- `Overrides must be a valid JSON object string: ${error48 instanceof Error ? error48.message : String(error48)}`,
32025
- "overrides"
32026
- );
32027
- }
32028
- if (!isPlainObject3(parsed)) {
32029
- throw new WorkflowInputError(
32030
- "Overrides JSON string must decode to an object.",
32031
- "overrides"
32032
- );
32033
- }
32034
- return parsed;
32483
+ return rawOverrides;
32035
32484
  }
32036
32485
  function validatePromptPreflight(prompt, objectInfo, widgetPathMap = {}) {
32037
32486
  if (!objectInfo) {
@@ -32155,7 +32604,7 @@ ${detailSummary}` : error48.message
32155
32604
  error48.promptId,
32156
32605
  error48.queueSnapshot
32157
32606
  );
32158
- 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.";
32607
+ 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; call comfyui_wait_for_workflow again with this prompt_id, or comfyui_interrupt_workflow if the run should stop." : "ComfyUI queue state was unavailable at timeout.";
32159
32608
  return {
32160
32609
  content: [
32161
32610
  {
@@ -32163,8 +32612,7 @@ ${detailSummary}` : error48.message
32163
32612
  text: [
32164
32613
  `${error48.message}.`,
32165
32614
  queueSummary,
32166
- promptVisibilityText,
32167
- `You can re-attach by calling 'comfyui_wait_for_workflow' again with prompt_id '${error48.promptId}'.`
32615
+ promptVisibilityText
32168
32616
  ].filter((line) => line && line.length > 0).join("\n")
32169
32617
  }
32170
32618
  ],
@@ -32179,6 +32627,11 @@ ${detailSummary}` : error48.message
32179
32627
  }
32180
32628
  return createInternalError(error48);
32181
32629
  }
32630
+ var WorkflowRefFields = {
32631
+ workflow_ref: z3.string().optional().describe("Path, filename, or discover id."),
32632
+ workflow_file_path: z3.string().optional().describe("Deprecated alias of workflow_ref."),
32633
+ workflow_id: z3.string().optional().describe("Deprecated alias of workflow_ref.")
32634
+ };
32182
32635
  var InspectNodeSchema = paginationSchema.extend({
32183
32636
  node_class: z3.string().optional().describe(
32184
32637
  "Specific node class to inspect. If omitted, lists all available nodes."
@@ -32191,33 +32644,126 @@ var DiscoverWorkflowsSchema = paginationSchema.extend({
32191
32644
  directory_path: z3.string().optional().describe("Custom directory path to scan for workflows.")
32192
32645
  });
32193
32646
  var WorkflowRunSchema = z3.object({
32194
- workflow: z3.unknown().optional().describe("Workflow JSON object to execute immediately."),
32195
- workflow_file_path: z3.string().optional().describe(
32196
- "Path or filename of workflow JSON. Scans default dirs if simple name provided."
32197
- ),
32198
- workflow_id: z3.string().optional().describe("Workflow identifier returned by discover_workflows."),
32647
+ ...WorkflowRefFields,
32199
32648
  overrides: OverridesSchema.optional(),
32200
- await: z3.boolean().default(true).describe("Wait for completion and return native images."),
32201
- timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
32649
+ await: z3.boolean().default(true).describe(
32650
+ "Wait briefly for images. Prefer short timeout polls if the host cuts long waits."
32651
+ ),
32652
+ timeout: z3.number().int().min(1).default(25).describe(
32653
+ "Wait poll seconds (default 25). Call wait again if timed out."
32654
+ ),
32655
+ max_images: z3.number().int().min(0).max(8).default(2).describe(
32656
+ "Native images to return (0=refs only). Prefer output over temp; extras stay in image_refs."
32657
+ )
32202
32658
  });
32203
32659
  var GetWorkflowSchema = z3.object({
32204
- workflow_file_path: z3.string().optional().describe("Workflow path or filename to inspect."),
32205
- workflow_id: z3.string().optional().describe("Workflow identifier returned by discovery."),
32206
- include_prompt: z3.boolean().default(false).describe("Include normalized API JSON in structuredContent.")
32660
+ ...WorkflowRefFields,
32661
+ include_prompt: z3.boolean().default(false).describe("Include normalized API JSON in structuredContent."),
32662
+ verbose: z3.boolean().default(false).describe("Include every editable input, not just high-signal ones.")
32207
32663
  });
32208
32664
  var SaveWorkflowSchema = z3.object({
32209
- workflow: z3.unknown().optional().describe("Workflow JSON object to save locally."),
32210
- workflow_file_path: z3.string().optional().describe("Existing workflow path or filename to save."),
32211
- workflow_id: z3.string().optional().describe("Existing workflow identifier to save."),
32665
+ ...WorkflowRefFields,
32212
32666
  overrides: OverridesSchema.optional(),
32213
32667
  output_file_name: z3.string().optional().describe("Filename to write locally. (default: anchored to ./)"),
32214
32668
  overwrite: z3.boolean().default(false).describe("Replace an existing local file when true."),
32215
- include_prompt: z3.boolean().default(false).describe("Include saved API JSON in structuredContent.")
32669
+ include_prompt: z3.boolean().default(false).describe("Include saved API JSON in structuredContent."),
32670
+ verbose: z3.boolean().default(false).describe("Include every editable input, not just high-signal ones.")
32216
32671
  });
32217
32672
  var WaitForWorkflowSchema = z3.object({
32218
32673
  prompt_id: z3.string().describe("The prompt_id of the submitted workflow."),
32219
- timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
32674
+ timeout: z3.number().int().min(1).default(25).describe(
32675
+ "Wait poll seconds (default 25). Call again with the same prompt_id if timed out."
32676
+ ),
32677
+ max_images: z3.number().int().min(0).max(8).default(2).describe(
32678
+ "Native images to return (0=refs only). Prefer output over temp; extras stay in image_refs."
32679
+ )
32680
+ });
32681
+ var InterruptWorkflowSchema = z3.object({
32682
+ prompt_id: z3.string().optional().describe("Running prompt to interrupt. Omit to stop the current job."),
32683
+ clear_queue: z3.boolean().default(false).describe("Clear pending queue items when true.")
32684
+ });
32685
+ var UploadImageSchema = z3.object({
32686
+ file_path: z3.string().optional().describe(
32687
+ "Workspace image path. Use when the file is already on disk."
32688
+ ),
32689
+ prompt_id: z3.string().optional().describe(
32690
+ "Previous wait/run prompt_id. Server copies that output into input without image bytes in the model."
32691
+ ),
32692
+ filename: z3.string().optional().describe(
32693
+ "Required with prompt_id (ComfyUI output filename). Optional destination name for file_path."
32694
+ ),
32695
+ subfolder: z3.string().optional().describe("ComfyUI subfolder for a prompt_id source."),
32696
+ source_type: z3.enum(["output", "input", "temp"]).default("output").describe("ComfyUI folder to read when using prompt_id."),
32697
+ image_type: z3.enum(["input", "temp"]).default("input").describe("ComfyUI upload folder type."),
32698
+ overwrite: z3.boolean().default(true).describe("Replace an existing file when true.")
32699
+ });
32700
+ var ListModelsSchema = paginationSchema.extend({
32701
+ model_type: z3.enum(COMFY_MODEL_TYPES).default("checkpoint").describe("Model folder to list.")
32220
32702
  });
32703
+ function hasStoredWorkflowRef(args) {
32704
+ return Boolean(getWorkflowReference(args));
32705
+ }
32706
+ function collectHistoryImages(outputs) {
32707
+ const images = [];
32708
+ for (const [nodeId, output] of Object.entries(outputs)) {
32709
+ if (!output || !Array.isArray(output.images)) {
32710
+ continue;
32711
+ }
32712
+ for (const image of output.images) {
32713
+ if (!image.filename || !image.type) {
32714
+ continue;
32715
+ }
32716
+ images.push({
32717
+ filename: image.filename,
32718
+ type: image.type,
32719
+ subfolder: image.subfolder ?? "",
32720
+ nodeId
32721
+ });
32722
+ }
32723
+ }
32724
+ return images;
32725
+ }
32726
+ function rankImageType(type) {
32727
+ if (type === "output") {
32728
+ return 0;
32729
+ }
32730
+ if (type === "temp") {
32731
+ return 1;
32732
+ }
32733
+ return 2;
32734
+ }
32735
+ function selectImagesForDisplay(images, maxImages) {
32736
+ if (maxImages <= 0) {
32737
+ return [];
32738
+ }
32739
+ return [...images].sort((left, right) => {
32740
+ const typeDiff = rankImageType(left.type) - rankImageType(right.type);
32741
+ if (typeDiff !== 0) {
32742
+ return typeDiff;
32743
+ }
32744
+ return left.nodeId.localeCompare(right.nodeId, void 0, {
32745
+ numeric: true
32746
+ });
32747
+ }).slice(0, maxImages);
32748
+ }
32749
+ function formatUploadReuseHint(promptId, image) {
32750
+ const parts = [
32751
+ `prompt_id='${promptId}'`,
32752
+ `filename='${image.filename}'`,
32753
+ `source_type='${image.type}'`
32754
+ ];
32755
+ if (image.subfolder) {
32756
+ parts.push(`subfolder='${image.subfolder}'`);
32757
+ }
32758
+ return `Reuse in img2img with comfyui_upload_image: ${parts.join(" ")}`;
32759
+ }
32760
+ function toPublicEditableInput(entry, verbose) {
32761
+ if (verbose) {
32762
+ return entry;
32763
+ }
32764
+ const { value: _value, ...rest } = entry;
32765
+ return rest;
32766
+ }
32221
32767
  async function handleInspectNode(args) {
32222
32768
  try {
32223
32769
  const rawInfo = await getObjectInfo();
@@ -32248,20 +32794,28 @@ async function handleInspectNode(args) {
32248
32794
  });
32249
32795
  }
32250
32796
  const paginated = applyPagination(classes, args);
32251
- const resultNodes = {};
32252
- for (const c of paginated.items) {
32253
- resultNodes[c] = rawInfo[c];
32254
- }
32797
+ const resultNodes = paginated.items.map(
32798
+ (nodeClass) => {
32799
+ const node = rawInfo[nodeClass];
32800
+ return {
32801
+ class_type: nodeClass,
32802
+ display_name: node?.display_name || nodeClass,
32803
+ category: node?.category || ""
32804
+ };
32805
+ }
32806
+ );
32255
32807
  return {
32256
32808
  content: [
32257
32809
  {
32258
32810
  type: "text",
32259
- text: formatNodeListText(paginated.items, rawInfo, {
32260
- total: paginated.total,
32261
- offset: paginated.offset,
32262
- limit: paginated.limit,
32263
- hasMore: paginated.hasMore
32264
- })
32811
+ text: truncateToLimit(
32812
+ formatNodeListText(paginated.items, rawInfo, {
32813
+ total: paginated.total,
32814
+ offset: paginated.offset,
32815
+ limit: paginated.limit,
32816
+ hasMore: paginated.hasMore
32817
+ })
32818
+ )
32265
32819
  }
32266
32820
  ],
32267
32821
  structuredContent: {
@@ -32331,6 +32885,10 @@ Hint: If your workflows are stored elsewhere, provide a custom path using the 'd
32331
32885
  if (workflow.error) {
32332
32886
  line += ` (Validation error: ${workflow.error})`;
32333
32887
  }
32888
+ const exampleKeys = workflow.override_keys ?? [];
32889
+ if (exampleKeys.length > 0) {
32890
+ line += ` | overrides: ${exampleKeys.join(", ")}`;
32891
+ }
32334
32892
  return line;
32335
32893
  })
32336
32894
  ];
@@ -32361,7 +32919,8 @@ Hint: If your workflows are stored elsewhere, provide a custom path using the 'd
32361
32919
  isWebUI: workflow.isWebUI,
32362
32920
  source_format: workflow.source_format,
32363
32921
  validation_status: workflow.validation_status,
32364
- ...workflow.error ? { error: workflow.error } : {}
32922
+ ...workflow.error ? { error: workflow.error } : {},
32923
+ ...workflow.override_keys && workflow.override_keys.length > 0 ? { override_keys: workflow.override_keys } : {}
32365
32924
  })),
32366
32925
  skipped_non_workflow_files: skippedNonWorkflowFiles,
32367
32926
  pagination: {
@@ -32380,61 +32939,98 @@ async function handleWaitForWorkflow(args) {
32380
32939
  args.timeout
32381
32940
  );
32382
32941
  const outputsRecord = historyEntry.outputs || {};
32942
+ const collectedImages = collectHistoryImages(outputsRecord);
32943
+ const displayImages = selectImagesForDisplay(
32944
+ collectedImages,
32945
+ args.max_images
32946
+ );
32947
+ const displayKeys = new Set(
32948
+ displayImages.map(
32949
+ (image) => `${image.nodeId}\0${image.type}\0${image.subfolder}\0${image.filename}`
32950
+ )
32951
+ );
32952
+ const orderedImages = [
32953
+ ...displayImages,
32954
+ ...collectedImages.filter(
32955
+ (image) => !displayKeys.has(
32956
+ `${image.nodeId}\0${image.type}\0${image.subfolder}\0${image.filename}`
32957
+ )
32958
+ )
32959
+ ];
32383
32960
  const workspaceOutputs = [];
32961
+ const imageRefs = [];
32384
32962
  const content = [];
32385
- let summaryText = `Workflow ${args.prompt_id} completed.
32386
- `;
32387
- for (const [, output] of Object.entries(outputsRecord)) {
32388
- if (output && Array.isArray(output.images)) {
32389
- for (const image of output.images) {
32390
- if (image.filename && image.type) {
32391
- try {
32392
- const asset = IS_MOCK ? createMockImageAsset() : await fetchImageAsset(
32393
- image.filename,
32394
- image.type,
32395
- image.subfolder
32396
- );
32397
- content.push({
32398
- type: "image",
32399
- data: asset.buffer.toString("base64"),
32400
- mimeType: asset.mimeType
32401
- });
32402
- summaryText += `Fetched image: ${image.filename}
32403
- `;
32404
- const summary = await saveToWorkspace(
32405
- args.prompt_id,
32406
- image,
32407
- asset
32408
- );
32409
- if (summary) {
32410
- workspaceOutputs.push(summary);
32411
- summaryText += `Copied image to workspace: ${summary.relative_path}
32412
- `;
32413
- }
32414
- } catch (e) {
32415
- summaryText += `Failed to fetch image ${image.filename}: ${String(e)}
32416
- `;
32417
- }
32963
+ const summaryLines = [
32964
+ `Workflow ${args.prompt_id} completed.`,
32965
+ `Images available: ${collectedImages.length}; returning ${displayImages.length} native image(s) (max_images=${args.max_images}). Prefer output over temp.`
32966
+ ];
32967
+ for (const image of orderedImages) {
32968
+ const imageKey = `${image.nodeId}\0${image.type}\0${image.subfolder}\0${image.filename}`;
32969
+ const shouldFetch = displayKeys.has(imageKey);
32970
+ const imageRef = {
32971
+ prompt_id: args.prompt_id,
32972
+ filename: image.filename,
32973
+ type: image.type,
32974
+ ...image.subfolder ? { subfolder: image.subfolder } : {}
32975
+ };
32976
+ if (shouldFetch) {
32977
+ try {
32978
+ const asset = IS_MOCK ? createMockImageAsset() : await fetchImageAsset(
32979
+ image.filename,
32980
+ image.type,
32981
+ image.subfolder || void 0
32982
+ );
32983
+ content.push({
32984
+ type: "image",
32985
+ data: asset.buffer.toString("base64"),
32986
+ mimeType: asset.mimeType
32987
+ });
32988
+ summaryLines.push(`Fetched image: ${image.filename}`);
32989
+ const summary = await saveToWorkspace(
32990
+ args.prompt_id,
32991
+ {
32992
+ filename: image.filename,
32993
+ subfolder: image.subfolder
32994
+ },
32995
+ asset
32996
+ );
32997
+ if (summary) {
32998
+ workspaceOutputs.push(summary);
32999
+ imageRef.workspace_path = summary.workspace_path;
33000
+ imageRef.relative_path = summary.relative_path;
33001
+ summaryLines.push(
33002
+ `Copied image to workspace: ${summary.relative_path}`
33003
+ );
32418
33004
  }
33005
+ } catch (error48) {
33006
+ summaryLines.push(
33007
+ `Failed to fetch image ${image.filename}: ${String(error48)}`
33008
+ );
32419
33009
  }
32420
33010
  }
33011
+ summaryLines.push(formatUploadReuseHint(args.prompt_id, image));
33012
+ imageRefs.push(imageRef);
32421
33013
  }
32422
- if (IS_MOCK && outputsRecord) {
33014
+ if (IS_MOCK && collectedImages.length === 0) {
32423
33015
  for (const [, output] of Object.entries(outputsRecord)) {
32424
33016
  if (output && Array.isArray(output.images)) {
32425
33017
  for (const image of output.images) {
32426
- summaryText += `Mock image: ${image.filename}
32427
- `;
33018
+ summaryLines.push(`Mock image: ${image.filename}`);
32428
33019
  }
32429
33020
  }
32430
33021
  }
32431
33022
  }
32432
- content.unshift({ type: "text", text: summaryText });
33023
+ content.unshift({ type: "text", text: `${summaryLines.join("\n")}
33024
+ ` });
32433
33025
  return {
32434
33026
  content,
32435
33027
  structuredContent: {
32436
- ...historyEntry,
32437
- workspace_outputs: workspaceOutputs
33028
+ prompt_id: args.prompt_id,
33029
+ status: historyEntry.status,
33030
+ image_refs: imageRefs,
33031
+ workspace_outputs: workspaceOutputs,
33032
+ images_returned: content.filter((item) => item.type === "image").length,
33033
+ images_available: collectedImages.length
32438
33034
  }
32439
33035
  };
32440
33036
  } catch (error48) {
@@ -32455,34 +33051,29 @@ async function handleGetWorkflow(args) {
32455
33051
  );
32456
33052
  const highSignalInputs = getHighSignalEditableInputs(editableInputs);
32457
33053
  const editableInputGroups = summarizeEditableInputGroups(editableInputs);
32458
- const overrideExamples = buildOverrideExamples(editableInputs);
33054
+ const overrideExamples = buildOverrideExamples(editableInputs, 5, {
33055
+ valueMode: "preview"
33056
+ });
32459
33057
  const workflowId = getWorkflowId(loadedWorkflow.targetPath);
32460
- const textSections = [
32461
- `Workflow ${workflowId} loaded from ${loadedWorkflow.targetPath}.`,
32462
- loadedWorkflow.sourceFormat === "webui" ? "Source format: Web UI (normalized to API format)." : "Source format: API format."
32463
- ];
32464
- if (objectInfoAvailability.warning) {
32465
- textSections.push(objectInfoAvailability.warning);
32466
- }
32467
- textSections.push(
32468
- `High-signal overrides (${highSignalInputs.length}):`,
32469
- formatEditableInputsText(highSignalInputs, {
32470
- includeOptions: true
32471
- }),
32472
- "Editable input groups:",
32473
- formatEditableInputGroupsText(editableInputGroups),
32474
- `Editable inputs (${editableInputs.length} total):`,
32475
- formatEditableInputsText(editableInputs),
32476
- "Copy-ready override example:",
32477
- formatOverrideExamplesText(overrideExamples.examples)
32478
- );
32479
- const text = textSections.join("\n");
33058
+ const text = formatWorkflowInspectionText({
33059
+ introLines: [
33060
+ `Workflow ${workflowId} loaded from ${loadedWorkflow.targetPath}.`,
33061
+ loadedWorkflow.sourceFormat === "webui" ? "Source format: Web UI (normalized to API format)." : "Source format: API format."
33062
+ ],
33063
+ warning: objectInfoAvailability.warning,
33064
+ highSignalInputs,
33065
+ editableInputGroups,
33066
+ editableInputs,
33067
+ overrideExamples: overrideExamples.examples,
33068
+ preferredOverridePaths: overrideExamples.preferred_paths,
33069
+ verbose: args.verbose
33070
+ });
33071
+ const publicInputs = (args.verbose ? editableInputs : highSignalInputs).map((entry) => toPublicEditableInput(entry, args.verbose));
32480
33072
  const structuredContent = {
32481
33073
  workflow_id: workflowId,
32482
33074
  path: loadedWorkflow.targetPath,
32483
33075
  source_format: loadedWorkflow.sourceFormat,
32484
- editable_inputs: editableInputs,
32485
- high_signal_inputs: highSignalInputs,
33076
+ editable_inputs: publicInputs,
32486
33077
  input_groups: editableInputGroups,
32487
33078
  override_examples: overrideExamples.examples,
32488
33079
  preferred_override_paths: overrideExamples.preferred_paths
@@ -32503,33 +33094,21 @@ async function handleGetWorkflow(args) {
32503
33094
  }
32504
33095
  async function handleSaveWorkflow(args) {
32505
33096
  try {
32506
- let prompt;
32507
- let widgetPathMap = {};
32508
- let sourcePath;
32509
33097
  const parsedOverrides = parseOverrides(args.overrides);
32510
33098
  const objectInfoAvailability = await tryGetObjectInfo();
32511
- if (args.workflow !== void 0) {
32512
- const normalized = await normalizeWorkflowPrompt(
32513
- args.workflow,
32514
- objectInfoAvailability.objectInfo
32515
- );
32516
- prompt = normalized.prompt;
32517
- widgetPathMap = normalized.widgetPathMap;
32518
- } else {
32519
- if (!args.workflow_file_path && !args.workflow_id) {
32520
- return createValidationError(
32521
- "workflow",
32522
- "Provide workflow JSON or an existing workflow reference."
32523
- );
32524
- }
32525
- const loadedWorkflow = await loadWorkflow(
32526
- args,
32527
- objectInfoAvailability.objectInfo
33099
+ if (!hasStoredWorkflowRef(args)) {
33100
+ return createValidationError(
33101
+ "workflow_ref",
33102
+ "Provide a stored workflow_ref (path, filename, or discover id)."
32528
33103
  );
32529
- prompt = loadedWorkflow.prompt;
32530
- sourcePath = loadedWorkflow.targetPath;
32531
- widgetPathMap = loadedWorkflow.widgetPathMap;
32532
33104
  }
33105
+ const loadedWorkflow = await loadWorkflow(
33106
+ args,
33107
+ objectInfoAvailability.objectInfo
33108
+ );
33109
+ const prompt = loadedWorkflow.prompt;
33110
+ const sourcePath = loadedWorkflow.targetPath;
33111
+ const widgetPathMap = loadedWorkflow.widgetPathMap;
32533
33112
  const editableInputs = analyzeEditableInputs(
32534
33113
  prompt,
32535
33114
  objectInfoAvailability.objectInfo,
@@ -32584,15 +33163,19 @@ async function handleSaveWorkflow(args) {
32584
33163
  const editableInputGroups = summarizeEditableInputGroups(
32585
33164
  updatedEditableInputs
32586
33165
  );
32587
- const overrideExamples = buildOverrideExamples(updatedEditableInputs);
33166
+ const overrideExamples = buildOverrideExamples(
33167
+ updatedEditableInputs,
33168
+ 5,
33169
+ { valueMode: "preview" }
33170
+ );
32588
33171
  const workflowId = getWorkflowId(outputPath);
33172
+ const publicInputs = (args.verbose ? updatedEditableInputs : highSignalInputs).map((entry) => toPublicEditableInput(entry, args.verbose));
32589
33173
  const structuredContent = {
32590
33174
  workflow_id: workflowId,
32591
33175
  path: outputPath,
32592
33176
  source_path: sourcePath,
32593
33177
  overwritten: existedBeforeSave,
32594
- editable_inputs: updatedEditableInputs,
32595
- high_signal_inputs: highSignalInputs,
33178
+ editable_inputs: publicInputs,
32596
33179
  input_groups: editableInputGroups,
32597
33180
  override_examples: overrideExamples.examples,
32598
33181
  preferred_override_paths: overrideExamples.preferred_paths
@@ -32604,26 +33187,19 @@ async function handleSaveWorkflow(args) {
32604
33187
  structuredContent.prompt = prompt;
32605
33188
  }
32606
33189
  const persistedChanges = parsedOverrides && Object.keys(parsedOverrides).length > 0 ? `Persisted ${Object.keys(parsedOverrides).length} override(s).` : "No overrides were applied.";
32607
- const textSections = [
32608
- `Saved workflow ${workflowId} to ${outputPath}.`,
32609
- persistedChanges
32610
- ];
32611
- if (objectInfoAvailability.warning) {
32612
- textSections.push(objectInfoAvailability.warning);
32613
- }
32614
- textSections.push(
32615
- `High-signal overrides (${highSignalInputs.length}):`,
32616
- formatEditableInputsText(highSignalInputs, {
32617
- includeOptions: true
32618
- }),
32619
- "Editable input groups:",
32620
- formatEditableInputGroupsText(editableInputGroups),
32621
- `Editable inputs (${updatedEditableInputs.length} total):`,
32622
- formatEditableInputsText(updatedEditableInputs),
32623
- "Copy-ready override example:",
32624
- formatOverrideExamplesText(overrideExamples.examples)
32625
- );
32626
- const text = textSections.join("\n");
33190
+ const text = formatWorkflowInspectionText({
33191
+ introLines: [
33192
+ `Saved workflow ${workflowId} to ${outputPath}.`,
33193
+ persistedChanges
33194
+ ],
33195
+ warning: objectInfoAvailability.warning,
33196
+ highSignalInputs,
33197
+ editableInputGroups,
33198
+ editableInputs: updatedEditableInputs,
33199
+ overrideExamples: overrideExamples.examples,
33200
+ preferredOverridePaths: overrideExamples.preferred_paths,
33201
+ verbose: args.verbose
33202
+ });
32627
33203
  return {
32628
33204
  content: [{ type: "text", text }],
32629
33205
  structuredContent
@@ -32634,30 +33210,14 @@ async function handleSaveWorkflow(args) {
32634
33210
  }
32635
33211
  async function handleWorkflowRun(args) {
32636
33212
  try {
32637
- let loadedWorkflow;
32638
- let objectInfo;
32639
- if (args.workflow !== void 0) {
32640
- objectInfo = await getObjectInfo();
32641
- const normalized = await normalizeWorkflowPrompt(
32642
- args.workflow,
32643
- objectInfo
33213
+ if (!hasStoredWorkflowRef(args)) {
33214
+ return createValidationError(
33215
+ "workflow_ref",
33216
+ "Provide a stored workflow_ref (path, filename, or discover id), or set COMFYUI_DEFAULT_WORKFLOW."
32644
33217
  );
32645
- loadedWorkflow = {
32646
- targetPath: "[inline workflow]",
32647
- prompt: normalized.prompt,
32648
- sourceFormat: normalized.sourceFormat,
32649
- widgetPathMap: normalized.widgetPathMap
32650
- };
32651
- } else {
32652
- if (!args.workflow_file_path && !args.workflow_id) {
32653
- return createValidationError(
32654
- "workflow",
32655
- "Provide workflow JSON or an existing workflow reference."
32656
- );
32657
- }
32658
- objectInfo = await getObjectInfo();
32659
- loadedWorkflow = await loadWorkflow(args, objectInfo);
32660
33218
  }
33219
+ const objectInfo = await getObjectInfo();
33220
+ const loadedWorkflow = await loadWorkflow(args, objectInfo);
32661
33221
  const { prompt } = loadedWorkflow;
32662
33222
  const promptObj = { ...prompt };
32663
33223
  const parsedOverrides = parseOverrides(args.overrides);
@@ -32700,10 +33260,11 @@ async function handleWorkflowRun(args) {
32700
33260
  if (args.await) {
32701
33261
  return await handleWaitForWorkflow({
32702
33262
  prompt_id: result.prompt_id,
32703
- timeout: args.timeout
33263
+ timeout: args.timeout,
33264
+ max_images: args.max_images
32704
33265
  });
32705
33266
  }
32706
- const queueSnapshot = await getQueueSnapshot();
33267
+ const queueSnapshot = summarizeQueue(await getQueueSnapshot());
32707
33268
  return {
32708
33269
  content: [
32709
33270
  {
@@ -32713,7 +33274,7 @@ async function handleWorkflowRun(args) {
32713
33274
  `ComfyUI queue number: ${result.number} (queue number is not an ETA).`,
32714
33275
  `Preflight checked ${preflight.checked_inputs} literal inputs (${preflight.combo_inputs} combo-backed).`,
32715
33276
  formatQueueSnapshotText(queueSnapshot),
32716
- `To retrieve the final results or check its status later, use the 'comfyui_wait_for_workflow' tool with this prompt_id.`
33277
+ `If host MCP timeouts cut long waits, poll with comfyui_wait_for_workflow (default ~25s) until complete.`
32717
33278
  ].filter((line) => line && line.length > 0).join("\n")
32718
33279
  }
32719
33280
  ],
@@ -32727,6 +33288,107 @@ async function handleWorkflowRun(args) {
32727
33288
  return handleComfyError(error48);
32728
33289
  }
32729
33290
  }
33291
+ async function handleInterruptWorkflow(args) {
33292
+ try {
33293
+ const result = await interruptWorkflow({
33294
+ promptId: args.prompt_id,
33295
+ clearQueue: args.clear_queue
33296
+ });
33297
+ const queueSummary = formatQueueSnapshotText(result.queue);
33298
+ const targetText = args.prompt_id ? result.interrupted ? `Interrupted prompt_id '${args.prompt_id}'.` : `Prompt_id '${args.prompt_id}' is not currently running; no interrupt was sent.` : result.interrupted ? "Interrupted the currently running workflow." : "No running workflow was interrupted.";
33299
+ const queueClearedText = result.queue_cleared ? "Pending queue cleared." : "Pending queue was left unchanged.";
33300
+ return {
33301
+ content: [
33302
+ {
33303
+ type: "text",
33304
+ text: [targetText, queueClearedText, queueSummary].filter((line) => line && line.length > 0).join("\n")
33305
+ }
33306
+ ],
33307
+ structuredContent: {
33308
+ interrupted: result.interrupted,
33309
+ prompt_id: result.prompt_id ?? null,
33310
+ queue_cleared: result.queue_cleared,
33311
+ queue: result.queue ?? null
33312
+ }
33313
+ };
33314
+ } catch (error48) {
33315
+ return handleComfyError(error48);
33316
+ }
33317
+ }
33318
+ async function handleUploadImage(args) {
33319
+ try {
33320
+ const source = await readUploadImageSource({
33321
+ filePath: args.file_path,
33322
+ promptId: args.prompt_id,
33323
+ filename: args.filename,
33324
+ subfolder: args.subfolder,
33325
+ sourceType: args.source_type
33326
+ });
33327
+ const uploaded = await uploadImage({
33328
+ buffer: source.buffer,
33329
+ filename: source.filename,
33330
+ mimeType: source.mimeType,
33331
+ imageType: args.image_type,
33332
+ overwrite: args.overwrite
33333
+ });
33334
+ const loadImageName = uploaded.subfolder ? `${uploaded.subfolder}/${uploaded.name}` : uploaded.name;
33335
+ return {
33336
+ content: [
33337
+ {
33338
+ type: "text",
33339
+ text: [
33340
+ `Uploaded image '${uploaded.name}' to ComfyUI ${uploaded.type}.`,
33341
+ `Use this filename in LoadImage overrides: ${loadImageName}`
33342
+ ].join("\n")
33343
+ }
33344
+ ],
33345
+ structuredContent: {
33346
+ name: uploaded.name,
33347
+ subfolder: uploaded.subfolder,
33348
+ type: uploaded.type,
33349
+ load_image_name: loadImageName
33350
+ }
33351
+ };
33352
+ } catch (error48) {
33353
+ return handleComfyError(error48);
33354
+ }
33355
+ }
33356
+ async function handleListModels(args) {
33357
+ try {
33358
+ const listedModels = await getModelList(args.model_type);
33359
+ const paginated = applyPagination(listedModels.models, args);
33360
+ const footer = paginated.hasMore ? formatPaginationFooter(
33361
+ paginated.offset,
33362
+ paginated.limit,
33363
+ paginated.total
33364
+ ) : "";
33365
+ const listed = paginated.items.length > 0 ? formatListItems(paginated.items, (model) => model) : "_No models found._";
33366
+ const text = listedModels.models_endpoint_available ? `Available ${args.model_type} models (${paginated.total} total):
33367
+ ${listed}${footer}` : `Could not list ${args.model_type} models: this ComfyUI server does not expose /models/{folder} (HTTP 404).
33368
+ Upgrade ComfyUI, or inspect combo-backed options via comfyui_inspect_node.`;
33369
+ return {
33370
+ content: [
33371
+ {
33372
+ type: "text",
33373
+ text: truncateToLimit(text)
33374
+ }
33375
+ ],
33376
+ structuredContent: {
33377
+ model_type: args.model_type,
33378
+ models: paginated.items,
33379
+ models_endpoint_available: listedModels.models_endpoint_available,
33380
+ pagination: {
33381
+ total: paginated.total,
33382
+ offset: paginated.offset,
33383
+ limit: paginated.limit,
33384
+ has_more: paginated.hasMore
33385
+ }
33386
+ }
33387
+ };
33388
+ } catch (error48) {
33389
+ return handleComfyError(error48);
33390
+ }
33391
+ }
32730
33392
  function createServer() {
32731
33393
  const server = new McpServer({
32732
33394
  name: "@fre4x/comfyui",
@@ -32746,7 +33408,7 @@ function createServer() {
32746
33408
  );
32747
33409
  server.tool(
32748
33410
  "comfyui_workflow_run",
32749
- "Run a workflow from a file path or ID, applying overrides, and optionally returning images.",
33411
+ "Run a stored workflow_ref with small overrides.",
32750
33412
  WorkflowRunSchema.shape,
32751
33413
  handleWorkflowRun
32752
33414
  );
@@ -32758,7 +33420,7 @@ function createServer() {
32758
33420
  );
32759
33421
  server.tool(
32760
33422
  "comfyui_save_workflow",
32761
- "Save a workflow into ./workflows with optional edits.",
33423
+ "Save a stored workflow_ref with optional overrides.",
32762
33424
  SaveWorkflowSchema.shape,
32763
33425
  handleSaveWorkflow
32764
33426
  );
@@ -32768,6 +33430,35 @@ function createServer() {
32768
33430
  WaitForWorkflowSchema.shape,
32769
33431
  handleWaitForWorkflow
32770
33432
  );
33433
+ server.tool(
33434
+ "comfyui_interrupt_workflow",
33435
+ "Interrupt the running workflow and optionally clear the queue.",
33436
+ InterruptWorkflowSchema.shape,
33437
+ {
33438
+ destructiveHint: true,
33439
+ openWorldHint: true
33440
+ },
33441
+ handleInterruptWorkflow
33442
+ );
33443
+ server.tool(
33444
+ "comfyui_upload_image",
33445
+ "Upload via file_path or previous prompt_id+filename. Image bytes never pass through the model.",
33446
+ UploadImageSchema.shape,
33447
+ {
33448
+ openWorldHint: true
33449
+ },
33450
+ handleUploadImage
33451
+ );
33452
+ server.tool(
33453
+ "comfyui_list_models",
33454
+ "List installed checkpoints, LoRAs, VAEs, and other models.",
33455
+ ListModelsSchema.shape,
33456
+ {
33457
+ readOnlyHint: true,
33458
+ openWorldHint: true
33459
+ },
33460
+ handleListModels
33461
+ );
32771
33462
  return server;
32772
33463
  }
32773
33464
  function isMainModule(url2) {