@soederpop/luca 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CLAUDE.md +10 -1
  2. package/RUNME.md +56 -0
  3. package/bun.lock +1 -1
  4. package/commands/build-bootstrap.ts +78 -0
  5. package/commands/build-scaffolds.ts +24 -2
  6. package/commands/try-all-challenges.ts +543 -0
  7. package/commands/try-challenge.ts +100 -0
  8. package/docs/README.md +52 -80
  9. package/docs/TABLE-OF-CONTENTS.md +82 -51
  10. package/docs/apis/clients/elevenlabs.md +232 -8
  11. package/docs/apis/clients/graph.md +59 -8
  12. package/docs/apis/clients/openai.md +362 -2
  13. package/docs/apis/clients/rest.md +122 -2
  14. package/docs/apis/clients/websocket.md +71 -17
  15. package/docs/apis/features/agi/assistant.md +9 -3
  16. package/docs/apis/features/agi/assistants-manager.md +2 -2
  17. package/docs/apis/features/agi/claude-code.md +153 -14
  18. package/docs/apis/features/agi/conversation-history.md +15 -3
  19. package/docs/apis/features/agi/conversation.md +133 -20
  20. package/docs/apis/features/agi/openai-codex.md +90 -12
  21. package/docs/apis/features/agi/skills-library.md +23 -5
  22. package/docs/apis/features/node/container-link.md +59 -0
  23. package/docs/apis/features/node/content-db.md +1 -1
  24. package/docs/apis/features/node/disk-cache.md +1 -1
  25. package/docs/apis/features/node/dns.md +1 -0
  26. package/docs/apis/features/node/docker.md +2 -1
  27. package/docs/apis/features/node/esbuild.md +4 -3
  28. package/docs/apis/features/node/file-manager.md +13 -4
  29. package/docs/apis/features/node/fs.md +726 -171
  30. package/docs/apis/features/node/git.md +1 -0
  31. package/docs/apis/features/node/google-auth.md +23 -4
  32. package/docs/apis/features/node/google-calendar.md +14 -2
  33. package/docs/apis/features/node/google-docs.md +15 -2
  34. package/docs/apis/features/node/google-drive.md +21 -3
  35. package/docs/apis/features/node/google-sheets.md +14 -2
  36. package/docs/apis/features/node/grep.md +2 -0
  37. package/docs/apis/features/node/helpers.md +29 -0
  38. package/docs/apis/features/node/ink.md +2 -2
  39. package/docs/apis/features/node/networking.md +39 -4
  40. package/docs/apis/features/node/os.md +28 -0
  41. package/docs/apis/features/node/postgres.md +26 -4
  42. package/docs/apis/features/node/proc.md +37 -28
  43. package/docs/apis/features/node/process-manager.md +33 -5
  44. package/docs/apis/features/node/repl.md +1 -1
  45. package/docs/apis/features/node/runpod.md +1 -0
  46. package/docs/apis/features/node/secure-shell.md +7 -0
  47. package/docs/apis/features/node/semantic-search.md +12 -5
  48. package/docs/apis/features/node/sqlite.md +26 -4
  49. package/docs/apis/features/node/telegram.md +30 -5
  50. package/docs/apis/features/node/tts.md +17 -2
  51. package/docs/apis/features/node/ui.md +1 -1
  52. package/docs/apis/features/node/vault.md +4 -9
  53. package/docs/apis/features/node/vm.md +3 -12
  54. package/docs/apis/features/node/window-manager.md +128 -20
  55. package/docs/apis/features/web/asset-loader.md +13 -1
  56. package/docs/apis/features/web/container-link.md +59 -0
  57. package/docs/apis/features/web/esbuild.md +4 -3
  58. package/docs/apis/features/web/helpers.md +29 -0
  59. package/docs/apis/features/web/network.md +16 -2
  60. package/docs/apis/features/web/speech.md +16 -2
  61. package/docs/apis/features/web/vault.md +4 -9
  62. package/docs/apis/features/web/vm.md +3 -12
  63. package/docs/apis/features/web/voice.md +18 -1
  64. package/docs/apis/servers/express.md +18 -2
  65. package/docs/apis/servers/mcp.md +29 -4
  66. package/docs/apis/servers/websocket.md +34 -6
  67. package/docs/bootstrap/CLAUDE.md +100 -0
  68. package/docs/bootstrap/SKILL.md +222 -0
  69. package/docs/bootstrap/templates/about-command.ts +41 -0
  70. package/docs/bootstrap/templates/docs-models.ts +22 -0
  71. package/docs/bootstrap/templates/docs-readme.md +43 -0
  72. package/docs/bootstrap/templates/example-feature.ts +53 -0
  73. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  74. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  75. package/docs/bootstrap/templates/runme.md +54 -0
  76. package/docs/challenges/caching-proxy.md +16 -0
  77. package/docs/challenges/content-db-round-trip.md +14 -0
  78. package/docs/challenges/custom-command.md +9 -0
  79. package/docs/challenges/file-watcher-pipeline.md +11 -0
  80. package/docs/challenges/grep-audit-report.md +15 -0
  81. package/docs/challenges/multi-feature-dashboard.md +14 -0
  82. package/docs/challenges/process-orchestrator.md +17 -0
  83. package/docs/challenges/rest-api-server-with-client.md +12 -0
  84. package/docs/challenges/script-runner-with-vm.md +11 -0
  85. package/docs/challenges/simple-rest-api.md +15 -0
  86. package/docs/challenges/websocket-serve-and-client.md +11 -0
  87. package/docs/challenges/yaml-config-system.md +14 -0
  88. package/docs/command-system-overhaul.md +94 -0
  89. package/docs/examples/assistant/CORE.md +18 -0
  90. package/docs/examples/assistant/hooks.ts +3 -0
  91. package/docs/examples/assistant/tools.ts +10 -0
  92. package/docs/examples/window-manager-layouts.md +180 -0
  93. package/docs/in-memory-fs.md +4 -0
  94. package/docs/models.ts +13 -10
  95. package/docs/philosophy.md +4 -3
  96. package/docs/reports/console-hmr-design.md +170 -0
  97. package/docs/reports/helper-semantic-search.md +72 -0
  98. package/docs/scaffolds/client.md +29 -20
  99. package/docs/scaffolds/command.md +64 -50
  100. package/docs/scaffolds/endpoint.md +31 -36
  101. package/docs/scaffolds/feature.md +28 -18
  102. package/docs/scaffolds/selector.md +91 -0
  103. package/docs/scaffolds/server.md +18 -9
  104. package/docs/selectors.md +115 -0
  105. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  106. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  107. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  108. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  109. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  110. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  111. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  112. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  113. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  114. package/docs/tutorials/00-bootstrap.md +148 -0
  115. package/docs/tutorials/07-endpoints.md +7 -7
  116. package/docs/tutorials/08-commands.md +153 -72
  117. package/luca.cli.ts +3 -0
  118. package/package.json +6 -5
  119. package/public/index.html +1430 -0
  120. package/scripts/examples/using-ollama.ts +2 -1
  121. package/scripts/update-introspection-data.ts +2 -2
  122. package/src/agi/endpoints/experts.ts +1 -1
  123. package/src/agi/features/assistant.ts +7 -0
  124. package/src/agi/features/assistants-manager.ts +5 -5
  125. package/src/agi/features/claude-code.ts +263 -3
  126. package/src/agi/features/conversation-history.ts +7 -1
  127. package/src/agi/features/conversation.ts +26 -3
  128. package/src/agi/features/openai-codex.ts +26 -2
  129. package/src/agi/features/openapi.ts +6 -1
  130. package/src/agi/features/skills-library.ts +9 -1
  131. package/src/bootstrap/generated.ts +595 -0
  132. package/src/cli/cli.ts +64 -21
  133. package/src/client.ts +23 -357
  134. package/src/clients/civitai/index.ts +1 -1
  135. package/src/clients/client-template.ts +1 -1
  136. package/src/clients/comfyui/index.ts +13 -2
  137. package/src/clients/elevenlabs/index.ts +2 -1
  138. package/src/clients/graph.ts +87 -0
  139. package/src/clients/openai/index.ts +10 -1
  140. package/src/clients/rest.ts +207 -0
  141. package/src/clients/websocket.ts +176 -0
  142. package/src/command.ts +281 -34
  143. package/src/commands/bootstrap.ts +185 -0
  144. package/src/commands/chat.ts +5 -4
  145. package/src/commands/describe.ts +341 -4
  146. package/src/commands/help.ts +35 -9
  147. package/src/commands/index.ts +3 -0
  148. package/src/commands/introspect.ts +92 -2
  149. package/src/commands/prompt.ts +5 -6
  150. package/src/commands/run.ts +75 -10
  151. package/src/commands/save-api-docs.ts +49 -0
  152. package/src/commands/scaffold.ts +169 -23
  153. package/src/commands/select.ts +94 -0
  154. package/src/commands/serve.ts +10 -1
  155. package/src/container.ts +15 -0
  156. package/src/endpoint.ts +19 -0
  157. package/src/graft.ts +181 -0
  158. package/src/introspection/generated.agi.ts +12458 -8968
  159. package/src/introspection/generated.node.ts +10573 -7145
  160. package/src/introspection/generated.web.ts +1 -1
  161. package/src/introspection/index.ts +26 -0
  162. package/src/node/container.ts +6 -7
  163. package/src/node/features/content-db.ts +49 -2
  164. package/src/node/features/disk-cache.ts +16 -9
  165. package/src/node/features/dns.ts +16 -3
  166. package/src/node/features/docker.ts +16 -4
  167. package/src/node/features/esbuild.ts +22 -2
  168. package/src/node/features/file-manager.ts +184 -29
  169. package/src/node/features/fs.ts +704 -248
  170. package/src/node/features/git.ts +21 -8
  171. package/src/node/features/grep.ts +23 -3
  172. package/src/node/features/helpers.ts +372 -43
  173. package/src/node/features/networking.ts +39 -4
  174. package/src/node/features/opener.ts +28 -15
  175. package/src/node/features/os.ts +76 -0
  176. package/src/node/features/port-exposer.ts +11 -1
  177. package/src/node/features/postgres.ts +17 -1
  178. package/src/node/features/proc.ts +4 -1
  179. package/src/node/features/python.ts +63 -14
  180. package/src/node/features/repl.ts +11 -7
  181. package/src/node/features/runpod.ts +16 -3
  182. package/src/node/features/secure-shell.ts +27 -2
  183. package/src/node/features/semantic-search.ts +12 -1
  184. package/src/node/features/ui.ts +5 -69
  185. package/src/node/features/vm.ts +17 -0
  186. package/src/node/features/window-manager.ts +68 -20
  187. package/src/node.ts +5 -0
  188. package/src/scaffolds/generated.ts +492 -290
  189. package/src/scaffolds/template.ts +9 -0
  190. package/src/schemas/base.ts +46 -5
  191. package/src/selector.ts +282 -0
  192. package/src/server.ts +11 -0
  193. package/src/servers/express.ts +27 -12
  194. package/src/servers/socket.ts +45 -11
  195. package/src/web/clients/socket.ts +4 -1
  196. package/src/web/container.ts +2 -1
  197. package/src/web/features/network.ts +7 -1
  198. package/src/web/features/voice-recognition.ts +16 -1
  199. package/test/clients-servers.test.ts +2 -1
  200. package/test/command.test.ts +267 -0
  201. package/test/vm-context.test.ts +146 -0
  202. package/test-integration/assistants-manager.test.ts +10 -20
  203. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  204. package/docs/examples/launcher-app-command-listener.md +0 -120
  205. package/docs/tasks/web-container-helper-discovery.md +0 -71
  206. package/docs/todos.md +0 -1
  207. package/scripts/test-command-listener.ts +0 -123
  208. package/src/node/features/launcher-app-command-listener.ts +0 -389
