@hypercli.com/sdk 0.9.1

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 (193) hide show
  1. package/README.md +629 -0
  2. package/dist/acp-driver.d.ts +72 -0
  3. package/dist/acp-driver.d.ts.map +1 -0
  4. package/dist/acp-driver.js +147 -0
  5. package/dist/acp-driver.js.map +1 -0
  6. package/dist/acp-pool.d.ts +70 -0
  7. package/dist/acp-pool.d.ts.map +1 -0
  8. package/dist/acp-pool.js +181 -0
  9. package/dist/acp-pool.js.map +1 -0
  10. package/dist/acp.d.ts +179 -0
  11. package/dist/acp.d.ts.map +1 -0
  12. package/dist/acp.js +487 -0
  13. package/dist/acp.js.map +1 -0
  14. package/dist/agent-persona.d.ts +68 -0
  15. package/dist/agent-persona.d.ts.map +1 -0
  16. package/dist/agent-persona.js +78 -0
  17. package/dist/agent-persona.js.map +1 -0
  18. package/dist/agent-slots.d.ts +18 -0
  19. package/dist/agent-slots.d.ts.map +1 -0
  20. package/dist/agent-slots.js +24 -0
  21. package/dist/agent-slots.js.map +1 -0
  22. package/dist/agent-urls.d.ts +47 -0
  23. package/dist/agent-urls.d.ts.map +1 -0
  24. package/dist/agent-urls.js +178 -0
  25. package/dist/agent-urls.js.map +1 -0
  26. package/dist/agent.d.ts +482 -0
  27. package/dist/agent.d.ts.map +1 -0
  28. package/dist/agent.js +926 -0
  29. package/dist/agent.js.map +1 -0
  30. package/dist/agents.d.ts +1452 -0
  31. package/dist/agents.d.ts.map +1 -0
  32. package/dist/agents.js +4022 -0
  33. package/dist/agents.js.map +1 -0
  34. package/dist/billing.d.ts +72 -0
  35. package/dist/billing.d.ts.map +1 -0
  36. package/dist/billing.js +93 -0
  37. package/dist/billing.js.map +1 -0
  38. package/dist/browser-jobs.d.ts +24 -0
  39. package/dist/browser-jobs.d.ts.map +1 -0
  40. package/dist/browser-jobs.js +209 -0
  41. package/dist/browser-jobs.js.map +1 -0
  42. package/dist/browser.d.ts +41 -0
  43. package/dist/browser.d.ts.map +1 -0
  44. package/dist/browser.js +57 -0
  45. package/dist/browser.js.map +1 -0
  46. package/dist/buzz-activity.d.ts +169 -0
  47. package/dist/buzz-activity.d.ts.map +1 -0
  48. package/dist/buzz-activity.js +844 -0
  49. package/dist/buzz-activity.js.map +1 -0
  50. package/dist/channels.d.ts +99 -0
  51. package/dist/channels.d.ts.map +1 -0
  52. package/dist/channels.js +27 -0
  53. package/dist/channels.js.map +1 -0
  54. package/dist/chat-integrations.d.ts +44 -0
  55. package/dist/chat-integrations.d.ts.map +1 -0
  56. package/dist/chat-integrations.js +138 -0
  57. package/dist/chat-integrations.js.map +1 -0
  58. package/dist/client.d.ts +88 -0
  59. package/dist/client.d.ts.map +1 -0
  60. package/dist/client.js +144 -0
  61. package/dist/client.js.map +1 -0
  62. package/dist/config.d.ts +44 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +192 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/connectors.d.ts +80 -0
  67. package/dist/connectors.d.ts.map +1 -0
  68. package/dist/connectors.js +2 -0
  69. package/dist/connectors.js.map +1 -0
  70. package/dist/errors.d.ts +16 -0
  71. package/dist/errors.d.ts.map +1 -0
  72. package/dist/errors.js +26 -0
  73. package/dist/errors.js.map +1 -0
  74. package/dist/files.d.ts +60 -0
  75. package/dist/files.d.ts.map +1 -0
  76. package/dist/files.js +128 -0
  77. package/dist/files.js.map +1 -0
  78. package/dist/http.d.ts +56 -0
  79. package/dist/http.d.ts.map +1 -0
  80. package/dist/http.js +362 -0
  81. package/dist/http.js.map +1 -0
  82. package/dist/index.d.ts +49 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +64 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/instances.d.ts +94 -0
  87. package/dist/instances.d.ts.map +1 -0
  88. package/dist/instances.js +197 -0
  89. package/dist/instances.js.map +1 -0
  90. package/dist/integrations.d.ts +111 -0
  91. package/dist/integrations.d.ts.map +1 -0
  92. package/dist/integrations.js +206 -0
  93. package/dist/integrations.js.map +1 -0
  94. package/dist/job/base.d.ts +86 -0
  95. package/dist/job/base.d.ts.map +1 -0
  96. package/dist/job/base.js +206 -0
  97. package/dist/job/base.js.map +1 -0
  98. package/dist/job/comfyui.d.ts +259 -0
  99. package/dist/job/comfyui.d.ts.map +1 -0
  100. package/dist/job/comfyui.js +1563 -0
  101. package/dist/job/comfyui.js.map +1 -0
  102. package/dist/job/gradio.d.ts +48 -0
  103. package/dist/job/gradio.d.ts.map +1 -0
  104. package/dist/job/gradio.js +90 -0
  105. package/dist/job/gradio.js.map +1 -0
  106. package/dist/job-codecs.d.ts +66 -0
  107. package/dist/job-codecs.d.ts.map +1 -0
  108. package/dist/job-codecs.js +132 -0
  109. package/dist/job-codecs.js.map +1 -0
  110. package/dist/jobs.d.ts +148 -0
  111. package/dist/jobs.d.ts.map +1 -0
  112. package/dist/jobs.js +480 -0
  113. package/dist/jobs.js.map +1 -0
  114. package/dist/keys.d.ts +99 -0
  115. package/dist/keys.d.ts.map +1 -0
  116. package/dist/keys.js +145 -0
  117. package/dist/keys.js.map +1 -0
  118. package/dist/logs.d.ts +54 -0
  119. package/dist/logs.d.ts.map +1 -0
  120. package/dist/logs.js +256 -0
  121. package/dist/logs.js.map +1 -0
  122. package/dist/managed-context.d.ts +20 -0
  123. package/dist/managed-context.d.ts.map +1 -0
  124. package/dist/managed-context.js +95 -0
  125. package/dist/managed-context.js.map +1 -0
  126. package/dist/mcp.d.ts +7 -0
  127. package/dist/mcp.d.ts.map +1 -0
  128. package/dist/mcp.js +8 -0
  129. package/dist/mcp.js.map +1 -0
  130. package/dist/memory.d.ts +87 -0
  131. package/dist/memory.d.ts.map +1 -0
  132. package/dist/memory.js +79 -0
  133. package/dist/memory.js.map +1 -0
  134. package/dist/models.d.ts +15 -0
  135. package/dist/models.d.ts.map +1 -0
  136. package/dist/models.js +19 -0
  137. package/dist/models.js.map +1 -0
  138. package/dist/openclaw-control-ui-origin.d.ts +23 -0
  139. package/dist/openclaw-control-ui-origin.d.ts.map +1 -0
  140. package/dist/openclaw-control-ui-origin.js +53 -0
  141. package/dist/openclaw-control-ui-origin.js.map +1 -0
  142. package/dist/renders.d.ts +187 -0
  143. package/dist/renders.d.ts.map +1 -0
  144. package/dist/renders.js +360 -0
  145. package/dist/renders.js.map +1 -0
  146. package/dist/routines.d.ts +59 -0
  147. package/dist/routines.d.ts.map +1 -0
  148. package/dist/routines.js +135 -0
  149. package/dist/routines.js.map +1 -0
  150. package/dist/runners.d.ts +61 -0
  151. package/dist/runners.d.ts.map +1 -0
  152. package/dist/runners.js +127 -0
  153. package/dist/runners.js.map +1 -0
  154. package/dist/sessions.d.ts +280 -0
  155. package/dist/sessions.d.ts.map +1 -0
  156. package/dist/sessions.js +292 -0
  157. package/dist/sessions.js.map +1 -0
  158. package/dist/skills.d.ts +170 -0
  159. package/dist/skills.d.ts.map +1 -0
  160. package/dist/skills.js +2 -0
  161. package/dist/skills.js.map +1 -0
  162. package/dist/user.d.ts +78 -0
  163. package/dist/user.d.ts.map +1 -0
  164. package/dist/user.js +143 -0
  165. package/dist/user.js.map +1 -0
  166. package/dist/version.d.ts +2 -0
  167. package/dist/version.d.ts.map +1 -0
  168. package/dist/version.js +3 -0
  169. package/dist/version.js.map +1 -0
  170. package/dist/voice-session.d.ts +66 -0
  171. package/dist/voice-session.d.ts.map +1 -0
  172. package/dist/voice-session.js +366 -0
  173. package/dist/voice-session.js.map +1 -0
  174. package/dist/voice-transcription-session.d.ts +64 -0
  175. package/dist/voice-transcription-session.d.ts.map +1 -0
  176. package/dist/voice-transcription-session.js +228 -0
  177. package/dist/voice-transcription-session.js.map +1 -0
  178. package/dist/voice.d.ts +62 -0
  179. package/dist/voice.d.ts.map +1 -0
  180. package/dist/voice.js +134 -0
  181. package/dist/voice.js.map +1 -0
  182. package/dist/workspace-tomd.d.ts +61 -0
  183. package/dist/workspace-tomd.d.ts.map +1 -0
  184. package/dist/workspace-tomd.js +177 -0
  185. package/dist/workspace-tomd.js.map +1 -0
  186. package/dist/workspaces.d.ts +255 -0
  187. package/dist/workspaces.d.ts.map +1 -0
  188. package/dist/workspaces.js +653 -0
  189. package/dist/workspaces.js.map +1 -0
  190. package/package.json +115 -0
  191. package/runtime/config-require.browser.js +4 -0
  192. package/runtime/config-require.d.ts +2 -0
  193. package/runtime/config-require.node.js +8 -0
