@soederpop/luca 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,597 @@
1
+ import {
2
+ type ClientsInterface,
3
+ clients,
4
+ RestClient,
5
+ } from "@soederpop/luca/client";
6
+ import type { Container, ContainerContext } from "@soederpop/luca/container";
7
+ import { z } from 'zod'
8
+ import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca/schemas/base.js'
9
+
10
+ declare module "@soederpop/luca/client" {
11
+ interface AvailableClients {
12
+ comfyui: typeof ComfyUIClient;
13
+ }
14
+ }
15
+
16
+ export const ComfyUIClientStateSchema = ClientStateSchema.extend({
17
+ clientId: z.string().default('').describe('Unique client ID used for WebSocket session tracking'),
18
+ queueRemaining: z.number().default(0).describe('Number of prompts remaining in the queue'),
19
+ executing: z.string().nullable().default(null).describe('Prompt ID currently being executed, or null'),
20
+ })
21
+
22
+ export const ComfyUIClientOptionsSchema = ClientOptionsSchema.extend({
23
+ wsURL: z.string().optional().describe('Override the WebSocket URL (defaults to ws version of baseURL)'),
24
+ })
25
+
26
+ export type ComfyUIClientState = z.infer<typeof ComfyUIClientStateSchema>
27
+ export type ComfyUIClientOptions = z.infer<typeof ComfyUIClientOptionsSchema>
28
+
29
+ /** Maps a semantic input name to a specific node ID and field */
30
+ export type InputMapping = Record<string, { nodeId: string; field: string }>;
31
+
32
+ export type WorkflowRunOptions = {
33
+ /** Use polling instead of WebSocket for tracking execution */
34
+ poll?: boolean;
35
+ /** Polling interval in ms (default 1000) */
36
+ pollInterval?: number;
37
+ /** Named input mapping: semantic name -> { nodeId, field } */
38
+ inputMap?: InputMapping;
39
+ /** If provided, output images are downloaded to this directory */
40
+ outputDir?: string;
41
+ };
42
+
43
+ export type WorkflowResult = {
44
+ promptId: string;
45
+ outputs: Record<string, any>;
46
+ images?: Array<{ filename: string; subfolder: string; type: string; localPath?: string }>;
47
+ };
48
+
49
+ /**
50
+ * ComfyUI client — execute Stable Diffusion workflows via the ComfyUI API.
51
+ *
52
+ * Connects to a ComfyUI instance to queue prompts, track execution via WebSocket or polling,
53
+ * and download generated images. Supports both UI-format and API-format workflows with
54
+ * automatic conversion.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const comfy = container.client('comfyui', { baseURL: 'http://localhost:8188' })
59
+ * const result = await comfy.runWorkflow(workflow, {
60
+ * '6': { text: 'a beautiful sunset' }
61
+ * })
62
+ * console.log(result.images)
63
+ * ```
64
+ */
65
+ export class ComfyUIClient extends RestClient<ComfyUIClientState, ComfyUIClientOptions> {
66
+ static override shortcut = "clients.comfyui" as const;
67
+ static override description = "ComfyUI workflow execution client";
68
+ static override stateSchema = ComfyUIClientStateSchema;
69
+ static override optionsSchema = ComfyUIClientOptionsSchema;
70
+
71
+ private ws: WebSocket | null = null;
72
+
73
+ static override attach(container: Container & ClientsInterface, options?: any) {
74
+ container.clients.register("comfyui", ComfyUIClient);
75
+ return container;
76
+ }
77
+
78
+ constructor(options: ComfyUIClientOptions, context: ContainerContext) {
79
+ super(
80
+ {
81
+ ...options,
82
+ baseURL: options.baseURL || "http://127.0.0.1:8000",
83
+ json: options.json ?? true,
84
+ } as ComfyUIClientOptions,
85
+ context
86
+ );
87
+ }
88
+
89
+ /** Initial state with a random client ID. */
90
+ override get initialState(): ComfyUIClientState {
91
+ return {
92
+ connected: false,
93
+ clientId: crypto.randomUUID(),
94
+ queueRemaining: 0,
95
+ executing: null,
96
+ } as ComfyUIClientState;
97
+ }
98
+
99
+ /** The unique client ID used for WebSocket session tracking. */
100
+ get clientId(): string {
101
+ return this.state.get("clientId")!;
102
+ }
103
+
104
+ /** The WebSocket URL derived from baseURL or overridden via options. */
105
+ get wsURL(): string {
106
+ if (this.options.wsURL) return this.options.wsURL;
107
+ return this.baseURL.replace(/^http/, "ws") + "/ws";
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Core API methods
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Queue a prompt (API-format workflow) for execution.
116
+ *
117
+ * @param prompt - The API-format workflow object
118
+ * @param clientId - Override the client ID for this request
119
+ * @returns The prompt ID and queue number
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const { prompt_id } = await comfy.queuePrompt(apiWorkflow)
124
+ * ```
125
+ */
126
+ async queuePrompt(prompt: Record<string, any>, clientId?: string): Promise<{ prompt_id: string; number: number }> {
127
+ return this.post("/prompt", {
128
+ prompt,
129
+ client_id: clientId ?? this.clientId,
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Get the current prompt queue status.
135
+ *
136
+ * @returns Running and pending queue items
137
+ */
138
+ async getQueue(): Promise<{ queue_running: any[]; queue_pending: any[] }> {
139
+ return this.get("/queue");
140
+ }
141
+
142
+ /**
143
+ * Get execution history, optionally for a specific prompt.
144
+ *
145
+ * @param promptId - If provided, returns history for this prompt only
146
+ * @returns History records keyed by prompt ID
147
+ */
148
+ async getHistory(promptId?: string): Promise<Record<string, any>> {
149
+ return this.get(promptId ? `/history/${promptId}` : "/history");
150
+ }
151
+
152
+ /**
153
+ * Get system stats including GPU memory and queue info.
154
+ *
155
+ * @returns System statistics
156
+ */
157
+ async getSystemStats(): Promise<any> {
158
+ return this.get("/system_stats");
159
+ }
160
+
161
+ /**
162
+ * Get node type info with input/output schemas.
163
+ *
164
+ * @param nodeClass - If provided, returns info for this node type only
165
+ * @returns Object info keyed by node class name
166
+ */
167
+ async getObjectInfo(nodeClass?: string): Promise<any> {
168
+ return this.get(nodeClass ? `/object_info/${nodeClass}` : "/object_info");
169
+ }
170
+
171
+ /** Interrupt the currently executing prompt. */
172
+ async interrupt(): Promise<void> {
173
+ await this.post("/interrupt", {});
174
+ }
175
+
176
+ /**
177
+ * List available models, optionally filtered by type.
178
+ *
179
+ * @param type - Model type filter (e.g., 'checkpoints', 'loras')
180
+ * @returns Array of model file names
181
+ */
182
+ async getModels(type?: string): Promise<string[]> {
183
+ return this.get(type ? `/models/${type}` : "/models");
184
+ }
185
+
186
+ /** List available embedding models. */
187
+ async getEmbeddings(): Promise<string[]> {
188
+ return this.get("/embeddings");
189
+ }
190
+
191
+ /**
192
+ * Upload an image to ComfyUI's input directory.
193
+ *
194
+ * @param file - The image data as Buffer or Blob
195
+ * @param filename - File name for the upload
196
+ * @param opts - Upload options (subfolder, type, overwrite)
197
+ * @returns Upload result from ComfyUI
198
+ */
199
+ async uploadImage(
200
+ file: Buffer | Blob,
201
+ filename: string,
202
+ opts: { subfolder?: string; type?: string; overwrite?: boolean } = {}
203
+ ): Promise<any> {
204
+ const formData = new FormData();
205
+ const blob = file instanceof Blob ? file : new Blob([file as BlobPart]);
206
+ formData.append("image", blob, filename);
207
+ if (opts.subfolder) formData.append("subfolder", opts.subfolder);
208
+ if (opts.type) formData.append("type", opts.type);
209
+ if (opts.overwrite) formData.append("overwrite", "true");
210
+
211
+ return this.axios
212
+ .post("/upload/image", formData, {
213
+ headers: { "Content-Type": "multipart/form-data" },
214
+ })
215
+ .then((r) => r.data);
216
+ }
217
+
218
+ /**
219
+ * Download a generated image from ComfyUI as a Buffer.
220
+ *
221
+ * @param filename - The image filename
222
+ * @param subfolder - Subfolder within the output directory
223
+ * @param type - Image type ('output', 'input', 'temp')
224
+ * @returns The image data as a Buffer
225
+ */
226
+ async viewImage(
227
+ filename: string,
228
+ subfolder = "",
229
+ type = "output"
230
+ ): Promise<Buffer> {
231
+ const resp = await this.axios.get("/view", {
232
+ params: { filename, subfolder, type },
233
+ responseType: "arraybuffer",
234
+ });
235
+ return Buffer.from(resp.data);
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // WebSocket connection
240
+ // ---------------------------------------------------------------------------
241
+
242
+ /**
243
+ * Open a WebSocket connection to ComfyUI for real-time execution tracking.
244
+ *
245
+ * Events emitted: `execution_start`, `executing`, `progress`, `executed`,
246
+ * `execution_cached`, `execution_error`, `execution_complete`.
247
+ */
248
+ async connectWs(): Promise<void> {
249
+ if (this.ws) return;
250
+
251
+ const url = `${this.wsURL}?clientId=${this.clientId}`;
252
+
253
+ return new Promise<void>((resolve, reject) => {
254
+ const ws = new WebSocket(url);
255
+
256
+ ws.addEventListener("open", () => {
257
+ this.ws = ws;
258
+ this.state.set("connected", true);
259
+ resolve();
260
+ });
261
+
262
+ ws.addEventListener("error", (e) => {
263
+ reject(e);
264
+ });
265
+
266
+ ws.addEventListener("close", () => {
267
+ this.ws = null;
268
+ this.state.set("connected", false);
269
+ });
270
+
271
+ ws.addEventListener("message", (event) => {
272
+ try {
273
+ const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
274
+ this.handleWsMessage(msg);
275
+ } catch {
276
+ // binary data (e.g. preview images), ignore
277
+ }
278
+ });
279
+ });
280
+ }
281
+
282
+ /** Close the WebSocket connection. */
283
+ disconnectWs(): void {
284
+ if (this.ws) {
285
+ this.ws.close();
286
+ this.ws = null;
287
+ this.state.set("connected", false);
288
+ }
289
+ }
290
+
291
+ private handleWsMessage(msg: { type: string; data: any }) {
292
+ switch (msg.type) {
293
+ case "status":
294
+ if (msg.data?.status?.exec_info) {
295
+ this.state.set("queueRemaining", msg.data.status.exec_info.queue_remaining);
296
+ }
297
+ break;
298
+ case "execution_start":
299
+ this.state.set("executing", msg.data.prompt_id);
300
+ this.emit("execution_start", { promptId: msg.data.prompt_id });
301
+ break;
302
+ case "executing":
303
+ if (msg.data.node === null) {
304
+ this.state.set("executing", null);
305
+ this.emit("execution_complete", { promptId: msg.data.prompt_id });
306
+ } else {
307
+ this.emit("executing", { node: msg.data.node, promptId: msg.data.prompt_id });
308
+ }
309
+ break;
310
+ case "progress":
311
+ this.emit("progress", {
312
+ node: msg.data.node,
313
+ value: msg.data.value,
314
+ max: msg.data.max,
315
+ promptId: msg.data.prompt_id,
316
+ });
317
+ break;
318
+ case "executed":
319
+ this.emit("executed", {
320
+ node: msg.data.node,
321
+ output: msg.data.output,
322
+ promptId: msg.data.prompt_id,
323
+ });
324
+ break;
325
+ case "execution_cached":
326
+ this.emit("execution_cached", {
327
+ nodes: msg.data.nodes,
328
+ promptId: msg.data.prompt_id,
329
+ });
330
+ break;
331
+ case "execution_error":
332
+ this.emit("execution_error", {
333
+ promptId: msg.data.prompt_id,
334
+ ...msg.data,
335
+ });
336
+ break;
337
+ }
338
+ }
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Workflow format detection & conversion
342
+ // ---------------------------------------------------------------------------
343
+
344
+ /**
345
+ * Detect whether a workflow object is in UI format (exported from the graph
346
+ * editor) or API format (flat node-id-keyed object with class_type).
347
+ */
348
+ static isUIFormat(workflow: Record<string, any>): boolean {
349
+ return Array.isArray(workflow.nodes) && Array.isArray(workflow.links);
350
+ }
351
+
352
+ /**
353
+ * Convert a UI-format workflow to the API format that /prompt expects.
354
+ *
355
+ * Requires a running ComfyUI instance to fetch `object_info` so we can
356
+ * map positional `widgets_values` to their named input fields.
357
+ *
358
+ * If the workflow is already in API format, it's returned as-is.
359
+ */
360
+ async toApiFormat(workflow: Record<string, any>): Promise<Record<string, any>> {
361
+ if (!ComfyUIClient.isUIFormat(workflow)) return workflow;
362
+
363
+ const nodes: any[] = workflow.nodes;
364
+ const links: any[] = workflow.links;
365
+
366
+ // Build a lookup: linkId -> { sourceNodeId, sourceSlot }
367
+ const linkMap = new Map<number, { sourceNodeId: string; sourceSlot: number }>();
368
+ for (const link of links) {
369
+ // link format: [linkId, sourceNodeId, sourceSlot, targetNodeId, targetSlot, type]
370
+ linkMap.set(link[0], { sourceNodeId: String(link[1]), sourceSlot: link[2] });
371
+ }
372
+
373
+ // Fetch object_info for all node types present in the workflow
374
+ const nodeTypes = [...new Set(nodes.map((n) => n.type))];
375
+ const objectInfo: Record<string, any> = {};
376
+ await Promise.all(
377
+ nodeTypes.map(async (type) => {
378
+ try {
379
+ const info = await this.getObjectInfo(type);
380
+ objectInfo[type] = info[type];
381
+ } catch {
382
+ // Node type not found on server — we'll do our best without it
383
+ }
384
+ })
385
+ );
386
+
387
+ const apiWorkflow: Record<string, any> = {};
388
+
389
+ for (const node of nodes) {
390
+ const nodeId = String(node.id);
391
+ const classType = node.type;
392
+ const inputs: Record<string, any> = {};
393
+
394
+ // Resolve connected inputs (from the node's input slots)
395
+ if (node.inputs) {
396
+ for (const input of node.inputs) {
397
+ if (input.link != null) {
398
+ const source = linkMap.get(input.link);
399
+ if (source) {
400
+ inputs[input.name] = [source.sourceNodeId, source.sourceSlot];
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ // Map widgets_values to named inputs using object_info
407
+ if (node.widgets_values && objectInfo[classType]) {
408
+ const info = objectInfo[classType];
409
+ const requiredInputs = info.input?.required ?? {};
410
+ const optionalInputs = info.input?.optional ?? {};
411
+
412
+ // Collect the widget input names in order (skip ones that are link-connected)
413
+ const connectedNames = new Set(
414
+ (node.inputs || []).filter((i: any) => i.link != null).map((i: any) => i.name)
415
+ );
416
+
417
+ const widgetNames: string[] = [];
418
+ for (const [name, config] of Object.entries(requiredInputs) as [string, any][]) {
419
+ if (connectedNames.has(name)) continue;
420
+ // Skip non-widget types (these are slot connections, not widgets)
421
+ const inputType = Array.isArray(config) ? config[0] : config;
422
+ if (typeof inputType === "string" && inputType === inputType.toUpperCase() && inputType.length > 1 && !Array.isArray(config[0])) continue;
423
+ widgetNames.push(name);
424
+ }
425
+ for (const [name, config] of Object.entries(optionalInputs) as [string, any][]) {
426
+ if (connectedNames.has(name)) continue;
427
+ const inputType = Array.isArray(config) ? config[0] : config;
428
+ if (typeof inputType === "string" && inputType === inputType.toUpperCase() && inputType.length > 1 && !Array.isArray(config[0])) continue;
429
+ widgetNames.push(name);
430
+ }
431
+
432
+ // Assign values positionally
433
+ for (let i = 0; i < node.widgets_values.length && i < widgetNames.length; i++) {
434
+ inputs[widgetNames[i]!] = node.widgets_values[i];
435
+ }
436
+ }
437
+
438
+ apiWorkflow[nodeId] = { class_type: classType, inputs };
439
+ }
440
+
441
+ return apiWorkflow;
442
+ }
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // High-level workflow execution
446
+ // ---------------------------------------------------------------------------
447
+
448
+ /**
449
+ * Run a ComfyUI workflow with optional runtime input overrides.
450
+ *
451
+ * Inputs can be provided in two forms:
452
+ *
453
+ * **Direct node mapping** (when no `inputMap` in options):
454
+ * ```
455
+ * { '3': { seed: 42 }, '6': { text: 'a cat' } }
456
+ * ```
457
+ *
458
+ * **Named inputs** (when `inputMap` is provided in options):
459
+ * ```
460
+ * inputs: { positive_prompt: 'a cat', seed: 42 }
461
+ * options.inputMap: {
462
+ * positive_prompt: { nodeId: '6', field: 'text' },
463
+ * seed: { nodeId: '3', field: 'seed' }
464
+ * }
465
+ * ```
466
+ */
467
+ async runWorkflow(
468
+ workflow: Record<string, any>,
469
+ inputs?: Record<string, any>,
470
+ options: WorkflowRunOptions = {}
471
+ ): Promise<WorkflowResult> {
472
+ // Auto-detect and convert UI format -> API format
473
+ const apiFormat = await this.toApiFormat(workflow);
474
+ const prompt = structuredClone(apiFormat);
475
+
476
+ // Apply inputs
477
+ if (inputs) {
478
+ if (options.inputMap) {
479
+ // Named input mode: resolve through the mapping
480
+ for (const [name, value] of Object.entries(inputs)) {
481
+ const mapping = options.inputMap[name];
482
+ if (!mapping) {
483
+ throw new Error(`No inputMap entry for "${name}". Available: ${Object.keys(options.inputMap).join(", ")}`);
484
+ }
485
+ if (!prompt[mapping.nodeId]) {
486
+ throw new Error(`Node "${mapping.nodeId}" not found in workflow (mapped from "${name}")`);
487
+ }
488
+ prompt[mapping.nodeId].inputs[mapping.field] = value;
489
+ }
490
+ } else {
491
+ // Direct node ID mapping
492
+ for (const [nodeId, fields] of Object.entries(inputs)) {
493
+ if (!prompt[nodeId]) {
494
+ throw new Error(`Node "${nodeId}" not found in workflow`);
495
+ }
496
+ if (typeof fields === "object" && fields !== null) {
497
+ Object.assign(prompt[nodeId].inputs, fields);
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ // Queue the prompt
504
+ const { prompt_id: promptId } = await this.queuePrompt(prompt);
505
+
506
+ // Track execution
507
+ let outputs: Record<string, any>;
508
+
509
+ if (options.poll) {
510
+ outputs = await this.pollForCompletion(promptId, options.pollInterval ?? 1000);
511
+ } else {
512
+ outputs = await this.waitForCompletionWs(promptId);
513
+ }
514
+
515
+ // Collect image outputs
516
+ const images: WorkflowResult["images"] = [];
517
+ for (const nodeOutputs of Object.values(outputs)) {
518
+ if (nodeOutputs.images) {
519
+ for (const img of nodeOutputs.images) {
520
+ images.push({
521
+ filename: img.filename,
522
+ subfolder: img.subfolder || "",
523
+ type: img.type || "output",
524
+ });
525
+ }
526
+ }
527
+ }
528
+
529
+ // Optionally download images to disk
530
+ if (options.outputDir && images.length) {
531
+ const { mkdir } = await import("fs/promises");
532
+ const { join } = await import("path");
533
+ await mkdir(options.outputDir, { recursive: true });
534
+
535
+ for (const img of images) {
536
+ const buf = await this.viewImage(img.filename, img.subfolder, img.type);
537
+ const localPath = join(options.outputDir, img.filename);
538
+ await Bun.write(localPath, buf);
539
+ img.localPath = localPath;
540
+ }
541
+ }
542
+
543
+ return { promptId, outputs, images };
544
+ }
545
+
546
+ private async waitForCompletionWs(promptId: string): Promise<Record<string, any>> {
547
+ const needsConnect = !this.ws;
548
+ if (needsConnect) await this.connectWs();
549
+
550
+ return new Promise<Record<string, any>>((resolve, reject) => {
551
+ const onComplete = (data: any) => {
552
+ if (data.promptId === promptId) {
553
+ cleanup();
554
+ this.getHistory(promptId).then((history) => {
555
+ const entry = history[promptId];
556
+ resolve(entry?.outputs ?? {});
557
+ });
558
+ }
559
+ };
560
+
561
+ const onError = (data: any) => {
562
+ if (data.promptId === promptId) {
563
+ cleanup();
564
+ reject(new Error(data.exception_message || "Execution error"));
565
+ }
566
+ };
567
+
568
+ const cleanup = () => {
569
+ this.off("execution_complete", onComplete);
570
+ this.off("execution_error", onError);
571
+ if (needsConnect) this.disconnectWs();
572
+ };
573
+
574
+ this.on("execution_complete", onComplete);
575
+ this.on("execution_error", onError);
576
+ });
577
+ }
578
+
579
+ private async pollForCompletion(promptId: string, interval: number): Promise<Record<string, any>> {
580
+ while (true) {
581
+ const history = await this.getHistory(promptId);
582
+ const entry = history[promptId];
583
+
584
+ if (entry?.status?.completed || entry?.outputs) {
585
+ return entry.outputs ?? {};
586
+ }
587
+
588
+ if (entry?.status?.status_str === "error") {
589
+ throw new Error("Workflow execution failed");
590
+ }
591
+
592
+ await new Promise((r) => setTimeout(r, interval));
593
+ }
594
+ }
595
+ }
596
+
597
+ export default clients.register("comfyui", ComfyUIClient);