@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
@@ -0,0 +1,207 @@
1
+ import axios, { type AxiosError, type AxiosInstance, type AxiosRequestConfig } from "axios";
2
+ import { Client, type ClientOptions, type ClientState } from '../client.js'
3
+ import type { ContainerContext } from '../container.js'
4
+ import { ClientEventsSchema } from '../schemas/base.js'
5
+ import { z } from 'zod'
6
+
7
+ export const RestClientEventsSchema = ClientEventsSchema.extend({}).describe('REST client events')
8
+
9
+ declare module '../client' {
10
+ interface AvailableClients {
11
+ rest: typeof RestClient
12
+ }
13
+ }
14
+
15
+ /**
16
+ * HTTP REST client built on top of axios. Provides convenience methods for
17
+ * GET, POST, PUT, PATCH, and DELETE requests with automatic JSON handling,
18
+ * configurable base URL, and error event emission.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const api = container.client('rest', { baseURL: 'https://api.example.com', json: true })
23
+ * const users = await api.get('/users')
24
+ * await api.post('/users', { name: 'Alice' })
25
+ * ```
26
+ */
27
+ export class RestClient<
28
+ T extends ClientState = ClientState,
29
+ K extends ClientOptions = ClientOptions
30
+ > extends Client<T, K> {
31
+ axios!: AxiosInstance;
32
+
33
+ static override shortcut: string = "clients.rest"
34
+ static override eventsSchema = RestClientEventsSchema
35
+ static { Client.register(this, 'rest') }
36
+
37
+ constructor(options: K, context: ContainerContext) {
38
+ super(options, context);
39
+
40
+ this.axios = axios.create({
41
+ baseURL: this.baseURL,
42
+ });
43
+
44
+ if (this.useJSON) {
45
+ this.axios.defaults.headers.common = {
46
+ ...this.axios.defaults.headers.common,
47
+ "Content-Type": "application/json",
48
+ Accept: "application/json",
49
+ }
50
+ }
51
+ }
52
+
53
+ async beforeRequest() {
54
+ }
55
+
56
+ /** Whether JSON content-type headers should be set automatically. */
57
+ get useJSON() {
58
+ return !!this.options.json
59
+ }
60
+
61
+ override get baseURL() {
62
+ return this.options.baseURL || '/'
63
+ }
64
+
65
+ /**
66
+ * Send a PATCH request. Returns the parsed response body directly (not an
67
+ * axios Response wrapper). On HTTP errors, returns the error as JSON instead
68
+ * of throwing.
69
+ * @param url - Request path relative to baseURL
70
+ * @param data - Request body
71
+ * @param options - Additional axios request config
72
+ * @returns Parsed response body
73
+ */
74
+ async patch(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
75
+ await this.beforeRequest();
76
+ return this.axios({
77
+ ...options,
78
+ method: "PATCH",
79
+ url,
80
+ data,
81
+ })
82
+ .then((r) => r.data)
83
+ .catch((e: any) => {
84
+ if (e.isAxiosError) {
85
+ return this.handleError(e);
86
+ } else {
87
+ throw e;
88
+ }
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Send a PUT request. Returns the parsed response body directly (not an
94
+ * axios Response wrapper). On HTTP errors, returns the error as JSON instead
95
+ * of throwing.
96
+ * @param url - Request path relative to baseURL
97
+ * @param data - Request body
98
+ * @param options - Additional axios request config
99
+ * @returns Parsed response body
100
+ */
101
+ async put(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
102
+ await this.beforeRequest();
103
+ return this.axios({
104
+ ...options,
105
+ method: "PUT",
106
+ url,
107
+ data,
108
+ })
109
+ .then((r) => r.data)
110
+ .catch((e: any) => {
111
+ if (e.isAxiosError) {
112
+ return this.handleError(e);
113
+ } else {
114
+ throw e;
115
+ }
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Send a POST request. Returns the parsed response body directly (not an
121
+ * axios Response wrapper). On HTTP errors, returns the error as JSON instead
122
+ * of throwing.
123
+ * @param url - Request path relative to baseURL
124
+ * @param data - Request body
125
+ * @param options - Additional axios request config
126
+ * @returns Parsed response body
127
+ */
128
+ async post(url: string, data: any = {}, options: AxiosRequestConfig = {}) {
129
+ await this.beforeRequest();
130
+ return this.axios({
131
+ ...options,
132
+ method: "POST",
133
+ url,
134
+ data,
135
+ })
136
+ .then((r) => r.data)
137
+ .catch((e: any) => {
138
+ if (e.isAxiosError) {
139
+ return this.handleError(e);
140
+ } else {
141
+ throw e;
142
+ }
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Send a DELETE request. Returns the parsed response body directly (not an
148
+ * axios Response wrapper). On HTTP errors, returns the error as JSON instead
149
+ * of throwing.
150
+ * @param url - Request path relative to baseURL
151
+ * @param params - Query parameters
152
+ * @param options - Additional axios request config
153
+ * @returns Parsed response body
154
+ */
155
+ async delete(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
156
+ await this.beforeRequest();
157
+ return this.axios({
158
+ ...options,
159
+ method: "DELETE",
160
+ url,
161
+ params,
162
+ })
163
+ .then((r) => r.data)
164
+ .catch((e: any) => {
165
+ if (e.isAxiosError) {
166
+ return this.handleError(e);
167
+ } else {
168
+ throw e;
169
+ }
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Send a GET request. Returns the parsed response body directly (not an
175
+ * axios Response wrapper). On HTTP errors, returns the error as JSON instead
176
+ * of throwing.
177
+ * @param url - Request path relative to baseURL
178
+ * @param params - Query parameters
179
+ * @param options - Additional axios request config
180
+ * @returns Parsed response body
181
+ */
182
+ async get(url: string, params: any = {}, options: AxiosRequestConfig = {}) {
183
+ await this.beforeRequest()
184
+ return this.axios({
185
+ ...options,
186
+ method: "GET",
187
+ url,
188
+ params,
189
+ })
190
+ .then((r) => r.data)
191
+ .catch((e: any) => {
192
+ if (e.isAxiosError) {
193
+ return this.handleError(e);
194
+ } else {
195
+ throw e;
196
+ }
197
+ });
198
+ }
199
+
200
+ /** Handle an axios error by emitting 'failure' and returning the error as JSON. */
201
+ async handleError(error: AxiosError) {
202
+ this.emit('failure', error)
203
+ return error.toJSON();
204
+ }
205
+ }
206
+
207
+ export default RestClient
@@ -0,0 +1,176 @@
1
+ import { z } from 'zod'
2
+ import { Client } from '../client.js'
3
+ import type { ContainerContext } from '../container.js'
4
+ import {
5
+ WebSocketClientStateSchema, WebSocketClientOptionsSchema, WebSocketClientEventsSchema,
6
+ } from '../schemas/base.js'
7
+
8
+ export type WebSocketClientState = z.infer<typeof WebSocketClientStateSchema>
9
+ export type WebSocketClientOptions = z.infer<typeof WebSocketClientOptionsSchema>
10
+
11
+ declare module '../client' {
12
+ interface AvailableClients {
13
+ websocket: typeof WebSocketClient
14
+ }
15
+ }
16
+
17
+ /**
18
+ * WebSocket client that bridges raw WebSocket events to Luca's Helper event bus,
19
+ * providing a clean interface for sending/receiving messages, tracking connection
20
+ * state, and optional auto-reconnection with exponential backoff.
21
+ *
22
+ * Events emitted:
23
+ * - `open` — connection established
24
+ * - `message` — message received (JSON-parsed when possible)
25
+ * - `close` — connection closed (with code and reason)
26
+ * - `error` — connection error
27
+ * - `reconnecting` — attempting reconnection (with attempt number)
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const ws = container.client('websocket', {
32
+ * baseURL: 'ws://localhost:8080',
33
+ * reconnect: true,
34
+ * maxReconnectAttempts: 5
35
+ * })
36
+ * ws.on('message', (data) => console.log('Received:', data))
37
+ * await ws.connect()
38
+ * await ws.send({ type: 'hello' })
39
+ * ```
40
+ */
41
+ export class WebSocketClient<
42
+ T extends WebSocketClientState = WebSocketClientState,
43
+ K extends WebSocketClientOptions = WebSocketClientOptions
44
+ > extends Client<T, K> {
45
+ ws!: WebSocket
46
+ _intentionalClose: boolean
47
+
48
+ static override shortcut = "clients.websocket" as const
49
+ static override stateSchema = WebSocketClientStateSchema
50
+ static override optionsSchema = WebSocketClientOptionsSchema
51
+ static override eventsSchema = WebSocketClientEventsSchema
52
+ static { Client.register(this, 'websocket') }
53
+
54
+ constructor(options?: K, context?: ContainerContext) {
55
+ super(options, context)
56
+ this._intentionalClose = false
57
+ }
58
+
59
+ override get initialState(): T {
60
+ return {
61
+ connected: false,
62
+ reconnectAttempts: 0,
63
+ } as T
64
+ }
65
+
66
+ /**
67
+ * Establish a WebSocket connection to the configured baseURL.
68
+ * Wires all raw WebSocket events (open, message, close, error) to the
69
+ * Helper event bus and updates connection state accordingly.
70
+ * Resolves once the connection is open; rejects on error.
71
+ */
72
+ override async connect(): Promise<this> {
73
+ if (this.isConnected) {
74
+ return this
75
+ }
76
+
77
+ const ws = this.ws = new WebSocket(this.baseURL)
78
+ const state = this.state as any
79
+
80
+ await new Promise<void>((resolve, reject) => {
81
+ ws.onopen = () => {
82
+ state.set('connected', true)
83
+ state.set('connectionError', undefined)
84
+ state.set('reconnectAttempts', 0)
85
+ this.emit('open')
86
+ resolve()
87
+ }
88
+
89
+ ws.onerror = (event: any) => {
90
+ state.set('connectionError', event)
91
+ this.emit('error', event)
92
+ reject(event)
93
+ }
94
+
95
+ ws.onmessage = (event: any) => {
96
+ let data = event?.data ?? event
97
+ try {
98
+ data = JSON.parse(data)
99
+ } catch {}
100
+ this.emit('message', data)
101
+ }
102
+
103
+ ws.onclose = (event: any) => {
104
+ state.set('connected', false)
105
+ this.emit('close', event?.code, event?.reason)
106
+ if (!this._intentionalClose) {
107
+ this.maybeReconnect()
108
+ }
109
+ this._intentionalClose = false
110
+ }
111
+ })
112
+
113
+ return this
114
+ }
115
+
116
+ /**
117
+ * Send data over the WebSocket connection. Automatically JSON-serializes
118
+ * the payload. If not currently connected, attempts to connect first.
119
+ * @param data - The data to send (will be JSON.stringify'd)
120
+ */
121
+ async send(data: any): Promise<void> {
122
+ if (!this.isConnected) {
123
+ await this.connect()
124
+ }
125
+
126
+ if (!this.ws) {
127
+ throw new Error('WebSocket instance not available')
128
+ }
129
+
130
+ this.ws.send(JSON.stringify(data))
131
+ }
132
+
133
+ /**
134
+ * Gracefully close the WebSocket connection. Suppresses auto-reconnect
135
+ * and updates connection state to disconnected.
136
+ */
137
+ async disconnect(): Promise<this> {
138
+ this._intentionalClose = true
139
+ if (this.ws) {
140
+ this.ws.close()
141
+ }
142
+ ;(this.state as any).set('connected', false)
143
+ return this
144
+ }
145
+
146
+ /** Whether the client is in an error state. */
147
+ get hasError() {
148
+ return !!(this.state as any).get('connectionError')
149
+ }
150
+
151
+ /**
152
+ * Attempt to reconnect if the reconnect option is enabled and we haven't
153
+ * exceeded maxReconnectAttempts. Uses exponential backoff capped at 30s.
154
+ */
155
+ private maybeReconnect() {
156
+ const opts = this.options as WebSocketClientOptions
157
+ if (!opts.reconnect) return
158
+
159
+ const state = this.state as any
160
+ const maxAttempts = opts.maxReconnectAttempts ?? Infinity
161
+ const baseInterval = opts.reconnectInterval ?? 1000
162
+ const attempts = ((state.get('reconnectAttempts') as number) ?? 0) + 1
163
+
164
+ if (attempts > maxAttempts) return
165
+
166
+ state.set('reconnectAttempts', attempts)
167
+ this.emit('reconnecting', attempts)
168
+
169
+ const delay = Math.min(baseInterval * Math.pow(2, attempts - 1), 30000)
170
+ setTimeout(() => {
171
+ this.connect().catch(() => {})
172
+ }, delay)
173
+ }
174
+ }
175
+
176
+ export default WebSocketClient