@@ -0,0 +1,1563 @@
1
+ import { BaseJob } from './base.js';
2
+ import { COMFYUI_IMAGE } from '../config.js';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { basename, join } from 'path';
5
+ import { requestWithRetry } from '../http.js';
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ /**
10
+ * Load a workflow template JSON in graph format (nodes array, links array).
11
+ *
12
+ * The Python SDK resolves IDs via the comfyui-workflow-templates pip package;
13
+ * Node has no equivalent package, so `templateId` must resolve to a JSON file:
14
+ * an explicit path, `<templateId>.json`, or — when `templatesDir` is given —
15
+ * `<templatesDir>/<templateId>.json` or `<templatesDir>/<templateId>/<templateId>.json`.
16
+ */
17
+ export function loadTemplate(templateId, options = {}) {
18
+ const candidates = [templateId, `${templateId}.json`];
19
+ if (options.templatesDir) {
20
+ candidates.push(join(options.templatesDir, `${templateId}.json`), join(options.templatesDir, templateId, `${templateId}.json`));
21
+ }
22
+ for (const candidate of candidates) {
23
+ if (existsSync(candidate)) {
24
+ return JSON.parse(readFileSync(candidate, 'utf8'));
25
+ }
26
+ }
27
+ throw new Error(`Workflow template not found for "${templateId}". ` +
28
+ 'Provide a path to the template JSON, name the file <templateId>.json, ' +
29
+ 'or pass templatesDir containing <templateId>.json.');
30
+ }
31
+ /** Check whether a widget value matches the expected input type. */
32
+ export function valueMatchesType(value, inputSpec) {
33
+ if (inputSpec === undefined || inputSpec === null) {
34
+ return true;
35
+ }
36
+ // inputSpec is typically [type, config] or just [type]
37
+ if (Array.isArray(inputSpec) && inputSpec.length > 0) {
38
+ const typeInfo = inputSpec[0];
39
+ const config = inputSpec.length > 1 ? inputSpec[1] : {};
40
+ // List of allowed values (enum/combo) - older format
41
+ if (Array.isArray(typeInfo)) {
42
+ // Accept if value is in list, or if value is a string (enum values may differ between versions)
43
+ if (typeInfo.includes(value)) {
44
+ return true;
45
+ }
46
+ if (typeof value === 'string' && typeInfo.some((v) => typeof v === 'string')) {
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ // Type string
52
+ if (typeof typeInfo === 'string') {
53
+ // COMBO type - options are in config["options"] (newer ComfyUI format)
54
+ if (typeInfo === 'COMBO') {
55
+ const options = config && typeof config === 'object' && Array.isArray(config.options) ? config.options : [];
56
+ if (options.includes(value)) {
57
+ return true;
58
+ }
59
+ // Accept any string for combo - versions may have different allowed values
60
+ if (typeof value === 'string' && options.length > 0 && options.some((v) => typeof v === 'string')) {
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ if (typeInfo === 'INT' || typeInfo === 'FLOAT') {
66
+ return typeof value === 'number' && Number.isFinite(value);
67
+ }
68
+ if (typeInfo === 'STRING') {
69
+ return typeof value === 'string';
70
+ }
71
+ if (typeInfo === 'BOOLEAN') {
72
+ return typeof value === 'boolean';
73
+ }
74
+ // Connection types (MODEL, CLIP, VIDEO, etc.) are handled via links, not widgets
75
+ if (/^[A-Z0-9_]+$/.test(typeInfo)) {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ return true;
81
+ }
82
+ /**
83
+ * Expand subgraph/group nodes into their constituent nodes.
84
+ *
85
+ * ComfyUI supports "workflow components" (group nodes) where a subgraph is
86
+ * collapsed into a single node with a UUID type. These must be expanded before
87
+ * the workflow can be executed. Returns a new graph; the input is not modified.
88
+ */
89
+ export function expandSubgraphs(graph, debug = false) {
90
+ const log = debug ? console.debug.bind(console) : () => undefined;
91
+ // Check whether there are subgraph definitions
92
+ const subgraphs = {};
93
+ for (const sg of graph?.definitions?.subgraphs ?? []) {
94
+ subgraphs[sg.id] = sg;
95
+ }
96
+ if (Object.keys(subgraphs).length === 0) {
97
+ return graph; // No subgraphs, return as-is
98
+ }
99
+ graph = JSON.parse(JSON.stringify(graph));
100
+ const nodes = graph.nodes ?? [];
101
+ const links = graph.links ?? [];
102
+ const newNodes = [];
103
+ const newLinks = [];
104
+ const nodesToRemove = new Set();
105
+ const linksToRemove = new Set();
106
+ let nextNodeId = (graph.last_node_id ?? 0) + 1;
107
+ let nextLinkId = (graph.last_link_id ?? 0) + 1;
108
+ const getLinkFields = (lnk) => {
109
+ if (lnk && typeof lnk === 'object' && !Array.isArray(lnk)) {
110
+ return [lnk.id, lnk.origin_id, lnk.origin_slot ?? 0, lnk.target_id, lnk.target_slot ?? 0, lnk.type ?? ''];
111
+ }
112
+ return [lnk[0], lnk[1], lnk[2], lnk[3], lnk[4], lnk[5] ?? ''];
113
+ };
114
+ for (const node of nodes) {
115
+ const nodeType = String(node?.type ?? '');
116
+ const nodeId = node?.id;
117
+ const mode = node?.mode ?? 0;
118
+ if (!(nodeType in subgraphs)) {
119
+ continue;
120
+ }
121
+ // Skip bypassed/muted group nodes
122
+ if (mode === 2 || mode === 4) {
123
+ log(`Skipping bypassed group node ${nodeId}`);
124
+ nodesToRemove.add(nodeId);
125
+ for (const link of links) {
126
+ if (link[1] === nodeId || link[3] === nodeId) {
127
+ linksToRemove.add(link[0]);
128
+ }
129
+ }
130
+ continue;
131
+ }
132
+ log(`Expanding group node ${nodeId} -> subgraph ${nodeType.slice(0, 20)}...`);
133
+ const sg = subgraphs[nodeType];
134
+ const sgNodes = sg.nodes ?? [];
135
+ const sgLinks = sg.links ?? [];
136
+ const idMap = {};
137
+ for (const sgNode of sgNodes) {
138
+ idMap[String(sgNode.id)] = nextNodeId;
139
+ nextNodeId += 1;
140
+ }
141
+ // Apply widget values from proxyWidgets
142
+ const proxyWidgets = node?.properties?.proxyWidgets ?? [];
143
+ const widgetValues = node?.widgets_values ?? [];
144
+ const widgetOverrides = {};
145
+ for (let index = 0; index < proxyWidgets.length && index < widgetValues.length; index += 1) {
146
+ const [targetId, widgetName] = proxyWidgets[index];
147
+ const key = String(targetId);
148
+ if (!widgetOverrides[key]) {
149
+ widgetOverrides[key] = {};
150
+ }
151
+ widgetOverrides[key][widgetName] = widgetValues[index];
152
+ }
153
+ for (const sgNode of sgNodes) {
154
+ const oldId = sgNode.id;
155
+ const newNode = JSON.parse(JSON.stringify(sgNode));
156
+ newNode.id = idMap[String(oldId)];
157
+ const overrides = widgetOverrides[String(oldId)];
158
+ if (overrides) {
159
+ const sgWidgets = newNode.widgets_values ?? [];
160
+ if (!Array.isArray(sgWidgets) && typeof sgWidgets === 'object') {
161
+ Object.assign(sgWidgets, overrides);
162
+ }
163
+ else {
164
+ const nodeTypeInner = newNode.type ?? '';
165
+ if ('text' in overrides && (nodeTypeInner === 'CLIPTextEncode' || nodeTypeInner === 'CLIPTextEncodeFlux')) {
166
+ if (sgWidgets.length) {
167
+ sgWidgets[0] = overrides.text;
168
+ }
169
+ else {
170
+ newNode.widgets_values = [overrides.text];
171
+ }
172
+ }
173
+ }
174
+ }
175
+ newNodes.push(newNode);
176
+ }
177
+ // Copy subgraph links with remapped IDs
178
+ const linkIdMap = {};
179
+ for (const sgLink of sgLinks) {
180
+ const [oldLinkId, fromNode, fromSlot, toNode, toSlot, linkType] = getLinkFields(sgLink);
181
+ const newFrom = idMap[String(fromNode)] ?? fromNode;
182
+ const newTo = idMap[String(toNode)] ?? toNode;
183
+ newLinks.push([nextLinkId, newFrom, fromSlot, newTo, toSlot, linkType]);
184
+ linkIdMap[String(oldLinkId)] = nextLinkId;
185
+ nextLinkId += 1;
186
+ }
187
+ // Update node input link references using the link ID mapping
188
+ for (const newNode of newNodes) {
189
+ for (const input of newNode.inputs ?? []) {
190
+ const oldLink = input.link;
191
+ if (oldLink !== null && oldLink !== undefined && String(oldLink) in linkIdMap) {
192
+ input.link = linkIdMap[String(oldLink)];
193
+ }
194
+ }
195
+ }
196
+ // Rewire external connections: group node inputs -> subgraph input nodes
197
+ const sgInputs = sg.inputs ?? [];
198
+ for (const input of node.inputs ?? []) {
199
+ const linkId = input.link;
200
+ if (linkId === null || linkId === undefined)
201
+ continue;
202
+ const inputName = input.name;
203
+ for (const sgInput of sgInputs) {
204
+ if (sgInput.name === inputName || sgInput.label === input.label) {
205
+ for (const sgLinkId of sgInput.linkIds ?? []) {
206
+ for (const sgLink of sgLinks) {
207
+ const fields = getLinkFields(sgLink);
208
+ if (String(fields[0]) === String(sgLinkId)) {
209
+ const targetNode = idMap[String(fields[3])] ?? fields[3];
210
+ const targetSlot = fields[4];
211
+ for (const link of links) {
212
+ if (link[0] === linkId) {
213
+ link[3] = targetNode;
214
+ link[4] = targetSlot;
215
+ break;
216
+ }
217
+ }
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ // Group node outputs -> subgraph output nodes
227
+ const sgOutputs = sg.outputs ?? [];
228
+ for (const output of node.outputs ?? []) {
229
+ const outLinks = output.links ?? [];
230
+ if (!outLinks.length)
231
+ continue;
232
+ for (const sgOutput of sgOutputs) {
233
+ let done = false;
234
+ for (const sgLinkId of sgOutput.linkIds ?? []) {
235
+ for (const sgLink of sgLinks) {
236
+ const fields = getLinkFields(sgLink);
237
+ if (String(fields[0]) === String(sgLinkId)) {
238
+ const sourceNode = idMap[String(fields[1])] ?? fields[1];
239
+ const sourceSlot = fields[2];
240
+ for (const extLinkId of outLinks) {
241
+ for (const link of links) {
242
+ if (link[0] === extLinkId) {
243
+ link[1] = sourceNode;
244
+ link[2] = sourceSlot;
245
+ break;
246
+ }
247
+ }
248
+ }
249
+ done = true;
250
+ break;
251
+ }
252
+ }
253
+ if (done)
254
+ break;
255
+ }
256
+ if (done)
257
+ break;
258
+ }
259
+ }
260
+ nodesToRemove.add(nodeId);
261
+ }
262
+ graph.nodes = nodes.filter((node) => !nodesToRemove.has(node.id)).concat(newNodes);
263
+ graph.links = links.filter((link) => !linksToRemove.has(link[0])).concat(newLinks);
264
+ graph.last_node_id = nextNodeId;
265
+ graph.last_link_id = nextLinkId;
266
+ log(`Expanded ${nodesToRemove.size} group nodes, added ${newNodes.length} nodes`);
267
+ return graph;
268
+ }
269
+ // Default object_info for offline workflow conversion (no running instance needed)
270
+ // This covers common node types - extend as needed for new workflows
271
+ export const DEFAULT_OBJECT_INFO = {
272
+ // Text encoders
273
+ CLIPTextEncode: {
274
+ input: { required: { clip: ['CLIP'], text: ['STRING', { multiline: true }] }, optional: {} },
275
+ input_order: { required: ['clip', 'text'], optional: [] },
276
+ },
277
+ CLIPLoader: {
278
+ input: { required: { clip_name: [['model.safetensors'], {}], type: [['stable_diffusion', 'wan'], {}], device: [['default', 'cpu'], {}] }, optional: {} },
279
+ input_order: { required: ['clip_name', 'type', 'device'], optional: [] },
280
+ },
281
+ QuadrupleCLIPLoader: {
282
+ input: { required: { clip_name1: [['clip.safetensors'], {}], clip_name2: [['clip.safetensors'], {}], clip_name3: [['clip.safetensors'], {}], clip_name4: [['clip.safetensors'], {}] }, optional: {} },
283
+ input_order: { required: ['clip_name1', 'clip_name2', 'clip_name3', 'clip_name4'], optional: [] },
284
+ },
285
+ AudioEncoderLoader: {
286
+ input: { required: { audio_encoder_name: [['whisper_large_v3_fp16.safetensors'], {}] }, optional: {} },
287
+ input_order: { required: ['audio_encoder_name'], optional: [] },
288
+ },
289
+ WanHuMoImageToVideo: {
290
+ input: { required: { positive: ['CONDITIONING'], negative: ['CONDITIONING'], vae: ['VAE'], width: ['INT', { default: 640 }], height: ['INT', { default: 640 }], length: ['INT', { default: 97 }], batch_size: ['INT', { default: 1 }] }, optional: { audio_encoder_output: ['AUDIO_ENCODER_OUTPUT'], ref_image: ['IMAGE'] } },
291
+ input_order: { required: ['positive', 'negative', 'vae', 'width', 'height', 'length', 'batch_size'], optional: ['audio_encoder_output', 'ref_image'] },
292
+ },
293
+ TextEncodeQwenImageEditPlus: {
294
+ input: {
295
+ required: { clip: ['CLIP'], vae: ['VAE'], prompt: ['STRING', { multiline: true }] },
296
+ optional: { image1: ['IMAGE'], image2: ['IMAGE'], image3: ['IMAGE'] },
297
+ },
298
+ input_order: { required: ['clip', 'vae', 'prompt'], optional: ['image1', 'image2', 'image3'] },
299
+ },
300
+ ModelSamplingAuraFlow: {
301
+ input: { required: { model: ['MODEL'], shift: ['FLOAT', { default: 1.73 }] }, optional: {} },
302
+ input_order: { required: ['model', 'shift'], optional: [] },
303
+ },
304
+ CFGNorm: {
305
+ input: { required: { model: ['MODEL'], strength: ['FLOAT', { default: 1.0 }] }, optional: {} },
306
+ input_order: { required: ['model', 'strength'], optional: [] },
307
+ },
308
+ FluxKontextImageScale: {
309
+ input: { required: { image: ['IMAGE'], max_pixels: ['INT', { default: 1048576 }] }, optional: {} },
310
+ input_order: { required: ['image', 'max_pixels'], optional: [] },
311
+ },
312
+ ReferenceLatent: {
313
+ input: { required: { conditioning: ['CONDITIONING'], latent: ['LATENT'] }, optional: {} },
314
+ input_order: { required: ['conditioning', 'latent'], optional: [] },
315
+ },
316
+ KSamplerAdvanced: {
317
+ input: {
318
+ required: {
319
+ model: ['MODEL'], positive: ['CONDITIONING'], negative: ['CONDITIONING'], latent_image: ['LATENT'],
320
+ add_noise: [['enable', 'disable'], {}], noise_seed: ['INT', { default: 0 }],
321
+ steps: ['INT', { default: 20 }], cfg: ['FLOAT', { default: 8.0 }],
322
+ sampler_name: [['euler', 'euler_ancestral'], {}], scheduler: [['normal', 'simple'], {}],
323
+ start_at_step: ['INT', { default: 0 }], end_at_step: ['INT', { default: 10000 }],
324
+ return_with_leftover_noise: [['disable', 'enable'], {}],
325
+ },
326
+ optional: {},
327
+ },
328
+ input_order: {
329
+ required: ['model', 'positive', 'negative', 'latent_image', 'add_noise', 'noise_seed', 'steps', 'cfg', 'sampler_name', 'scheduler', 'start_at_step', 'end_at_step', 'return_with_leftover_noise'],
330
+ optional: [],
331
+ },
332
+ },
333
+ EmptySD3LatentImage: {
334
+ input: { required: { width: ['INT', {}], height: ['INT', {}], batch_size: ['INT', {}] }, optional: {} },
335
+ input_order: { required: ['width', 'height', 'batch_size'], optional: [] },
336
+ },
337
+ EmptyHunyuanLatentVideo: {
338
+ input: { required: { width: ['INT', {}], height: ['INT', {}], length: ['INT', {}], batch_size: ['INT', {}] }, optional: {} },
339
+ input_order: { required: ['width', 'height', 'length', 'batch_size'], optional: [] },
340
+ },
341
+ WanImageToVideo: {
342
+ input: { required: { width: ['INT', {}], height: ['INT', {}], length: ['INT', {}], batch_size: ['INT', {}] }, optional: {} },
343
+ input_order: { required: ['width', 'height', 'length', 'batch_size'], optional: [] },
344
+ },
345
+ WanStartEndFrames: {
346
+ input: { required: { width: ['INT', {}], height: ['INT', {}], length: ['INT', {}], batch_size: ['INT', {}] }, optional: {} },
347
+ input_order: { required: ['width', 'height', 'length', 'batch_size'], optional: [] },
348
+ },
349
+ WanFirstLastFrameToVideo: {
350
+ input: { required: { width: ['INT', {}], height: ['INT', {}], length: ['INT', {}], batch_size: ['INT', {}], positive: ['CONDITIONING'], negative: ['CONDITIONING'], vae: ['VAE'], start_image: ['IMAGE'], end_image: ['IMAGE'] }, optional: {} },
351
+ input_order: { required: ['width', 'height', 'length', 'batch_size', 'positive', 'negative', 'vae', 'start_image', 'end_image'], optional: [] },
352
+ },
353
+ // Samplers
354
+ KSampler: {
355
+ input: {
356
+ required: {
357
+ model: ['MODEL'], positive: ['CONDITIONING'], negative: ['CONDITIONING'], latent_image: ['LATENT'],
358
+ seed: ['INT', { default: 0 }], steps: ['INT', { default: 20 }], cfg: ['FLOAT', { default: 8.0 }],
359
+ sampler_name: [['euler', 'euler_ancestral', 'dpm_2'], {}], scheduler: [['normal', 'karras', 'simple'], {}],
360
+ denoise: ['FLOAT', { default: 1.0 }],
361
+ },
362
+ optional: {},
363
+ },
364
+ input_order: { required: ['model', 'positive', 'negative', 'latent_image', 'seed', 'steps', 'cfg', 'sampler_name', 'scheduler', 'denoise'], optional: [] },
365
+ },
366
+ // Latent generators
367
+ EmptyLatentImage: {
368
+ input: { required: { width: ['INT', {}], height: ['INT', {}], batch_size: ['INT', {}] }, optional: {} },
369
+ input_order: { required: ['width', 'height', 'batch_size'], optional: [] },
370
+ },
371
+ // Model loaders
372
+ UNETLoader: {
373
+ input: { required: { unet_name: [['model.safetensors'], {}], weight_dtype: [['default', 'fp8_e4m3fn'], {}] }, optional: {} },
374
+ input_order: { required: ['unet_name', 'weight_dtype'], optional: [] },
375
+ },
376
+ VAELoader: {
377
+ input: { required: { vae_name: [['vae.safetensors'], {}] }, optional: {} },
378
+ input_order: { required: ['vae_name'], optional: [] },
379
+ },
380
+ CheckpointLoaderSimple: {
381
+ input: { required: { ckpt_name: [['model.safetensors'], {}] }, optional: {} },
382
+ input_order: { required: ['ckpt_name'], optional: [] },
383
+ },
384
+ // Video/Image processing
385
+ VAEDecode: {
386
+ input: { required: { samples: ['LATENT'], vae: ['VAE'] }, optional: {} },
387
+ input_order: { required: ['samples', 'vae'], optional: [] },
388
+ },
389
+ // Save nodes
390
+ SaveImage: {
391
+ input: { required: { images: ['IMAGE'], filename_prefix: ['STRING', {}] }, optional: {} },
392
+ input_order: { required: ['images', 'filename_prefix'], optional: [] },
393
+ },
394
+ // Input loaders
395
+ LoadImage: {
396
+ input: { required: { image: ['STRING', {}] }, optional: {} },
397
+ input_order: { required: ['image'], optional: [] },
398
+ },
399
+ LoadAudio: {
400
+ input: { required: { audio: ['STRING', {}] }, optional: {} },
401
+ input_order: { required: ['audio'], optional: [] },
402
+ },
403
+ LoraLoaderModelOnly: {
404
+ input: { required: { model: ['MODEL'], lora_name: [['lora.safetensors'], {}], strength_model: ['FLOAT', {}] }, optional: {} },
405
+ input_order: { required: ['model', 'lora_name', 'strength_model'], optional: [] },
406
+ },
407
+ ModelSamplingSD3: {
408
+ input: { required: { model: ['MODEL'], shift: ['FLOAT', {}] }, optional: {} },
409
+ input_order: { required: ['model', 'shift'], optional: [] },
410
+ },
411
+ // Video/Image processing
412
+ VAEEncode: {
413
+ input: { required: { pixels: ['IMAGE'], vae: ['VAE'] }, optional: {} },
414
+ input_order: { required: ['pixels', 'vae'], optional: [] },
415
+ },
416
+ CreateVideo: {
417
+ input: { required: { images: ['IMAGE'], fps: ['FLOAT', { default: 16 }] }, optional: { audio: ['AUDIO'] } },
418
+ input_order: { required: ['images', 'fps'], optional: ['audio'] },
419
+ },
420
+ SaveVideo: {
421
+ input: {
422
+ required: {
423
+ video: ['VIDEO'],
424
+ filename_prefix: ['STRING', { default: 'video/ComfyUI' }],
425
+ format: ['COMBO', { default: 'auto', options: ['auto', 'mp4'] }],
426
+ codec: ['COMBO', { default: 'auto', options: ['auto', 'h264'] }],
427
+ },
428
+ optional: {},
429
+ },
430
+ input_order: { required: ['video', 'filename_prefix', 'format', 'codec'], optional: [] },
431
+ },
432
+ SaveAnimatedWEBP: {
433
+ input: { required: { images: ['IMAGE'], filename_prefix: ['STRING', {}], fps: ['FLOAT', {}], lossless: ['BOOLEAN', {}], quality: ['INT', {}], method: [['default'], {}] }, optional: {} },
434
+ input_order: { required: ['images', 'filename_prefix', 'fps', 'lossless', 'quality', 'method'], optional: [] },
435
+ },
436
+ };
437
+ /**
438
+ * Find nodes in API-format workflow by class_type and optional title pattern
439
+ */
440
+ export function findNodes(workflow, classType, titleContains) {
441
+ const results = [];
442
+ for (const [nodeId, node] of Object.entries(workflow)) {
443
+ if (node.class_type === classType) {
444
+ if (!titleContains) {
445
+ results.push([nodeId, node]);
446
+ }
447
+ else {
448
+ const title = node._meta?.title || '';
449
+ if (title.toLowerCase().includes(titleContains.toLowerCase())) {
450
+ results.push([nodeId, node]);
451
+ }
452
+ }
453
+ }
454
+ }
455
+ return results;
456
+ }
457
+ /**
458
+ * Find first node matching class_type and optional title pattern
459
+ */
460
+ export function findNode(workflow, classType, titleContains) {
461
+ const nodes = findNodes(workflow, classType, titleContains);
462
+ return nodes.length > 0 ? nodes[0] : [null, null];
463
+ }
464
+ /**
465
+ * Apply parameters to workflow nodes
466
+ */
467
+ export function applyParams(workflow, params) {
468
+ const clipTypes = ['CLIPTextEncode', 'CLIPTextEncodeFlux', 'CLIPTextEncodeSD3', 'TextEncodeQwenImageEditPlus'];
469
+ // Helper to find first matching node from a list of types
470
+ const findFirst = (types, title) => {
471
+ for (const t of types) {
472
+ const result = findNode(workflow, t, title);
473
+ if (result[0])
474
+ return result;
475
+ }
476
+ return [null, null];
477
+ };
478
+ // Positive prompt
479
+ if (params.prompt) {
480
+ let [_nodeId, node] = findNode(workflow, 'TextEncodeQwenImageEditPlus', 'Positive');
481
+ if (node) {
482
+ node.inputs.prompt = params.prompt;
483
+ }
484
+ else {
485
+ [_nodeId, node] = findFirst(clipTypes, 'Positive');
486
+ if (!node) {
487
+ for (const t of clipTypes) {
488
+ const nodes = findNodes(workflow, t);
489
+ if (nodes.length > 0) {
490
+ [_nodeId, node] = nodes[0];
491
+ break;
492
+ }
493
+ }
494
+ }
495
+ if (node) {
496
+ node.inputs.text = params.prompt;
497
+ }
498
+ }
499
+ }
500
+ // Negative prompt
501
+ if (params.negative) {
502
+ let [_nodeId, node] = findNode(workflow, 'TextEncodeQwenImageEditPlus', 'Negative');
503
+ if (node) {
504
+ node.inputs.prompt = params.negative;
505
+ }
506
+ else {
507
+ [_nodeId, node] = findFirst(clipTypes, 'Negative');
508
+ if (node) {
509
+ node.inputs.text = params.negative;
510
+ }
511
+ }
512
+ }
513
+ // Width/Height/Length
514
+ if (params.width || params.height || params.length) {
515
+ const latentTypes = [
516
+ 'EmptySD3LatentImage', 'EmptyFlux2LatentImage', 'EmptyLatentImage',
517
+ 'EmptyHunyuanLatentVideo', 'EmptyMochiLatentVideo', 'EmptyLTXVLatentVideo',
518
+ 'WanImageToVideo', 'WanStartEndFrames', 'WanFirstLastFrameToVideo', 'WanHuMoImageToVideo',
519
+ ];
520
+ const [_nodeId, node] = findFirst(latentTypes);
521
+ if (node) {
522
+ if (params.width)
523
+ node.inputs.width = params.width;
524
+ if (params.height)
525
+ node.inputs.height = params.height;
526
+ if (params.length)
527
+ node.inputs.length = params.length;
528
+ if (params.batch_size)
529
+ node.inputs.batch_size = params.batch_size;
530
+ }
531
+ else {
532
+ // Try PrimitiveNode with "width"/"height" title (Flux2 style)
533
+ if (params.width) {
534
+ const [_wid, widthNode] = findNode(workflow, 'PrimitiveNode', 'width');
535
+ if (widthNode && 'value' in (widthNode.inputs ?? {})) {
536
+ widthNode.inputs.value = params.width;
537
+ }
538
+ }
539
+ if (params.height) {
540
+ const [_hid, heightNode] = findNode(workflow, 'PrimitiveNode', 'height');
541
+ if (heightNode && 'value' in (heightNode.inputs ?? {})) {
542
+ heightNode.inputs.value = params.height;
543
+ }
544
+ }
545
+ }
546
+ }
547
+ // Seed
548
+ if (params.seed !== undefined) {
549
+ const [_nodeId, node] = findNode(workflow, 'KSampler');
550
+ if (node) {
551
+ node.inputs.seed = params.seed;
552
+ }
553
+ else {
554
+ const advancedNodes = findNodes(workflow, 'KSamplerAdvanced');
555
+ let targetNode = null;
556
+ for (const [_nid, n] of advancedNodes) {
557
+ if (n.inputs.add_noise === 'enable') {
558
+ targetNode = n;
559
+ break;
560
+ }
561
+ }
562
+ if (!targetNode && advancedNodes.length > 0) {
563
+ targetNode = advancedNodes[0][1];
564
+ }
565
+ if (targetNode) {
566
+ targetNode.inputs.noise_seed = params.seed;
567
+ }
568
+ else {
569
+ // Try other sampler types
570
+ const [_rnid, randomNoiseNode] = findNode(workflow, 'RandomNoise');
571
+ if (randomNoiseNode) {
572
+ randomNoiseNode.inputs.noise_seed = params.seed;
573
+ }
574
+ }
575
+ }
576
+ }
577
+ // Steps
578
+ if (params.steps !== undefined) {
579
+ const [_nodeId, node] = findFirst(['KSampler', 'KSamplerAdvanced', 'SamplerCustom', 'SamplerCustomAdvanced']);
580
+ if (node) {
581
+ node.inputs.steps = params.steps;
582
+ }
583
+ else {
584
+ const [_fsid, fluxScheduler] = findNode(workflow, 'Flux2Scheduler');
585
+ if (fluxScheduler) {
586
+ fluxScheduler.inputs.steps = params.steps;
587
+ }
588
+ }
589
+ }
590
+ // CFG
591
+ if (params.cfg !== undefined) {
592
+ const [_nodeId, node] = findFirst(['KSampler', 'KSamplerAdvanced', 'SamplerCustom', 'SamplerCustomAdvanced']);
593
+ if (node) {
594
+ node.inputs.cfg = params.cfg;
595
+ }
596
+ else {
597
+ const [_fgid, fluxGuidance] = findNode(workflow, 'FluxGuidance');
598
+ if (fluxGuidance) {
599
+ fluxGuidance.inputs.guidance = params.cfg;
600
+ }
601
+ }
602
+ }
603
+ // Filename prefix
604
+ if (params.filename_prefix) {
605
+ const [_nodeId, node] = findFirst(['SaveImage', 'SaveVideo', 'SaveAnimatedWEBP', 'SaveAnimatedPNG']);
606
+ if (node) {
607
+ node.inputs.filename_prefix = params.filename_prefix;
608
+ }
609
+ }
610
+ // Node-specific params
611
+ if (params.nodes) {
612
+ for (const [nodeId, values] of Object.entries(params.nodes)) {
613
+ const node = workflow[nodeId];
614
+ if (!node)
615
+ continue;
616
+ const nodeType = node.class_type || '';
617
+ for (const [key, value] of Object.entries(values)) {
618
+ if (key === 'image' && nodeType === 'LoadImage') {
619
+ node.inputs.image = value;
620
+ }
621
+ else if (key === 'audio' && nodeType === 'LoadAudio') {
622
+ node.inputs.audio = value;
623
+ }
624
+ else if (key === 'text' && nodeType.includes('Text')) {
625
+ node.inputs.text = value;
626
+ }
627
+ else {
628
+ node.inputs[key] = value;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ return workflow;
634
+ }
635
+ /**
636
+ * Convert ComfyUI graph format (from UI) to API format (for /prompt endpoint).
637
+ */
638
+ export function graphToApi(graph, objectInfo, debug = false) {
639
+ const log = debug ? console.debug.bind(console) : () => undefined;
640
+ // First, expand any subgraphs/group nodes
641
+ graph = expandSubgraphs(graph, debug);
642
+ if (!objectInfo) {
643
+ objectInfo = DEFAULT_OBJECT_INFO;
644
+ }
645
+ const api = {};
646
+ const nodesById = {};
647
+ const links = {};
648
+ // Build lookups
649
+ for (const node of graph.nodes || []) {
650
+ nodesById[node.id] = node;
651
+ }
652
+ for (const link of graph.links || []) {
653
+ const [linkId, fromNode, fromSlot] = link;
654
+ links[linkId] = [fromNode, fromSlot];
655
+ }
656
+ const isSkippedNode = (node) => {
657
+ if (!node)
658
+ return true;
659
+ const classType = node.type;
660
+ if (!classType || ['Note', 'Reroute', 'MarkdownNote'].includes(classType)) {
661
+ return true;
662
+ }
663
+ // mode 2 = muted, mode 4 = bypassed
664
+ if ([2, 4].includes(node.mode ?? 0)) {
665
+ return true;
666
+ }
667
+ return false;
668
+ };
669
+ // Follow a link through skipped nodes (reroutes, bypassed) to the real source.
670
+ const resolveLink = (linkId, visited = new Set()) => {
671
+ const key = String(linkId);
672
+ if (visited.has(key))
673
+ return null; // Cycle detection
674
+ visited.add(key);
675
+ if (!(key in links))
676
+ return null;
677
+ const [fromNodeId, fromSlot] = links[key];
678
+ const fromNode = nodesById[fromNodeId];
679
+ if (!isSkippedNode(fromNode)) {
680
+ return [fromNodeId, fromSlot];
681
+ }
682
+ // Node is skipped - follow through its input; output slot 0 passes through input slot 0
683
+ if (fromSlot === 0 && fromNode) {
684
+ const nodeInputs = fromNode.inputs ?? [];
685
+ if (nodeInputs.length) {
686
+ const upstreamLink = nodeInputs[0].link;
687
+ if (upstreamLink !== null && upstreamLink !== undefined) {
688
+ return resolveLink(upstreamLink, visited);
689
+ }
690
+ }
691
+ }
692
+ return null;
693
+ };
694
+ // Convert nodes
695
+ for (const node of graph.nodes || []) {
696
+ const nodeId = String(node.id);
697
+ const classType = node.type;
698
+ if (!classType || ['Note', 'Reroute', 'MarkdownNote'].includes(classType)) {
699
+ continue;
700
+ }
701
+ if ([2, 4].includes(node.mode ?? 0)) {
702
+ continue; // Skip muted/bypassed
703
+ }
704
+ const info = objectInfo[classType] || {};
705
+ const inputs = {};
706
+ // Get input specs from schema
707
+ const inputSpecs = {};
708
+ for (const section of ['required', 'optional']) {
709
+ for (const [name, spec] of Object.entries(info.input?.[section] ?? {})) {
710
+ inputSpecs[name] = spec;
711
+ }
712
+ }
713
+ // Get input order from schema; fall back to input spec keys for older ComfyUI
714
+ const inputOrder = [...(info.input_order?.required || []), ...(info.input_order?.optional || [])];
715
+ if (inputOrder.length === 0 && Object.keys(inputSpecs).length > 0) {
716
+ inputOrder.push(...Object.keys(info.input?.required ?? {}), ...Object.keys(info.input?.optional ?? {}));
717
+ }
718
+ // Map connections
719
+ const connectedInputs = new Set();
720
+ for (const inp of node.inputs || []) {
721
+ const linkId = inp.link;
722
+ if (linkId !== null && linkId !== undefined) {
723
+ const resolved = resolveLink(linkId);
724
+ if (resolved) {
725
+ const [fromNode, fromSlot] = resolved;
726
+ inputs[inp.name] = [String(fromNode), fromSlot];
727
+ connectedInputs.add(inp.name);
728
+ }
729
+ }
730
+ }
731
+ // Map widget values to unconnected inputs with type validation
732
+ const widgets = node.widgets_values ?? [];
733
+ if (!Array.isArray(widgets) && widgets && typeof widgets === 'object') {
734
+ // Some nodes use dict format for widgets
735
+ for (const [name, value] of Object.entries(widgets)) {
736
+ if (!connectedInputs.has(name)) {
737
+ inputs[name] = value;
738
+ }
739
+ }
740
+ }
741
+ else {
742
+ // List format - map positionally to input names, skipping UI-only widgets
743
+ let wIdx = 0;
744
+ for (const name of inputOrder) {
745
+ if (connectedInputs.has(name))
746
+ continue;
747
+ const inputSpec = inputSpecs[name];
748
+ while (wIdx < widgets.length) {
749
+ const value = widgets[wIdx];
750
+ wIdx += 1;
751
+ if (valueMatchesType(value, inputSpec)) {
752
+ inputs[name] = value;
753
+ break;
754
+ }
755
+ // Skip UI-only widgets (e.g., 'randomize', 'fixed', etc.)
756
+ }
757
+ }
758
+ }
759
+ log(`node ${nodeId} (${classType}) inputs: ${JSON.stringify(inputs)}`);
760
+ api[nodeId] = {
761
+ class_type: classType,
762
+ inputs,
763
+ _meta: { title: node.title || classType },
764
+ };
765
+ }
766
+ return api;
767
+ }
768
+ /**
769
+ * Apply graph modes (enable/disable nodes)
770
+ */
771
+ export function applyGraphModes(graph, nodesConfig) {
772
+ const nodesById = {};
773
+ for (const node of graph.nodes || []) {
774
+ nodesById[String(node.id)] = node;
775
+ }
776
+ for (const [nodeId, config] of Object.entries(nodesConfig)) {
777
+ const node = nodesById[nodeId];
778
+ if (!node)
779
+ continue;
780
+ if ('mode' in config) {
781
+ node.mode = config.mode;
782
+ }
783
+ else if ('enabled' in config) {
784
+ node.mode = config.enabled ? 0 : 4;
785
+ }
786
+ }
787
+ return graph;
788
+ }
789
+ /**
790
+ * ComfyUI-specific job with workflow execution helpers
791
+ */
792
+ export class ComfyUIJob extends BaseJob {
793
+ static DEFAULT_IMAGE = COMFYUI_IMAGE;
794
+ static DEFAULT_GPU_TYPE = 'l40s';
795
+ static HEALTH_ENDPOINT = '/system_stats';
796
+ static COMFYUI_PORT = 8188;
797
+ _objectInfo = null;
798
+ _jobToken = null;
799
+ _useLb;
800
+ useAuth;
801
+ template = null;
802
+ constructor(client, job, template, useLb = false, useAuth = false) {
803
+ super(client, job);
804
+ this.template = template || null;
805
+ this._useLb = useLb;
806
+ this.useAuth = useAuth;
807
+ }
808
+ get useLb() {
809
+ return this._useLb;
810
+ }
811
+ set useLb(value) {
812
+ this._useLb = value;
813
+ this._baseUrl = null;
814
+ }
815
+ get baseUrl() {
816
+ if (!this._baseUrl && this.hostname) {
817
+ if (this._useLb) {
818
+ this._baseUrl = `https://${this.hostname}`;
819
+ }
820
+ else {
821
+ this._baseUrl = `http://${this.hostname}:${ComfyUIJob.COMFYUI_PORT}`;
822
+ }
823
+ }
824
+ return this._baseUrl || '';
825
+ }
826
+ get authHeaders() {
827
+ if (this.useAuth) {
828
+ if (!this._jobToken) {
829
+ throw new Error('Job token not loaded. Call await job.jobToken() first.');
830
+ }
831
+ return { 'Authorization': `Bearer ${this._jobToken}` };
832
+ }
833
+ return { 'Authorization': `Bearer ${this.client.apiKey}` };
834
+ }
835
+ async jobToken() {
836
+ if (!this._jobToken) {
837
+ this._jobToken = await this.client.jobs.token(this.jobId);
838
+ }
839
+ return this._jobToken;
840
+ }
841
+ /**
842
+ * Create a new ComfyUI job configured for a specific template
843
+ */
844
+ static async createForTemplate(client, template, options = {}) {
845
+ const { gpuType, gpuCount = 1, runtime = 3600, lb, auth = false, ...kwargs } = options;
846
+ const env = kwargs.env || {};
847
+ env.COMFYUI_TEMPLATES = template;
848
+ const ports = {};
849
+ if (lb) {
850
+ ports.lb = lb;
851
+ }
852
+ else {
853
+ ports[String(ComfyUIJob.COMFYUI_PORT)] = ComfyUIJob.COMFYUI_PORT;
854
+ }
855
+ const job = await client.jobs.create({
856
+ image: ComfyUIJob.DEFAULT_IMAGE,
857
+ gpuType: gpuType || ComfyUIJob.DEFAULT_GPU_TYPE,
858
+ gpuCount,
859
+ runtime,
860
+ env,
861
+ ports,
862
+ auth,
863
+ ...kwargs,
864
+ });
865
+ return new ComfyUIJob(client, job, template, Boolean(lb), auth);
866
+ }
867
+ /**
868
+ * Get object_info from ComfyUI (cached)
869
+ */
870
+ async getObjectInfo(refresh = false) {
871
+ if (this._objectInfo === null || refresh) {
872
+ const response = await fetch(`${this.baseUrl}/object_info`, {
873
+ headers: await this.resolvedAuthHeaders(),
874
+ });
875
+ if (!response.ok) {
876
+ throw new Error(`Failed to get object_info: ${response.statusText}`);
877
+ }
878
+ this._objectInfo = (await response.json());
879
+ }
880
+ return this._objectInfo;
881
+ }
882
+ /**
883
+ * Convert graph format workflow to API format
884
+ */
885
+ async convertWorkflow(graph, debug = false) {
886
+ const objectInfo = await this.getObjectInfo();
887
+ return graphToApi(graph, objectInfo, debug);
888
+ }
889
+ /**
890
+ * Connect to a specific ComfyUI instance by job ID or hostname.
891
+ */
892
+ static async getInstance(client, instance, options = {}) {
893
+ let job = null;
894
+ // Check if it looks like a UUID (job ID)
895
+ if (instance.includes('-') && instance.length > 30) {
896
+ job = await client.jobs.get(instance);
897
+ }
898
+ else {
899
+ // Assume hostname - search running jobs
900
+ const jobs = await client.jobs.list('running');
901
+ for (const candidate of jobs) {
902
+ if (candidate.hostname && (candidate.hostname === instance || candidate.hostname.startsWith(instance))) {
903
+ job = candidate;
904
+ break;
905
+ }
906
+ }
907
+ if (!job) {
908
+ throw new Error(`No running job found with hostname: ${instance}`);
909
+ }
910
+ }
911
+ return new ComfyUIJob(client, job, undefined, Boolean(options.useLb), Boolean(options.useAuth));
912
+ }
913
+ /**
914
+ * Get existing running job or create new one for a template.
915
+ *
916
+ * If reuse is true and a ComfyUI job is already running, it will be reused
917
+ * (note: the existing job may have different models loaded).
918
+ */
919
+ static async getOrCreateForTemplate(client, template, options = {}) {
920
+ const { reuse = true, gpuType, gpuCount, runtime, lb, auth = false, ...kwargs } = options;
921
+ if (reuse) {
922
+ const existing = await ComfyUIJob.getRunning(client, ComfyUIJob.DEFAULT_IMAGE);
923
+ if (existing) {
924
+ existing.template = template;
925
+ existing.useLb = Boolean(lb);
926
+ existing.useAuth = auth;
927
+ return existing;
928
+ }
929
+ }
930
+ return ComfyUIJob.createForTemplate(client, template, {
931
+ gpuType,
932
+ gpuCount,
933
+ runtime,
934
+ lb,
935
+ auth,
936
+ ...kwargs,
937
+ });
938
+ }
939
+ /**
940
+ * Load workflow template JSON in graph format.
941
+ *
942
+ * Unlike the Python SDK, Node has no comfyui-workflow-templates package, so
943
+ * the ID must resolve to a JSON file (see the module-level loadTemplate).
944
+ */
945
+ loadTemplate(templateId, options = {}) {
946
+ return loadTemplate(templateId, options);
947
+ }
948
+ // =========================================================================
949
+ // ComfyUI Manager - Custom Node Installation
950
+ // =========================================================================
951
+ async comfyJson(method, path, options = {}) {
952
+ const response = await requestWithRetry({
953
+ method,
954
+ url: `${this.baseUrl}${path}`,
955
+ headers: {
956
+ ...(await this.resolvedAuthHeaders()),
957
+ ...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
958
+ },
959
+ params: options.params,
960
+ body: options.body,
961
+ timeout: options.timeout ?? 60000,
962
+ retries: options.retries ?? 3,
963
+ });
964
+ if (response.status >= 400) {
965
+ const text = await response.text();
966
+ throw new Error(`ComfyUI ${method} ${path} failed (${response.status}): ${text}`);
967
+ }
968
+ return response.json();
969
+ }
970
+ async resolvedAuthHeaders() {
971
+ if (this.useAuth) {
972
+ await this.jobToken();
973
+ }
974
+ return this.authHeaders;
975
+ }
976
+ static isTransientNetworkError(error) {
977
+ if (!(error instanceof Error))
978
+ return false;
979
+ // fetch() throws TypeError on connect/DNS failures; AbortError on timeout.
980
+ return error instanceof TypeError || error.name === 'AbortError';
981
+ }
982
+ /**
983
+ * Get set of available node class_types from ComfyUI's /object_info.
984
+ */
985
+ async getAvailableNodeTypes() {
986
+ const info = await this.comfyJson('GET', '/object_info', { timeout: 60000 });
987
+ return new Set(Object.keys(info ?? {}));
988
+ }
989
+ /**
990
+ * Extract all class_type values from a workflow (API format or UI format).
991
+ */
992
+ getWorkflowNodeTypes(workflow) {
993
+ const classTypes = new Set();
994
+ // UI format: {"nodes": [{"type": "..."}, ...]}
995
+ if (Array.isArray(workflow?.nodes)) {
996
+ for (const node of workflow.nodes) {
997
+ if (node && typeof node.type === 'string') {
998
+ classTypes.add(node.type);
999
+ }
1000
+ }
1001
+ }
1002
+ else {
1003
+ // API format: {"1": {"class_type": "..."}, ...}
1004
+ for (const nodeData of Object.values(workflow ?? {})) {
1005
+ if (nodeData && typeof nodeData === 'object' && typeof nodeData.class_type === 'string') {
1006
+ classTypes.add(nodeData.class_type);
1007
+ }
1008
+ }
1009
+ }
1010
+ return classTypes;
1011
+ }
1012
+ /**
1013
+ * Find node types in workflow that aren't available in ComfyUI.
1014
+ */
1015
+ async getMissingNodeTypes(workflow) {
1016
+ const available = await this.getAvailableNodeTypes();
1017
+ const required = this.getWorkflowNodeTypes(workflow);
1018
+ const missing = new Set();
1019
+ for (const nodeType of required) {
1020
+ if (!available.has(nodeType)) {
1021
+ missing.add(nodeType);
1022
+ }
1023
+ }
1024
+ return missing;
1025
+ }
1026
+ /**
1027
+ * Get node class_type to package mappings from ComfyUI Manager.
1028
+ *
1029
+ * Returns a mapping of package URL to [node_list, metadata].
1030
+ */
1031
+ async getNodeMappings() {
1032
+ return await this.comfyJson('GET', '/customnode/getmappings', { timeout: 60000 });
1033
+ }
1034
+ /**
1035
+ * Look up which packages provide the given node types.
1036
+ *
1037
+ * Returns a mapping of package URL to the node types it provides.
1038
+ */
1039
+ async lookupPackagesForNodes(nodeTypes) {
1040
+ const mappings = await this.getNodeMappings();
1041
+ // Invert the mapping: node_type -> package_url
1042
+ const nodeToPackage = {};
1043
+ for (const [url, data] of Object.entries(mappings ?? {})) {
1044
+ if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
1045
+ for (const node of data[0]) {
1046
+ nodeToPackage[node] = url;
1047
+ }
1048
+ }
1049
+ }
1050
+ // Group requested nodes by package
1051
+ const packages = {};
1052
+ for (const nodeType of nodeTypes) {
1053
+ const pkg = nodeToPackage[nodeType];
1054
+ if (pkg) {
1055
+ (packages[pkg] ??= []).push(nodeType);
1056
+ }
1057
+ }
1058
+ return packages;
1059
+ }
1060
+ /**
1061
+ * Get the full list of available custom node packages.
1062
+ */
1063
+ async getCustomNodeList() {
1064
+ return await this.comfyJson('GET', '/customnode/getlist', {
1065
+ params: { skip_update: 'true' },
1066
+ timeout: 60000,
1067
+ });
1068
+ }
1069
+ /**
1070
+ * Install custom node packages by their repository URLs. Returns
1071
+ * { queued, failed, notFound } package titles/URLs.
1072
+ */
1073
+ async installPackagesByUrl(packageUrls) {
1074
+ const results = { queued: [], failed: [], notFound: [] };
1075
+ const pkgList = await this.getCustomNodeList();
1076
+ const nodePacks = pkgList?.node_packs ?? {};
1077
+ const urlToMetadata = {};
1078
+ for (const metadata of Object.values(nodePacks)) {
1079
+ for (const url of metadata?.files ?? []) {
1080
+ urlToMetadata[url] = metadata;
1081
+ }
1082
+ }
1083
+ for (const url of packageUrls) {
1084
+ const metadata = urlToMetadata[url];
1085
+ if (!metadata) {
1086
+ results.notFound.push(url);
1087
+ continue;
1088
+ }
1089
+ const response = await requestWithRetry({
1090
+ method: 'POST',
1091
+ url: `${this.baseUrl}/manager/queue/install`,
1092
+ headers: {
1093
+ ...(await this.resolvedAuthHeaders()),
1094
+ 'Content-Type': 'application/json',
1095
+ },
1096
+ body: metadata,
1097
+ retries: 1,
1098
+ timeout: 60000,
1099
+ });
1100
+ if (response.status === 200 || response.status === 201) {
1101
+ results.queued.push(metadata.title || url);
1102
+ }
1103
+ else {
1104
+ results.failed.push(metadata.title || url);
1105
+ }
1106
+ }
1107
+ // Start processing if we queued anything
1108
+ if (results.queued.length) {
1109
+ await requestWithRetry({
1110
+ method: 'GET',
1111
+ url: `${this.baseUrl}/manager/queue/start`,
1112
+ headers: await this.resolvedAuthHeaders(),
1113
+ retries: 1,
1114
+ timeout: 60000,
1115
+ });
1116
+ }
1117
+ return results;
1118
+ }
1119
+ /**
1120
+ * Automatically install missing custom nodes for a workflow.
1121
+ */
1122
+ async autoInstallWorkflowNodes(workflow, options = {}) {
1123
+ const { wait = true, reboot = true } = options;
1124
+ const results = {
1125
+ missingNodes: [],
1126
+ packagesToInstall: {},
1127
+ installed: [],
1128
+ failed: [],
1129
+ notFoundNodes: [],
1130
+ };
1131
+ const missing = await this.getMissingNodeTypes(workflow);
1132
+ if (!missing.size) {
1133
+ return results;
1134
+ }
1135
+ results.missingNodes = [...missing];
1136
+ const packages = await this.lookupPackagesForNodes(missing);
1137
+ results.packagesToInstall = packages;
1138
+ const foundNodes = new Set(Object.values(packages).flat());
1139
+ results.notFoundNodes = [...missing].filter((nodeType) => !foundNodes.has(nodeType));
1140
+ if (!Object.keys(packages).length) {
1141
+ return results;
1142
+ }
1143
+ const installResult = await this.installPackagesByUrl(Object.keys(packages));
1144
+ results.installed = installResult.queued;
1145
+ results.failed = [...installResult.failed, ...installResult.notFound];
1146
+ // Wait for installation (max 3 minutes)
1147
+ if (wait && results.installed.length) {
1148
+ const headers = await this.resolvedAuthHeaders();
1149
+ for (let attempt = 0; attempt < 180; attempt += 1) {
1150
+ await sleep(1000);
1151
+ const response = await requestWithRetry({
1152
+ method: 'GET',
1153
+ url: `${this.baseUrl}/manager/queue/status`,
1154
+ headers,
1155
+ retries: 1,
1156
+ timeout: 60000,
1157
+ }).catch(() => null);
1158
+ if (response && response.status === 200) {
1159
+ const status = await response.json();
1160
+ if (!status?.is_processing) {
1161
+ break;
1162
+ }
1163
+ }
1164
+ }
1165
+ }
1166
+ if (reboot && results.installed.length) {
1167
+ await this.reboot();
1168
+ }
1169
+ return results;
1170
+ }
1171
+ /**
1172
+ * Get list of installed custom nodes from ComfyUI Manager.
1173
+ */
1174
+ async getInstalledNodes() {
1175
+ return await this.comfyJson('GET', '/customnode/installed', { timeout: 30000 });
1176
+ }
1177
+ /**
1178
+ * Install a custom node package via ComfyUI Manager.
1179
+ * Returns true if installation was queued successfully.
1180
+ */
1181
+ async installNode(nodeName) {
1182
+ const headers = await this.resolvedAuthHeaders();
1183
+ const response = await requestWithRetry({
1184
+ method: 'POST',
1185
+ url: `${this.baseUrl}/manager/queue/install`,
1186
+ headers: { ...headers, 'Content-Type': 'application/json' },
1187
+ body: { name: nodeName },
1188
+ retries: 1,
1189
+ timeout: 60000,
1190
+ });
1191
+ if (response.status !== 200 && response.status !== 201) {
1192
+ return false;
1193
+ }
1194
+ const start = await requestWithRetry({
1195
+ method: 'GET',
1196
+ url: `${this.baseUrl}/manager/queue/start`,
1197
+ headers,
1198
+ retries: 1,
1199
+ timeout: 60000,
1200
+ });
1201
+ return start.status === 200 || start.status === 201;
1202
+ }
1203
+ /**
1204
+ * Install multiple custom node packages. Returns { queued, failed }.
1205
+ */
1206
+ async installNodes(nodeNames, options = {}) {
1207
+ const { wait = true } = options;
1208
+ const results = { queued: [], failed: [] };
1209
+ const headers = await this.resolvedAuthHeaders();
1210
+ for (const name of nodeNames) {
1211
+ const response = await requestWithRetry({
1212
+ method: 'POST',
1213
+ url: `${this.baseUrl}/manager/queue/install`,
1214
+ headers: { ...headers, 'Content-Type': 'application/json' },
1215
+ body: { name },
1216
+ retries: 1,
1217
+ timeout: 60000,
1218
+ });
1219
+ if (response.status === 200 || response.status === 201) {
1220
+ results.queued.push(name);
1221
+ }
1222
+ else {
1223
+ results.failed.push(name);
1224
+ }
1225
+ }
1226
+ if (!results.queued.length) {
1227
+ return results;
1228
+ }
1229
+ await requestWithRetry({
1230
+ method: 'GET',
1231
+ url: `${this.baseUrl}/manager/queue/start`,
1232
+ headers,
1233
+ retries: 1,
1234
+ timeout: 60000,
1235
+ });
1236
+ // Wait for completion (max 2 minutes)
1237
+ if (wait) {
1238
+ for (let attempt = 0; attempt < 120; attempt += 1) {
1239
+ await sleep(1000);
1240
+ const response = await requestWithRetry({
1241
+ method: 'GET',
1242
+ url: `${this.baseUrl}/manager/queue/status`,
1243
+ headers,
1244
+ retries: 1,
1245
+ timeout: 60000,
1246
+ }).catch(() => null);
1247
+ if (response && response.status === 200) {
1248
+ const status = await response.json();
1249
+ if (!status?.is_processing) {
1250
+ break;
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ return results;
1256
+ }
1257
+ /**
1258
+ * Reboot ComfyUI server (required after installing nodes).
1259
+ */
1260
+ async reboot(options = {}) {
1261
+ const { waitReady = true, timeoutSeconds = 120 } = options;
1262
+ try {
1263
+ await requestWithRetry({
1264
+ method: 'GET',
1265
+ url: `${this.baseUrl}/manager/reboot`,
1266
+ headers: await this.resolvedAuthHeaders(),
1267
+ retries: 1,
1268
+ timeout: 10000,
1269
+ });
1270
+ }
1271
+ catch {
1272
+ // The server may not respond as it is rebooting; that is expected.
1273
+ }
1274
+ if (waitReady) {
1275
+ // Wait a moment for the server to start shutting down
1276
+ await sleep(3000);
1277
+ return await this.waitReady(timeoutSeconds * 1000);
1278
+ }
1279
+ return true;
1280
+ }
1281
+ /**
1282
+ * Ensure custom nodes are installed, installing missing ones. Returns
1283
+ * { alreadyInstalled, installed, failed }.
1284
+ */
1285
+ async ensureNodesInstalled(nodeNames) {
1286
+ const results = {
1287
+ alreadyInstalled: [],
1288
+ installed: [],
1289
+ failed: [],
1290
+ };
1291
+ let installedNames;
1292
+ try {
1293
+ const installed = await this.getInstalledNodes();
1294
+ installedNames = new Set(installed && typeof installed === 'object' ? Object.keys(installed) : []);
1295
+ }
1296
+ catch {
1297
+ installedNames = new Set();
1298
+ }
1299
+ results.alreadyInstalled = nodeNames.filter((name) => installedNames.has(name));
1300
+ const missing = nodeNames.filter((name) => !installedNames.has(name));
1301
+ if (!missing.length) {
1302
+ return results;
1303
+ }
1304
+ const installResult = await this.installNodes(missing, { wait: true });
1305
+ results.installed = installResult.queued;
1306
+ results.failed = installResult.failed;
1307
+ if (results.installed.length) {
1308
+ await this.reboot();
1309
+ }
1310
+ return results;
1311
+ }
1312
+ /**
1313
+ * Submit workflow to ComfyUI, returns prompt_id.
1314
+ * Retries on connection errors including DNS failures.
1315
+ */
1316
+ async queuePrompt(workflow, retries = 5) {
1317
+ let lastError;
1318
+ for (let attempt = 0; attempt < retries; attempt += 1) {
1319
+ try {
1320
+ const response = await fetch(`${this.baseUrl}/prompt`, {
1321
+ method: 'POST',
1322
+ headers: {
1323
+ ...(await this.resolvedAuthHeaders()),
1324
+ 'Content-Type': 'application/json',
1325
+ },
1326
+ body: JSON.stringify({ prompt: workflow }),
1327
+ signal: AbortSignal.timeout(30000),
1328
+ });
1329
+ if (response.status !== 200) {
1330
+ let errorDetail;
1331
+ try {
1332
+ errorDetail = await response.json();
1333
+ }
1334
+ catch {
1335
+ errorDetail = await response.text();
1336
+ }
1337
+ throw new Error(`ComfyUI prompt failed (${response.status}): ${JSON.stringify(errorDetail)}`);
1338
+ }
1339
+ const data = await response.json();
1340
+ return data.prompt_id;
1341
+ }
1342
+ catch (error) {
1343
+ if (error instanceof Error && error.message.startsWith('ComfyUI prompt failed')) {
1344
+ throw error;
1345
+ }
1346
+ if (!ComfyUIJob.isTransientNetworkError(error)) {
1347
+ throw error;
1348
+ }
1349
+ lastError = error;
1350
+ if (attempt < retries - 1) {
1351
+ await sleep(2 ** attempt * 1000); // Exponential backoff
1352
+ continue;
1353
+ }
1354
+ throw error;
1355
+ }
1356
+ }
1357
+ throw lastError;
1358
+ }
1359
+ /**
1360
+ * Get execution history for a prompt. Retries on connection errors including
1361
+ * DNS failures.
1362
+ */
1363
+ async getHistory(promptId, retries = 5) {
1364
+ for (let attempt = 0; attempt < retries; attempt += 1) {
1365
+ try {
1366
+ const response = await fetch(`${this.baseUrl}/history/${encodeURIComponent(promptId)}`, {
1367
+ headers: await this.resolvedAuthHeaders(),
1368
+ signal: AbortSignal.timeout(30000),
1369
+ });
1370
+ if (response.status >= 400) {
1371
+ throw new Error(`ComfyUI history failed (${response.status}): ${await response.text()}`);
1372
+ }
1373
+ const data = await response.json();
1374
+ return data?.[promptId] ?? null;
1375
+ }
1376
+ catch (error) {
1377
+ if (!ComfyUIJob.isTransientNetworkError(error)) {
1378
+ throw error;
1379
+ }
1380
+ if (attempt < retries - 1) {
1381
+ await sleep(2 ** attempt * 1000); // Exponential backoff: 1s, 2s, 4s, 8s
1382
+ continue;
1383
+ }
1384
+ throw error;
1385
+ }
1386
+ }
1387
+ return null;
1388
+ }
1389
+ /**
1390
+ * Wait for prompt execution to complete, returns the history entry.
1391
+ */
1392
+ async waitForCompletion(promptId, options = {}) {
1393
+ const timeoutSeconds = options.timeoutSeconds ?? 300;
1394
+ const pollIntervalSeconds = options.pollIntervalSeconds ?? 2;
1395
+ const start = Date.now();
1396
+ let consecutiveErrors = 0;
1397
+ const maxConsecutiveErrors = 10; // Handle DNS propagation delays
1398
+ while (Date.now() - start < timeoutSeconds * 1000) {
1399
+ try {
1400
+ const history = await this.getHistory(promptId);
1401
+ consecutiveErrors = 0;
1402
+ if (history) {
1403
+ const status = history.status ?? {};
1404
+ if (status.completed) {
1405
+ return history;
1406
+ }
1407
+ if (status.status_str === 'error') {
1408
+ throw new Error(`Workflow execution failed: ${JSON.stringify(status)}`);
1409
+ }
1410
+ }
1411
+ }
1412
+ catch (error) {
1413
+ if (!ComfyUIJob.isTransientNetworkError(error)) {
1414
+ throw error;
1415
+ }
1416
+ consecutiveErrors += 1;
1417
+ if (consecutiveErrors >= maxConsecutiveErrors) {
1418
+ throw new Error(`Lost connection to ComfyUI after ${consecutiveErrors} retries: ${error}`);
1419
+ }
1420
+ await sleep(pollIntervalSeconds * 2 * 1000);
1421
+ continue;
1422
+ }
1423
+ await sleep(pollIntervalSeconds * 1000);
1424
+ }
1425
+ throw new Error(`Workflow did not complete within ${timeoutSeconds}s`);
1426
+ }
1427
+ /**
1428
+ * Download output file from ComfyUI server.
1429
+ * If the file already exists, increments the name (file_1.png, file_2.png, etc.)
1430
+ */
1431
+ async downloadOutput(filename, options = {}) {
1432
+ const outputDir = options.outputDir ?? '.';
1433
+ const subfolder = options.subfolder ?? '';
1434
+ const retries = options.retries ?? 5;
1435
+ if (!existsSync(outputDir)) {
1436
+ mkdirSync(outputDir, { recursive: true });
1437
+ }
1438
+ const params = new URLSearchParams({ filename, type: 'output' });
1439
+ if (subfolder)
1440
+ params.set('subfolder', subfolder);
1441
+ const url = `${this.baseUrl}/view?${params.toString()}`;
1442
+ for (let attempt = 0; attempt < retries; attempt += 1) {
1443
+ try {
1444
+ const response = await fetch(url, {
1445
+ headers: await this.resolvedAuthHeaders(),
1446
+ signal: AbortSignal.timeout(120000),
1447
+ });
1448
+ if (response.status >= 400) {
1449
+ throw new Error(`ComfyUI download failed (${response.status}): ${await response.text()}`);
1450
+ }
1451
+ const content = Buffer.from(await response.arrayBuffer());
1452
+ // Auto-increment filename if exists
1453
+ let outputPath = join(outputDir, filename);
1454
+ if (existsSync(outputPath)) {
1455
+ const dotIndex = filename.lastIndexOf('.');
1456
+ const stem = dotIndex > 0 ? filename.slice(0, dotIndex) : filename;
1457
+ const suffix = dotIndex > 0 ? filename.slice(dotIndex) : '';
1458
+ let index = 1;
1459
+ while (existsSync(outputPath)) {
1460
+ outputPath = join(outputDir, `${stem}_${index}${suffix}`);
1461
+ index += 1;
1462
+ }
1463
+ }
1464
+ writeFileSync(outputPath, content);
1465
+ return outputPath;
1466
+ }
1467
+ catch (error) {
1468
+ if (error instanceof Error && error.message.startsWith('ComfyUI download failed')) {
1469
+ throw error;
1470
+ }
1471
+ if (!ComfyUIJob.isTransientNetworkError(error)) {
1472
+ throw error;
1473
+ }
1474
+ if (attempt < retries - 1) {
1475
+ await sleep(2 ** attempt * 1000);
1476
+ continue;
1477
+ }
1478
+ throw error;
1479
+ }
1480
+ }
1481
+ throw new Error('ComfyUI download failed');
1482
+ }
1483
+ /**
1484
+ * Run a workflow and wait for completion.
1485
+ */
1486
+ async run(workflow, options = {}) {
1487
+ const { timeoutSeconds = 300, convert = true } = options;
1488
+ // Detect format and convert if needed
1489
+ if (convert && 'nodes' in workflow) {
1490
+ workflow = await this.convertWorkflow(workflow);
1491
+ }
1492
+ const promptId = await this.queuePrompt(workflow);
1493
+ return await this.waitForCompletion(promptId, { timeoutSeconds });
1494
+ }
1495
+ /**
1496
+ * Run a template workflow with parameter overrides.
1497
+ */
1498
+ async runTemplate(templateId, options = {}) {
1499
+ const graph = this.loadTemplate(templateId, { templatesDir: options.templatesDir });
1500
+ const workflow = await this.convertWorkflow(graph);
1501
+ // Apply parameter overrides using type-based node lookup
1502
+ if (options.params) {
1503
+ applyParams(workflow, options.params);
1504
+ }
1505
+ return await this.run(workflow, { timeoutSeconds: options.timeoutSeconds, convert: false });
1506
+ }
1507
+ /**
1508
+ * Extract output file info from a history entry (images, videos, gifs).
1509
+ */
1510
+ getOutputImages(history) {
1511
+ const outputs = [];
1512
+ for (const nodeOutput of Object.values(history?.outputs ?? {})) {
1513
+ for (const key of ['images', 'videos', 'gifs']) {
1514
+ const items = nodeOutput?.[key];
1515
+ if (Array.isArray(items)) {
1516
+ outputs.push(...items);
1517
+ }
1518
+ }
1519
+ }
1520
+ return outputs;
1521
+ }
1522
+ /**
1523
+ * Upload image to ComfyUI server
1524
+ */
1525
+ async uploadImage(filePath, filename) {
1526
+ const content = readFileSync(filePath);
1527
+ const fname = filename || basename(filePath);
1528
+ const formData = new FormData();
1529
+ const blob = new Blob([content], { type: 'image/png' });
1530
+ formData.append('image', blob, fname);
1531
+ const response = await fetch(`${this.baseUrl}/upload/image`, {
1532
+ method: 'POST',
1533
+ headers: await this.resolvedAuthHeaders(),
1534
+ body: formData,
1535
+ });
1536
+ if (!response.ok) {
1537
+ throw new Error(`Failed to upload image: ${response.statusText}`);
1538
+ }
1539
+ const data = await response.json();
1540
+ return data.name || fname;
1541
+ }
1542
+ /**
1543
+ * Upload audio to ComfyUI server
1544
+ */
1545
+ async uploadAudio(filePath, filename) {
1546
+ const content = readFileSync(filePath);
1547
+ const fname = filename || basename(filePath);
1548
+ const formData = new FormData();
1549
+ const blob = new Blob([content], { type: 'audio/mpeg' });
1550
+ formData.append('image', blob, fname); // ComfyUI uses /upload/image for all files
1551
+ const response = await fetch(`${this.baseUrl}/upload/image`, {
1552
+ method: 'POST',
1553
+ headers: await this.resolvedAuthHeaders(),
1554
+ body: formData,
1555
+ });
1556
+ if (!response.ok) {
1557
+ throw new Error(`Failed to upload audio: ${response.statusText}`);
1558
+ }
1559
+ const data = await response.json();
1560
+ return data.name || fname;
1561
+ }
1562
+ }
1563
+ //# sourceMappingURL=comfyui.js.map