@soederpop/luca 0.0.5 → 0.0.7

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 (211) hide show
  1. package/CLAUDE.md +10 -1
  2. package/bun.lock +1 -1
  3. package/commands/build-bootstrap.ts +78 -0
  4. package/commands/build-scaffolds.ts +24 -2
  5. package/commands/try-all-challenges.ts +543 -0
  6. package/commands/try-challenge.ts +100 -0
  7. package/docs/README.md +52 -80
  8. package/docs/TABLE-OF-CONTENTS.md +82 -51
  9. package/docs/apis/clients/elevenlabs.md +232 -8
  10. package/docs/apis/clients/graph.md +59 -8
  11. package/docs/apis/clients/openai.md +362 -2
  12. package/docs/apis/clients/rest.md +122 -2
  13. package/docs/apis/clients/websocket.md +71 -17
  14. package/docs/apis/features/agi/assistant.md +9 -3
  15. package/docs/apis/features/agi/assistants-manager.md +2 -2
  16. package/docs/apis/features/agi/claude-code.md +153 -14
  17. package/docs/apis/features/agi/conversation-history.md +15 -3
  18. package/docs/apis/features/agi/conversation.md +133 -20
  19. package/docs/apis/features/agi/openai-codex.md +90 -12
  20. package/docs/apis/features/agi/skills-library.md +23 -5
  21. package/docs/apis/features/node/container-link.md +59 -0
  22. package/docs/apis/features/node/content-db.md +1 -1
  23. package/docs/apis/features/node/disk-cache.md +1 -1
  24. package/docs/apis/features/node/dns.md +1 -0
  25. package/docs/apis/features/node/docker.md +2 -1
  26. package/docs/apis/features/node/esbuild.md +4 -3
  27. package/docs/apis/features/node/file-manager.md +13 -4
  28. package/docs/apis/features/node/fs.md +726 -171
  29. package/docs/apis/features/node/git.md +1 -0
  30. package/docs/apis/features/node/google-auth.md +23 -4
  31. package/docs/apis/features/node/google-calendar.md +14 -2
  32. package/docs/apis/features/node/google-docs.md +15 -2
  33. package/docs/apis/features/node/google-drive.md +21 -3
  34. package/docs/apis/features/node/google-sheets.md +14 -2
  35. package/docs/apis/features/node/grep.md +2 -0
  36. package/docs/apis/features/node/helpers.md +29 -0
  37. package/docs/apis/features/node/ink.md +2 -2
  38. package/docs/apis/features/node/networking.md +39 -4
  39. package/docs/apis/features/node/os.md +28 -0
  40. package/docs/apis/features/node/postgres.md +26 -4
  41. package/docs/apis/features/node/proc.md +37 -28
  42. package/docs/apis/features/node/process-manager.md +33 -5
  43. package/docs/apis/features/node/repl.md +1 -1
  44. package/docs/apis/features/node/runpod.md +1 -0
  45. package/docs/apis/features/node/secure-shell.md +7 -0
  46. package/docs/apis/features/node/semantic-search.md +12 -5
  47. package/docs/apis/features/node/sqlite.md +26 -4
  48. package/docs/apis/features/node/telegram.md +30 -5
  49. package/docs/apis/features/node/tts.md +17 -2
  50. package/docs/apis/features/node/ui.md +1 -1
  51. package/docs/apis/features/node/vault.md +4 -9
  52. package/docs/apis/features/node/vm.md +3 -12
  53. package/docs/apis/features/node/window-manager.md +128 -20
  54. package/docs/apis/features/web/asset-loader.md +13 -1
  55. package/docs/apis/features/web/container-link.md +59 -0
  56. package/docs/apis/features/web/esbuild.md +4 -3
  57. package/docs/apis/features/web/helpers.md +29 -0
  58. package/docs/apis/features/web/network.md +16 -2
  59. package/docs/apis/features/web/speech.md +16 -2
  60. package/docs/apis/features/web/vault.md +4 -9
  61. package/docs/apis/features/web/vm.md +3 -12
  62. package/docs/apis/features/web/voice.md +18 -1
  63. package/docs/apis/servers/express.md +18 -2
  64. package/docs/apis/servers/mcp.md +29 -4
  65. package/docs/apis/servers/websocket.md +34 -6
  66. package/docs/bootstrap/CLAUDE.md +100 -0
  67. package/docs/bootstrap/SKILL.md +222 -0
  68. package/docs/bootstrap/templates/about-command.ts +41 -0
  69. package/docs/bootstrap/templates/docs-models.ts +22 -0
  70. package/docs/bootstrap/templates/docs-readme.md +43 -0
  71. package/docs/bootstrap/templates/example-feature.ts +53 -0
  72. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  73. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  74. package/docs/challenges/caching-proxy.md +16 -0
  75. package/docs/challenges/content-db-round-trip.md +14 -0
  76. package/docs/challenges/custom-command.md +9 -0
  77. package/docs/challenges/file-watcher-pipeline.md +11 -0
  78. package/docs/challenges/grep-audit-report.md +15 -0
  79. package/docs/challenges/multi-feature-dashboard.md +14 -0
  80. package/docs/challenges/process-orchestrator.md +17 -0
  81. package/docs/challenges/rest-api-server-with-client.md +12 -0
  82. package/docs/challenges/script-runner-with-vm.md +11 -0
  83. package/docs/challenges/simple-rest-api.md +15 -0
  84. package/docs/challenges/websocket-serve-and-client.md +11 -0
  85. package/docs/challenges/yaml-config-system.md +14 -0
  86. package/docs/command-system-overhaul.md +94 -0
  87. package/docs/examples/assistant/CORE.md +18 -0
  88. package/docs/examples/assistant/hooks.ts +3 -0
  89. package/docs/examples/assistant/tools.ts +10 -0
  90. package/docs/examples/window-manager-layouts.md +180 -0
  91. package/docs/in-memory-fs.md +4 -0
  92. package/docs/models.ts +13 -10
  93. package/docs/philosophy.md +4 -3
  94. package/docs/reports/console-hmr-design.md +170 -0
  95. package/docs/reports/helper-semantic-search.md +72 -0
  96. package/docs/scaffolds/client.md +29 -20
  97. package/docs/scaffolds/command.md +64 -50
  98. package/docs/scaffolds/endpoint.md +31 -36
  99. package/docs/scaffolds/feature.md +28 -18
  100. package/docs/scaffolds/selector.md +91 -0
  101. package/docs/scaffolds/server.md +18 -9
  102. package/docs/selectors.md +115 -0
  103. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  104. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  105. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  106. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  107. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  108. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  109. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  110. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  111. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  112. package/docs/tutorials/00-bootstrap.md +148 -0
  113. package/docs/tutorials/07-endpoints.md +7 -7
  114. package/docs/tutorials/08-commands.md +153 -72
  115. package/luca.cli.ts +3 -0
  116. package/package.json +6 -5
  117. package/public/index.html +1430 -0
  118. package/scripts/examples/using-ollama.ts +2 -1
  119. package/scripts/update-introspection-data.ts +2 -2
  120. package/src/agi/endpoints/experts.ts +1 -1
  121. package/src/agi/features/assistant.ts +7 -0
  122. package/src/agi/features/assistants-manager.ts +5 -5
  123. package/src/agi/features/claude-code.ts +263 -3
  124. package/src/agi/features/conversation-history.ts +7 -1
  125. package/src/agi/features/conversation.ts +26 -3
  126. package/src/agi/features/openai-codex.ts +26 -2
  127. package/src/agi/features/openapi.ts +6 -1
  128. package/src/agi/features/skills-library.ts +9 -1
  129. package/src/bootstrap/generated.ts +540 -0
  130. package/src/cli/cli.ts +64 -21
  131. package/src/client.ts +23 -357
  132. package/src/clients/civitai/index.ts +1 -1
  133. package/src/clients/client-template.ts +1 -1
  134. package/src/clients/comfyui/index.ts +13 -2
  135. package/src/clients/elevenlabs/index.ts +2 -1
  136. package/src/clients/graph.ts +87 -0
  137. package/src/clients/openai/index.ts +10 -1
  138. package/src/clients/rest.ts +207 -0
  139. package/src/clients/websocket.ts +176 -0
  140. package/src/command.ts +281 -34
  141. package/src/commands/bootstrap.ts +181 -0
  142. package/src/commands/chat.ts +5 -4
  143. package/src/commands/describe.ts +225 -2
  144. package/src/commands/help.ts +35 -9
  145. package/src/commands/index.ts +3 -0
  146. package/src/commands/introspect.ts +92 -2
  147. package/src/commands/prompt.ts +5 -6
  148. package/src/commands/run.ts +33 -10
  149. package/src/commands/save-api-docs.ts +49 -0
  150. package/src/commands/scaffold.ts +169 -23
  151. package/src/commands/select.ts +94 -0
  152. package/src/commands/serve.ts +10 -1
  153. package/src/container.ts +15 -0
  154. package/src/endpoint.ts +19 -0
  155. package/src/graft.ts +181 -0
  156. package/src/introspection/generated.agi.ts +12458 -8968
  157. package/src/introspection/generated.node.ts +10573 -7145
  158. package/src/introspection/generated.web.ts +1 -1
  159. package/src/introspection/index.ts +26 -0
  160. package/src/node/container.ts +6 -7
  161. package/src/node/features/content-db.ts +49 -2
  162. package/src/node/features/disk-cache.ts +16 -9
  163. package/src/node/features/dns.ts +16 -3
  164. package/src/node/features/docker.ts +16 -4
  165. package/src/node/features/esbuild.ts +20 -0
  166. package/src/node/features/file-manager.ts +184 -29
  167. package/src/node/features/fs.ts +704 -248
  168. package/src/node/features/git.ts +21 -8
  169. package/src/node/features/grep.ts +23 -3
  170. package/src/node/features/helpers.ts +372 -43
  171. package/src/node/features/networking.ts +39 -4
  172. package/src/node/features/opener.ts +28 -15
  173. package/src/node/features/os.ts +76 -0
  174. package/src/node/features/port-exposer.ts +11 -1
  175. package/src/node/features/postgres.ts +17 -1
  176. package/src/node/features/proc.ts +4 -1
  177. package/src/node/features/python.ts +63 -14
  178. package/src/node/features/repl.ts +11 -7
  179. package/src/node/features/runpod.ts +16 -3
  180. package/src/node/features/secure-shell.ts +27 -2
  181. package/src/node/features/semantic-search.ts +12 -1
  182. package/src/node/features/ui.ts +5 -69
  183. package/src/node/features/vm.ts +17 -0
  184. package/src/node/features/window-manager.ts +68 -20
  185. package/src/node.ts +5 -0
  186. package/src/scaffolds/generated.ts +492 -290
  187. package/src/scaffolds/template.ts +9 -0
  188. package/src/schemas/base.ts +46 -5
  189. package/src/selector.ts +282 -0
  190. package/src/server.ts +11 -0
  191. package/src/servers/express.ts +27 -12
  192. package/src/servers/socket.ts +45 -11
  193. package/src/web/clients/socket.ts +4 -1
  194. package/src/web/container.ts +2 -1
  195. package/src/web/features/network.ts +7 -1
  196. package/src/web/features/voice-recognition.ts +16 -1
  197. package/test/clients-servers.test.ts +2 -1
  198. package/test/command.test.ts +267 -0
  199. package/test-integration/assistants-manager.test.ts +10 -20
  200. package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
  201. package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
  202. package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
  203. package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
  204. package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
  205. package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
  206. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  207. package/docs/examples/launcher-app-command-listener.md +0 -120
  208. package/docs/tasks/web-container-helper-discovery.md +0 -71
  209. package/docs/todos.md +0 -1
  210. package/scripts/test-command-listener.ts +0 -123
  211. package/src/node/features/launcher-app-command-listener.ts +0 -389
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
 