package/src/cli/cli.ts CHANGED
@@ -5,30 +5,74 @@ import { homedir } from 'os'
5
5
  import { join } from 'path'
6
6
 
7
7
  async function main() {
8
+ const profile = process.env.LUCA_PROFILE === '1'
9
+ const t = (label?: string) => {
10
+ if (!profile) return () => {}
11
+ const start = performance.now()
12
+ return (suffix?: string) => {
13
+ const ms = (performance.now() - start).toFixed(1)
14
+ console.error(`[profile] ${label}${suffix ? ` ${suffix}` : ''}: ${ms}ms`)
15
+ }
16
+ }
17
+
18
+ const tTotal = t('total boot')
19
+
20
+ // LUCA_COMMAND_DISCOVERY: "disable" skips all, "no-local" skips project, "no-home" skips user
21
+ const discovery = process.env.LUCA_COMMAND_DISCOVERY || ''
22
+
23
+ // Snapshot built-in commands BEFORE loadCliModule — luca.cli.ts may call
24
+ // helpers.discoverAll() which registers project commands early
25
+ const builtinCommands = new Set(container.commands.available as string[])
26
+
8
27
  // Load project-level CLI module (luca.cli.ts) for container customization
28
+ let done = t('loadCliModule')
9
29
  await loadCliModule()
30
+ done()
31
+
10
32
  // Discover project-local commands (commands/ or src/commands/)
11
- await discoverProjectCommands()
33
+ done = t('discoverProjectCommands')
34
+ if (discovery !== 'disable' && discovery !== 'no-local') {
35
+ await discoverProjectCommands()
36
+ }
37
+ done()
38
+ const afterProject = new Set(container.commands.available as string[])
39
+ const projectCommands = new Set([...afterProject].filter((n) => !builtinCommands.has(n)))
40
+
12
41
  // Discover user-level commands (~/.luca/commands/)
13
- await discoverUserCommands()
42
+ done = t('discoverUserCommands')
43
+ if (discovery !== 'disable' && discovery !== 'no-home') {
44
+ await discoverUserCommands()
45
+ }
46
+ done()
47
+ const afterUser = new Set(container.commands.available as string[])
48
+ const userCommands = new Set([...afterUser].filter((n) => !builtinCommands.has(n) && !projectCommands.has(n)))
49
+
50
+ // Store command sources for help display
51
+ ;(container as any)._commandSources = { builtinCommands, projectCommands, userCommands }
52
+
14
53
  // Load generated introspection data if present
54
+ done = t('loadProjectIntrospection')
15
55
  await loadProjectIntrospection()
56
+ done()
16
57
 
17
58
  const commandName = container.argv._[0] as string
18
59
 
60
+ done = t('dispatch')
19
61
  if (commandName && container.commands.has(commandName)) {
20
62
  const cmd = container.command(commandName as any)
21
- await cmd.run()
63
+ await cmd.dispatch()
22
64
  } else if (commandName) {
23
65
  // not a known command — treat as implicit `run`
24
66
  container.argv._.splice(0, 0, 'run')
25
67
  const cmd = container.command('run' as any)
26
- await cmd.run()
68
+ await cmd.dispatch()
27
69
  } else {
28
70
  container.argv._.splice(0, 0, 'help')
29
71
  const cmd = container.command('help' as any)
30
- await cmd.run()
72
+ await cmd.dispatch()
31
73
  }
74
+ done()
75
+ tTotal()
32
76
  }