@@ -0,0 +1,87 @@
1
+ import { z } from 'zod'
2
+ import { Client } from '../client.js'
3
+ import { RestClient } from './rest.js'
4
+ import type { ClientState } from '../client.js'
5
+ import {
6
+ GraphClientOptionsSchema, GraphClientEventsSchema,
7
+ } from '../schemas/base.js'
8
+
9
+ export type GraphClientOptions = z.infer<typeof GraphClientOptionsSchema>
10
+
11
+ declare module '../client' {
12
+ interface AvailableClients {
13
+ graph: typeof GraphClient
14
+ }
15
+ }
16
+
17
+ /**
18
+ * GraphQL client that wraps RestClient with convenience methods for executing
19
+ * queries and mutations. Automatically handles the GraphQL request envelope
20
+ * (query/variables/operationName) and unwraps responses, extracting the `data`
21
+ * field and emitting events for GraphQL-level errors.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const gql = container.client('graph', { baseURL: 'https://api.example.com' })
26
+ * const data = await gql.query(`{ users { id name } }`)
27
+ * await gql.mutate(`mutation($name: String!) { createUser(name: $name) { id } }`, { name: 'Alice' })
28
+ * ```
29
+ */
30
+ export class GraphClient<
31
+ T extends ClientState = ClientState,
32
+ K extends GraphClientOptions = GraphClientOptions
33
+ > extends RestClient<T, K> {
34
+ static override shortcut = "clients.graph" as const
35
+ static override optionsSchema = GraphClientOptionsSchema
36
+ static override eventsSchema = GraphClientEventsSchema
37
+ static { Client.register(this, 'graph') }
38
+
39
+ /** The GraphQL endpoint path. Defaults to '/graphql'. */
40
+ get endpoint() {
41
+ return (this.options as GraphClientOptions).endpoint || '/graphql'
42
+ }
43
+
44
+ /**
45
+ * Execute a GraphQL query and return the unwrapped data.
46
+ * @param query - The GraphQL query string
47
+ * @param variables - Optional variables for the query
48
+ * @param operationName - Optional operation name when the query contains multiple operations
49
+ */
50
+ async query<R = any>(query: string, variables?: Record<string, any>, operationName?: string): Promise<R> {
51
+ return this.execute<R>(query, variables, operationName)
52
+ }
53
+
54
+ /**
55
+ * Execute a GraphQL mutation and return the unwrapped data.
56
+ * Semantically identical to query() but named for clarity when performing mutations.
57
+ * @param mutation - The GraphQL mutation string
58
+ * @param variables - Optional variables for the mutation
59
+ * @param operationName - Optional operation name when the mutation contains multiple operations
60
+ */
61
+ async mutate<R = any>(mutation: string, variables?: Record<string, any>, operationName?: string): Promise<R> {
62
+ return this.execute<R>(mutation, variables, operationName)
63
+ }
64
+
65
+ /**
66
+ * Execute a GraphQL operation, unwrap the response, and handle errors.
67
+ * Posts to the configured endpoint with the standard GraphQL envelope.
68
+ * If the response contains GraphQL-level errors, emits both 'graphqlError'
69
+ * and 'failure' events before returning the data.
70
+ */
71
+ private async execute<R = any>(query: string, variables?: Record<string, any>, operationName?: string): Promise<R> {
72
+ const body: Record<string, any> = { query }
73
+ if (variables) body.variables = variables
74
+ if (operationName) body.operationName = operationName
75
+
76
+ const response = await this.post(this.endpoint, body)
77
+
78
+ if (response?.errors?.length) {
79
+ this.emit('graphqlError', response.errors)
80
+ this.emit('failure', response.errors)
81
+ }
82
+
83
+ return response?.data as R
84
+ }
85
+ }
86
+
87
+ export default GraphClient
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca/schemas/base.js'
2
+ import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca/schemas/base.js'
3
3
  import { Client } from "@soederpop/luca/client";
4
4
  import type { Container, ContainerContext } from "@soederpop/luca/container";
5
5
 
@@ -27,6 +27,14 @@ export const OpenAIClientOptionsSchema = ClientOptionsSchema.extend({
27
27
  })
28
28
  export type OpenAIClientOptions = z.infer<typeof OpenAIClientOptionsSchema>
29
29
 
30
+ export const OpenAIClientEventsSchema = ClientEventsSchema.extend({
31
+ connected: z.tuple([]).describe('Emitted when the API connection is verified'),
32
+ completion: z.tuple([z.any().describe('The completion or response object')]).describe('Emitted after a chat completion, legacy completion, or response is created'),
33
+ embedding: z.tuple([z.any().describe('The embedding response object')]).describe('Emitted after embeddings are created'),
34
+ image: z.tuple([z.any().describe('The image generation response object')]).describe('Emitted after an image is generated'),
35
+ models: z.tuple([z.any().describe('The models list response')]).describe('Emitted after listing available models'),
36
+ }).describe('OpenAI client events')
37
+
30
38
  /**
31
39
  * OpenAI client — wraps the OpenAI SDK for chat completions, responses API, embeddings, and image generation.
32
40
  *
@@ -47,6 +55,7 @@ export class OpenAIClient extends Client<OpenAIClientState, OpenAIClientOptions>
47
55
  static override envVars = ['OPENAI_API_KEY']
48
56
  static override stateSchema = OpenAIClientStateSchema
49
57
  static override optionsSchema = OpenAIClientOptionsSchema
58
+ static override eventsSchema = OpenAIClientEventsSchema
50
59
 
51
60
  static { Client.register(this, 'openai') }
52
61