33
77
 
34
78
 
@@ -36,28 +80,26 @@ async function loadCliModule() {
36
80
  const modulePath = container.paths.resolve('luca.cli.ts')
37
81
  if (!container.fs.exists(modulePath)) return
38
82
 
39
- const mod = await import(modulePath)
40
- const exports = mod.default || mod
83
+ // Use the helpers feature to load the module — it handles the native import
84
+ // vs VM decision using the same useNativeImport check as discovery
85
+ const helpers = container.feature('helpers') as any
86
+ const exports = await helpers.loadModuleExports(modulePath)
41
87
 
42
- if (typeof exports.main === 'function') {
88
+ if (typeof exports?.main === 'function') {
43
89
  await exports.main(container)
44
90
  }
45
91
 
46
- if (typeof exports.onStart === 'function') {
92
+ if (typeof exports?.onStart === 'function') {
47
93
  container.once('started', () => exports.onStart(container))
48
94
  }
49
95
  }
50
96
 
51
97
  async function discoverProjectCommands() {
52
- const { fs, paths } = container
53
-
54
- for (const candidate of ['commands', 'src/commands']) {
55
- const dir = paths.resolve(candidate)
56
- if (fs.exists(dir)) {
57
- await container.commands.discover({ directory: dir })
58
- return
59
- }
60
- }
98
+ // Always route through the helpers feature — it handles native import vs VM
99
+ // internally, and deduplicates concurrent/repeated discovery via promise caching.
100
+ // If luca.cli.ts already called helpers.discoverAll(), this resolves instantly.
101
+ const helpers = container.feature('helpers') as any
102
+ await helpers.discover('commands')
61
103
  }
62
104
 
63
105
  async function loadProjectIntrospection() {
@@ -81,11 +123,12 @@ async function loadProjectIntrospection() {
81
123
  }
82
124
 
83
125
  async function discoverUserCommands() {
84
- const { fs } = container
85
126
  const dir = join(homedir(), '.luca', 'commands')
86
127
 
87
- if (fs.exists(dir)) {
88
- await container.commands.discover({ directory: dir })
128
+ if (container.fs.exists(dir)) {
129
+ // Route through helpers for consistent dedup and VM/native handling
130
+ const helpers = container.feature('helpers') as any
131
+ await helpers.discover('commands', { directory: dir })
89
132
  }
90
133
  }
91
134
 
package/src/client.ts CHANGED
@@ -1,25 +1,24 @@
1
1
  import { Helper } from "./helper.js";
2
2
  import type { Container, ContainerContext } from "./container.js";
3
- import axios, { type AxiosError, type AxiosInstance, type AxiosRequestConfig } from "axios";
4
3
  import { Registry } from "./registry.js";
5
4
  import { z } from 'zod'
6
5
  import {
7
6
  ClientStateSchema, ClientOptionsSchema, ClientEventsSchema,
8
- WebSocketClientStateSchema, WebSocketClientOptionsSchema, WebSocketClientEventsSchema,
9
- GraphClientOptionsSchema, GraphClientEventsSchema,
10
7
  } from './schemas/base.js'
11
8
 
12
9
  export type ClientOptions = z.infer<typeof ClientOptionsSchema>
13
10
  export type ClientState = z.infer<typeof ClientStateSchema>
14
- export type WebSocketClientState = z.infer<typeof WebSocketClientStateSchema>
15
- export type WebSocketClientOptions = z.infer<typeof WebSocketClientOptionsSchema>
16
- export type GraphClientOptions = z.infer<typeof GraphClientOptionsSchema>
17
11
 
18
- export interface AvailableClients {
19
- rest: typeof RestClient;
20
- graph: typeof GraphClient;
21
- websocket: typeof WebSocketClient;
22
- }
12
+ // Subclass types re-exported for backward compatibility.
13
+ // Import the concrete classes from their individual files:
14
+ // import { RestClient } from './clients/rest'
15
+ // import { GraphClient } from './clients/graph'
16
+ // import { WebSocketClient } from './clients/websocket'
17
+ export type { WebSocketClientState, WebSocketClientOptions } from './clients/websocket.js'
18
+ export type { GraphClientOptions } from './clients/graph.js'
19
+
20
+ // AvailableClients is an open interface — subclasses augment it via `declare module`
21
+ export interface AvailableClients {}
23
22
 
24
23
  export interface ClientsInterface {
25
24
  clients: ClientsRegistry;
@@ -29,6 +28,14 @@ export interface ClientsInterface {
29
28
  ): InstanceType<AvailableClients[T]>;
30
29
  }
31
30
 
31
+ /**
32
+ * Base client class for all Luca network clients. Provides connection state
33
+ * tracking, configuration, and the registry/factory infrastructure for
34
+ * creating typed client instances via `container.client('rest')`.
35
+ *
36
+ * Subclasses should override `connect()` and add protocol-specific methods.
37
+ * Register subclasses using `Client.register(this, 'myClient')` in a static block.
38
+ */
32
39
  export class Client<
33
40
  T extends ClientState = ClientState,
34
41
  K extends ClientOptions = ClientOptions
@@ -81,6 +88,7 @@ export class Client<
81
88
  this.state.set("connected", false);
82
89
  }
83
90
 
91
+ /** The base URL for this client's connections/requests. */
84
92
  get baseURL() {
85
93
  return this.options.baseURL || ''
86
94
  }
@@ -89,14 +97,17 @@ export class Client<
89
97
  return this._options as K;
90
98
  }
91
99
 
100
+ /** Configure this client instance with additional options. */
92
101
  configure(options?: any): this {
93
102
  return this;
94
103
  }
95
104
 
105
+ /** Whether the client is currently connected. */
96
106
  get isConnected() {
97
107
  return !!this.state.get("connected");
98
108
  }
99
109
 
110
+ /** Establish a connection. Subclasses should override with protocol-specific logic. */
100
111
  async connect(): Promise<this> {
101
112
  this.state.set("connected", true);
102
113
  return this;
@@ -104,7 +115,7 @@ export class Client<
104
115
  }
105
116
 
106
117
  // --- Registry and Client.register must be defined BEFORE subclasses ---
107
- // because static blocks in RestClient/GraphClient/WebSocketClient run at class declaration time.
118
+ // because static blocks in subclass files run at class declaration time.
108
119
 
109
120
  export class ClientsRegistry extends Registry<Client<any>> {
110
121
  override scope = "clients"
@@ -156,349 +167,4 @@ Client.register = function registerClient(
156
167
  return SubClass
157
168
  }
158
169
 
159
- // --- Built-in client subclasses ---
160
-
161
- export class RestClient<
162
- T extends ClientState = ClientState,
163
- K extends ClientOptions = ClientOptions
164
- > extends Client<T, K> {
165
- axios!: AxiosInstance;
166
-
167
- static override shortcut: string = "clients.rest"
168
- static { Client.register(this, 'rest') }
169
-
170
- constructor(options: K, context: ContainerContext) {
171
- super(options, context);
172
-
173
- this.axios = axios.create({
174
- baseURL: this.baseURL,
175
- });
176
-
177
- if (this.useJSON) {
178
- this.axios.defaults.headers.common = {
179
- ...this.axios.defaults.headers.common,
180
- "Content-Type": "application/json",
181
- Accept: "application/json",
182
- }
183
- }
184
- }
185
-
186
- async beforeRequest() {
187
- }
188
-
189
- get useJSON() {
190
- return !!this.options.json
191
- }
192
-
193
- override get baseURL() {
194
- return this.options.baseURL || '/'
195
- }
196
-
197
- async patch(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
198
- await this.beforeRequest();
199
- return this.axios({
200
- ...options,
201
- method: "PATCH",
202
- url,
203
- data,
204
- })
205
- .then((r) => r.data)
206
- .catch((e: any) => {
207
- if (e.isAxiosError) {
208
- return this.handleError(e);
209
- } else {
210
- throw e;
211
- }
212
- });
213
- }
214
-
215
- async put(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
216
- await this.beforeRequest();
217
- return this.axios({
218
- ...options,
219
- method: "PUT",
220
- url,
221
- data,
222
- })
223
- .then((r) => r.data)
224
- .catch((e: any) => {
225
- if (e.isAxiosError) {
226
- return this.handleError(e);
227
- } else {
228
- throw e;
229
- }
230
- });
231
- }
232
-
233
- async post(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
234
- await this.beforeRequest();
235
- return this.axios({
236
- ...options,
237
- method: "POST",
238
- url,
239
- data,
240
- })
241
- .then((r) => r.data)
242
- .catch((e: any) => {
243
- if (e.isAxiosError) {
244
- return this.handleError(e);
245
- } else {
246
- throw e;
247
- }
248
- });
249
- }
250
-
251
- async delete(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
252
- await this.beforeRequest();
253
- return this.axios({
254
- ...options,
255
- method: "DELETE",
256
- url,
257
- params,
258
- })
259
- .then((r) => r.data)
260
- .catch((e: any) => {
261
- if (e.isAxiosError) {
262
- return this.handleError(e);
263
- } else {
264
- throw e;
265
- }
266
- });
267
- }
268
-
269
-
270
- async get(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
271
- await this.beforeRequest()
272
- return this.axios({
273
- ...options,
274
- method: "GET",
275
- url,
276
- params,
277
- })
278
- .then((r) => r.data)
279
- .catch((e: any) => {
280
- if (e.isAxiosError) {
281
- return this.handleError(e);
282
- } else {
283
- throw e;
284
- }
285
- });
286
- }
287
-
288
- async handleError(error: AxiosError) {
289
- this.emit('failure', error)
290
- return error.toJSON();
291
- }
292
- }
293
-
294
- /**
295
- * GraphQL client that wraps RestClient with convenience methods for executing
296
- * queries and mutations. Automatically handles the GraphQL request envelope
297
- * (query/variables/operationName) and unwraps responses, extracting the `data`
298
- * field and emitting events for GraphQL-level errors.
299
- */
300
- export class GraphClient<
301
- T extends ClientState = ClientState,
302
- K extends GraphClientOptions = GraphClientOptions
303
- > extends RestClient<T, K> {
304
- static override shortcut = "clients.graph" as const
305
- static override optionsSchema = GraphClientOptionsSchema
306
- static override eventsSchema = GraphClientEventsSchema
307
- static { Client.register(this, 'graph') }
308
-
309
- /** The GraphQL endpoint path. Defaults to '/graphql'. */
310
- get endpoint() {
311
- return (this.options as GraphClientOptions).endpoint || '/graphql'
312
- }
313
-
314
- /**
315
- * Execute a GraphQL query and return the unwrapped data.
316
- * @param query - The GraphQL query string
317
- * @param variables - Optional variables for the query
318
- * @param operationName - Optional operation name when the query contains multiple operations
319
- */
320
- async query<R = any>(query: string, variables?: Record<string, any>, operationName?: string): Promise<R> {
321
- return this.execute<R>(query, variables, operationName)
322
- }
323
-
324
- /**
325
- * Execute a GraphQL mutation and return the unwrapped data.
326
- * Semantically identical to query() but named for clarity when performing mutations.
327
- * @param mutation - The GraphQL mutation string
328
- * @param variables - Optional variables for the mutation
329
- * @param operationName - Optional operation name when the mutation contains multiple operations
330
- */
331
- async mutate<R = any>(mutation: string, variables?: Record<string, any>, operationName?: string): Promise<R> {
332
- return this.execute<R>(mutation, variables, operationName)
333
- }
334
-
335
- /**
336
- * Execute a GraphQL operation, unwrap the response, and handle errors.
337
- * Posts to the configured endpoint with the standard GraphQL envelope.
338
- * If the response contains GraphQL-level errors, emits both 'graphqlError'
339
- * and 'failure' events before returning the data.
340
- */
341
- private async execute<R = any>(query: string, variables?: Record<string, any>, operationName?: string): Promise<R> {
342
- const body: Record<string, any> = { query }
343
- if (variables) body.variables = variables
344
- if (operationName) body.operationName = operationName
345
-
346
- const response = await this.post(this.endpoint, body)
347
-
348
- if (response?.errors?.length) {
349
- this.emit('graphqlError', response.errors)
350
- this.emit('failure', response.errors)
351
- }
352
-
353
- return response?.data as R
354
- }
355
- }
356
-
357
- /**
358
- * WebSocket client that bridges raw WebSocket events to Luca's Helper event bus,
359
- * providing a clean interface for sending/receiving messages, tracking connection
360
- * state, and optional auto-reconnection with exponential backoff.
361
- *
362
- * Events emitted:
363
- * - `open` — connection established
364
- * - `message` — message received (JSON-parsed when possible)
365
- * - `close` — connection closed (with code and reason)
366
- * - `error` — connection error
367
- * - `reconnecting` — attempting reconnection (with attempt number)
368
- */
369
- export class WebSocketClient<
370
- T extends WebSocketClientState = WebSocketClientState,
371
- K extends WebSocketClientOptions = WebSocketClientOptions
372
- > extends Client<T, K> {
373
- ws!: WebSocket
374
- _intentionalClose: boolean
375
-
376
- static override shortcut = "clients.websocket" as const
377
- static override stateSchema = WebSocketClientStateSchema
378
- static override optionsSchema = WebSocketClientOptionsSchema
379
- static override eventsSchema = WebSocketClientEventsSchema
380
- static { Client.register(this, 'websocket') }
381
-
382
- constructor(options?: K, context?: ContainerContext) {
383
- super(options, context)
384
- this._intentionalClose = false
385
- }
386
-
387
- override get initialState(): T {
388
- return {
389
- connected: false,
390
- reconnectAttempts: 0,
391
- } as T
392
- }
393
-
394
- /**
395
- * Establish a WebSocket connection to the configured baseURL.
396
- * Wires all raw WebSocket events (open, message, close, error) to the
397
- * Helper event bus and updates connection state accordingly.
398
- * Resolves once the connection is open; rejects on error.
399
- */
400
- override async connect(): Promise<this> {
401
- if (this.isConnected) {
402
- return this
403
- }
404
-
405
- const ws = this.ws = new WebSocket(this.baseURL)
406
- const state = this.state as any
407
-
408
- await new Promise<void>((resolve, reject) => {
409
- ws.onopen = () => {
410
- state.set('connected', true)
411
- state.set('connectionError', undefined)
412
- state.set('reconnectAttempts', 0)
413
- this.emit('open')
414
- resolve()
415
- }
416
-
417
- ws.onerror = (event: any) => {
418
- state.set('connectionError', event)
419
- this.emit('error', event)
420
- reject(event)
421
- }
422
-
423
- ws.onmessage = (event: any) => {
424
- let data = event?.data ?? event
425
- try {
426
- data = JSON.parse(data)
427
- } catch {}
428
- this.emit('message', data)
429
- }
430
-
431
- ws.onclose = (event: any) => {
432
- state.set('connected', false)
433
- this.emit('close', event?.code, event?.reason)
434
- if (!this._intentionalClose) {
435
- this.maybeReconnect()
436
- }
437
- this._intentionalClose = false
438
- }
439
- })
440
-
441
- return this
442
- }
443
-
444
- /**
445
- * Send data over the WebSocket connection. Automatically JSON-serializes
446
- * the payload. If not currently connected, attempts to connect first.
447
- * @param data - The data to send (will be JSON.stringify'd)
448
- */
449
- async send(data: any): Promise<void> {
450
- if (!this.isConnected) {
451
- await this.connect()
452
- }
453
-
454
- if (!this.ws) {
455
- throw new Error('WebSocket instance not available')
456
- }
457
-
458
- this.ws.send(JSON.stringify(data))
459
- }
460
-
461
- /**
462
- * Gracefully close the WebSocket connection. Suppresses auto-reconnect
463
- * and updates connection state to disconnected.
464
- */
465
- async disconnect(): Promise<this> {
466
- this._intentionalClose = true
467
- if (this.ws) {
468
- this.ws.close()
469
- }
470
- ;(this.state as any).set('connected', false)
471
- return this
472
- }
473
-
474
- /** Whether the client is in an error state. */
475
- get hasError() {
476
- return !!(this.state as any).get('connectionError')
477
- }
478
-
479
- /**
480
- * Attempt to reconnect if the reconnect option is enabled and we haven't
481
- * exceeded maxReconnectAttempts. Uses exponential backoff capped at 30s.
482
- */
483
- private maybeReconnect() {
484
- const opts = this.options as WebSocketClientOptions
485
- if (!opts.reconnect) return
486
-
487
- const state = this.state as any
488
- const maxAttempts = opts.maxReconnectAttempts ?? Infinity
489
- const baseInterval = opts.reconnectInterval ?? 1000
490
- const attempts = ((state.get('reconnectAttempts') as number) ?? 0) + 1
491
-
492
- if (attempts > maxAttempts) return
493
-
494
- state.set('reconnectAttempts', attempts)
495
- this.emit('reconnecting', attempts)
496
-
497
- const delay = Math.min(baseInterval * Math.pow(2, attempts - 1), 30000)
498
- setTimeout(() => {
499
- this.connect().catch(() => {})
500
- }, delay)
501
- }
502
- }
503
-
504
170
  export default Client;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  Client,
3
3
  type ClientOptions,
4
- RestClient,
5
4
  } from "@soederpop/luca/client";
5
+ import { RestClient } from "../rest";
6
6
  import { type ContainerContext } from "@soederpop/luca/container";
7
7
  import { isEmpty, maxBy, omitBy } from "lodash-es";
8
8
  import { NodeContainer } from "@soederpop/luca/node/container";
@@ -2,8 +2,8 @@ import {
2
2
  type ClientOptions,
3
3
  type ClientsInterface,
4
4
  clients,
5
- RestClient,
6
5
  } from "@soederpop/luca/client";
6
+ import { RestClient } from "./rest";
7
7
  import { type ContainerContext } from "@soederpop/luca/container";
8
8
  import { z } from 'zod'
9
9
  import { ClientStateSchema } from '@soederpop/luca/schemas/base.js'
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  Client,
3
- RestClient,
4
3
  } from "@soederpop/luca/client";
4
+ import { RestClient } from "../rest";
5
5
  import type { ContainerContext } from "@soederpop/luca/container";
6
6
  import { z } from 'zod'
7
- import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca/schemas/base.js'
7
+ import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca/schemas/base.js'
8
8
 
9
9
  declare module "@soederpop/luca/client" {
10
10
  interface AvailableClients {
@@ -25,6 +25,16 @@ export const ComfyUIClientOptionsSchema = ClientOptionsSchema.extend({
25
25
  export type ComfyUIClientState = z.infer<typeof ComfyUIClientStateSchema>
26
26
  export type ComfyUIClientOptions = z.infer<typeof ComfyUIClientOptionsSchema>
27
27
 
28
+ export const ComfyUIClientEventsSchema = ClientEventsSchema.extend({
29
+ execution_start: z.tuple([z.object({ promptId: z.string().describe('The prompt ID that started executing') })]).describe('Emitted when prompt execution begins'),
30
+ executing: z.tuple([z.object({ node: z.string().describe('The node ID currently executing'), promptId: z.string().describe('The prompt ID') })]).describe('Emitted when a specific node begins executing'),
31
+ progress: z.tuple([z.object({ node: z.string().describe('The node ID'), value: z.number().describe('Current progress value'), max: z.number().describe('Maximum progress value'), promptId: z.string().describe('The prompt ID') })]).describe('Emitted during node execution with progress updates'),
32
+ executed: z.tuple([z.object({ node: z.string().describe('The node ID that finished'), output: z.any().describe('The node output data'), promptId: z.string().describe('The prompt ID') })]).describe('Emitted when a node finishes execution'),
33
+ execution_cached: z.tuple([z.object({ nodes: z.array(z.string()).describe('Array of cached node IDs'), promptId: z.string().describe('The prompt ID') })]).describe('Emitted when nodes are served from cache'),
34
+ execution_error: z.tuple([z.object({ promptId: z.string().describe('The prompt ID'), exception_message: z.string().optional().describe('Error message') }).passthrough()]).describe('Emitted when prompt execution fails'),
35
+ execution_complete: z.tuple([z.object({ promptId: z.string().describe('The prompt ID that completed') })]).describe('Emitted when prompt execution finishes successfully'),
36
+ }).describe('ComfyUI client events')
37
+
28
38
  /** Maps a semantic input name to a specific node ID and field */
29
39
  export type InputMapping = Record<string, { nodeId: string; field: string }>;
30
40
 
@@ -66,6 +76,7 @@ export class ComfyUIClient extends RestClient<ComfyUIClientState, ComfyUIClientO
66
76
  static override description = "ComfyUI workflow execution client";
67
77
  static override stateSchema = ComfyUIClientStateSchema;
68
78
  static override optionsSchema = ComfyUIClientOptionsSchema;
79
+ static override eventsSchema = ComfyUIClientEventsSchema;
69
80
 
70
81
  static { Client.register(this, 'comfyui') }
71
82
 
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
  import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca/schemas/base.js'
3
- import { Client, RestClient } from "@soederpop/luca/client";
3
+ import { Client } from "@soederpop/luca/client";
4
+ import { RestClient } from "../rest";
4
5
  import type { ContainerContext } from "@soederpop/luca/container";
5
6
  import type { AxiosRequestConfig } from 'axios'
6
7