@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
@@ -1,389 +0,0 @@
1
- import { z } from 'zod'
2
- import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
- import { Feature } from '../feature.js'
4
- import { Server as NetServer, Socket } from 'net'
5
- import { homedir } from 'os'
6
- import { join, dirname } from 'path'
7
- import { existsSync, unlinkSync, mkdirSync } from 'fs'
8
-
9
- const DEFAULT_SOCKET_PATH = join(
10
- homedir(),
11
- 'Library',
12
- 'Application Support',
13
- 'LucaVoiceLauncher',
14
- 'ipc-command.sock'
15
- )
16
-
17
- // --- CommandHandle ---
18
-
19
- /**
20
- * A handle to a single incoming command from the native app.
21
- * Provides methods to acknowledge, report progress, and finish the command.
22
- * All responses are automatically correlated by the command's `id`.
23
- */
24
- export class CommandHandle {
25
- /** The correlation UUID from the app. */
26
- readonly id: string
27
- /** The command text (e.g. "open notes"). */
28
- readonly text: string
29
- /** The input source (e.g. "voice", "hotkey"). */
30
- readonly source: string
31
- /** The full payload object from the app. */
32
- readonly payload: any
33
- /** The entire raw message from the app. */
34
- readonly raw: any
35
-
36
- private _send: (msg: Record<string, any>) => boolean
37
- private _finished = false
38
-
39
- constructor(msg: any, send: (msg: Record<string, any>) => boolean) {
40
- this.id = msg.id
41
- this.text = msg.payload?.text ?? ''
42
- this.source = msg.payload?.source ?? ''
43
- this.payload = msg.payload ?? {}
44
- this.raw = msg
45
- this._send = send
46
- }
47
-
48
- /** Whether `finish()` or `fail()` has been called. */
49
- get isFinished(): boolean {
50
- return this._finished
51
- }
52
-
53
- /**
54
- * Send a processing acknowledgement to the app.
55
- * Optionally include a speech phrase for TTS or an audio file path for playback.
56
- *
57
- * @param speechOrOpts - Text the app will speak, or an options object with speech and/or audioFile
58
- */
59
- ack(speechOrOpts?: string | { speech?: string; audioFile?: string }): boolean {
60
- const opts = typeof speechOrOpts === 'string' ? { speech: speechOrOpts } : speechOrOpts
61
- return this._send({
62
- id: this.id,
63
- status: 'processing',
64
- ...(opts?.speech ? { speech: opts.speech } : {}),
65
- ...(opts?.audioFile ? { audioFile: opts.audioFile } : {}),
66
- timestamp: new Date().toISOString(),
67
- })
68
- }
69
-
70
- /**
71
- * Send a progress update to the app.
72
- *
73
- * @param progress - A number between 0 and 1
74
- * @param message - Optional human-readable progress message
75
- */
76
- progress(progress: number, message?: string): boolean {
77
- return this._send({
78
- id: this.id,
79
- status: 'progress',
80
- progress,
81
- ...(message ? { message } : {}),
82
- timestamp: new Date().toISOString(),
83
- })
84
- }
85
-
86
- /**
87
- * Mark the command as successfully finished.
88
- * Can only be called once per command. All arguments are optional.
89
- *
90
- * @param opts - Optional result payload, speech phrase, and/or audio file path
91
- */
92
- finish(opts?: { result?: Record<string, any>; speech?: string; audioFile?: string }): boolean {
93
- if (this._finished) return false
94
- this._finished = true
95
- return this._send({
96
- id: this.id,
97
- status: 'finished',
98
- success: true,
99
- ...(opts?.result ? { result: opts.result } : {}),
100
- ...(opts?.speech ? { speech: opts.speech } : {}),
101
- ...(opts?.audioFile ? { audioFile: opts.audioFile } : {}),
102
- timestamp: new Date().toISOString(),
103
- })
104
- }
105
-
106
- /**
107
- * Mark the command as failed.
108
- * Can only be called once per command.
109
- *
110
- * @param opts - Optional error description, speech phrase, and/or audio file path
111
- */
112
- fail(opts?: { error?: string; speech?: string; audioFile?: string }): boolean {
113
- if (this._finished) return false
114
- this._finished = true
115
- return this._send({
116
- id: this.id,
117
- status: 'finished',
118
- success: false,
119
- ...(opts?.error ? { error: opts.error } : {}),
120
- ...(opts?.speech ? { speech: opts.speech } : {}),
121
- ...(opts?.audioFile ? { audioFile: opts.audioFile } : {}),
122
- timestamp: new Date().toISOString(),
123
- })
124
- }
125
- }
126
-
127
- // --- Schemas ---
128
-
129
- export const LauncherAppCommandListenerOptionsSchema = FeatureOptionsSchema.extend({
130
- socketPath: z.string().default(DEFAULT_SOCKET_PATH)
131
- .describe('Path to the Unix domain socket to listen on'),
132
- autoListen: z.boolean().optional()
133
- .describe('Automatically start listening when the feature is enabled'),
134
- })
135
- export type LauncherAppCommandListenerOptions = z.infer<typeof LauncherAppCommandListenerOptionsSchema>
136
-
137
- export const LauncherAppCommandListenerStateSchema = FeatureStateSchema.extend({
138
- listening: z.boolean().default(false)
139
- .describe('Whether the IPC server is listening'),
140
- clientConnected: z.boolean().default(false)
141
- .describe('Whether the native launcher app is connected'),
142
- socketPath: z.string().optional()
143
- .describe('The socket path in use'),
144
- commandsReceived: z.number().default(0)
145
- .describe('Total number of commands received'),
146
- lastCommandText: z.string().optional()
147
- .describe('The text of the last received command'),
148
- lastError: z.string().optional()
149
- .describe('Last error message'),
150
- })
151
- export type LauncherAppCommandListenerState = z.infer<typeof LauncherAppCommandListenerStateSchema>
152
-
153
- export const LauncherAppCommandListenerEventsSchema = FeatureEventsSchema.extend({
154
- listening: z.tuple([]).describe('Emitted when the IPC server starts listening'),
155
- clientConnected: z.tuple([z.any().describe('The client socket')]).describe('Emitted when the native app connects'),
156
- clientDisconnected: z.tuple([]).describe('Emitted when the native app disconnects'),
157
- command: z.tuple([z.any().describe('A CommandHandle for the incoming command')]).describe('Emitted when a command is received. The listener is responsible for calling ack(), finish(), or fail() on the handle.'),
158
- message: z.tuple([z.any().describe('The parsed message')]).describe('Emitted for any non-command message from the app'),
159
- })
160
-
161
- // --- Private types ---
162
-
163
- interface ClientConnection {
164
- socket: Socket
165
- buffer: string
166
- }
167
-
168
- // --- Feature ---
169
-
170
- /**
171
- * LauncherAppCommandListener — IPC transport for commands from the LucaVoiceLauncher app
172
- *
173
- * Listens on a Unix domain socket for the native macOS launcher app to connect.
174
- * When a command event arrives (voice, hotkey, text input), it wraps it in a
175
- * `CommandHandle` and emits a `command` event. The consumer is responsible for
176
- * acknowledging, processing, and finishing the command via the handle.
177
- *
178
- * Uses NDJSON (newline-delimited JSON) over the socket per the CLIENT_SPEC protocol.
179
- *
180
- * @example
181
- * ```typescript
182
- * const listener = container.feature('launcherAppCommandListener', {
183
- * enable: true,
184
- * autoListen: true,
185
- * })
186
- *
187
- * listener.on('command', async (cmd) => {
188
- * cmd.ack('Working on it!') // or just cmd.ack() for silent
189
- *
190
- * // ... do your actual work ...
191
- * cmd.progress(0.5, 'Halfway there')
192
- *
193
- * cmd.finish() // silent finish
194
- * cmd.finish({ result: { action: 'completed' }, speech: 'All done!' })
195
- * // or: cmd.fail({ error: 'not found', speech: 'Sorry, that failed.' })
196
- * })
197
- * ```
198
- */
199
- export class LauncherAppCommandListener extends Feature<LauncherAppCommandListenerState, LauncherAppCommandListenerOptions> {
200
- static override shortcut = 'features.launcherAppCommandListener' as const
201
- static override stateSchema = LauncherAppCommandListenerStateSchema
202
- static override optionsSchema = LauncherAppCommandListenerOptionsSchema
203
- static override eventsSchema = LauncherAppCommandListenerEventsSchema
204
- static { Feature.register(this, 'launcherAppCommandListener') }
205
-
206
- private _server?: NetServer
207
- private _client?: ClientConnection
208
-
209
- override get initialState(): LauncherAppCommandListenerState {
210
- return {
211
- ...super.initialState,
212
- listening: false,
213
- clientConnected: false,
214
- commandsReceived: 0,
215
- }
216
- }
217
-
218
- /** Whether the IPC server is currently listening. */
219
- get isListening(): boolean {
220
- return this.state.get('listening') || false
221
- }
222
-
223
- /** Whether the native app client is currently connected. */
224
- get isClientConnected(): boolean {
225
- return this.state.get('clientConnected') || false
226
- }
227
-
228
- override async enable(options: any = {}): Promise<this> {
229
- await super.enable(options)
230
-
231
- if (this.options.autoListen) {
232
- this.listen()
233
- }
234
-
235
- return this
236
- }
237
-
238
- /**
239
- * Start listening on the Unix domain socket for the native app to connect.
240
- * Fire-and-forget — binds the socket and returns immediately. Sits quietly
241
- * until the native app connects; does nothing visible if it never does.
242
- *
243
- * @param socketPath - Override the configured socket path
244
- * @returns This feature instance for chaining
245
- */
246
- listen(socketPath?: string): this {
247
- if (this._server) return this
248
-
249
- socketPath = socketPath || this.options.socketPath || DEFAULT_SOCKET_PATH
250
-
251
- const dir = dirname(socketPath)
252
- if (!existsSync(dir)) {
253
- try {
254
- mkdirSync(dir, { recursive: true })
255
- } catch (error: any) {
256
- this.setState({ lastError: `Failed to create socket directory ${dir}: ${error?.message || String(error)}` })
257
- return this
258
- }
259
- }
260
-
261
- if (existsSync(socketPath)) {
262
- try {
263
- unlinkSync(socketPath)
264
- } catch (error: any) {
265
- this.setState({ lastError: `Failed to remove stale socket at ${socketPath}: ${error?.message || String(error)}` })
266
- return this
267
- }
268
- }
269
-
270
- const server = new NetServer((socket) => {
271
- this.handleClientConnect(socket)
272
- })
273
-
274
- server.on('error', (err) => {
275
- this.setState({ lastError: err.message })
276
- })
277
-
278
- const finalPath = socketPath
279
- server.listen(finalPath, () => {
280
- this._server = server
281
- this.setState({ listening: true, socketPath: finalPath })
282
- this.emit('listening')
283
- })
284
-
285
- return this
286
- }
287
-
288
- /**
289
- * Stop the IPC server and clean up all connections.
290
- *
291
- * @returns This feature instance for chaining
292
- */
293
- async stop(): Promise<this> {
294
- if (this._client) {
295
- this._client.socket.destroy()
296
- this._client = undefined
297
- }
298
-
299
- const socketPath = this.state.get('socketPath')
300
-
301
- if (this._server) {
302
- await new Promise<void>((resolve) => {
303
- this._server!.close(() => resolve())
304
- })
305
- this._server = undefined
306
- }
307
-
308
- if (socketPath && existsSync(socketPath)) {
309
- try { unlinkSync(socketPath) } catch { /* ignore */ }
310
- }
311
-
312
- this.setState({ listening: false, clientConnected: false, socketPath: undefined })
313
- return this
314
- }
315
-
316
- /**
317
- * Write an NDJSON message to the connected app client.
318
- *
319
- * @param msg - The message object to send (will be JSON-serialized + newline)
320
- * @returns True if the message was written, false if no client is connected
321
- */
322
- send(msg: Record<string, any>): boolean {
323
- if (!this._client) return false
324
- this._client.socket.write(JSON.stringify(msg) + '\n')
325
- return true
326
- }
327
-
328
- // --- Private ---
329
-
330
- /** Handle a new client connection from the native app. */
331
- private handleClientConnect(socket: Socket): void {
332
- const client: ClientConnection = { socket, buffer: '' }
333
-
334
- if (this._client) {
335
- this._client.socket.destroy()
336
- }
337
- this._client = client
338
-
339
- this.setState({ clientConnected: true })
340
- this.emit('clientConnected', socket)
341
-
342
- socket.on('data', (chunk) => {
343
- client.buffer += chunk.toString()
344
- const lines = client.buffer.split('\n')
345
- client.buffer = lines.pop() || ''
346
- for (const line of lines) {
347
- if (line.trim()) this.processLine(line)
348
- }
349
- })
350
-
351
- socket.on('close', () => {
352
- if (this._client === client) {
353
- this._client = undefined
354
- this.setState({ clientConnected: false })
355
- this.emit('clientDisconnected')
356
- }
357
- })
358
-
359
- socket.on('error', (err) => {
360
- this.setState({ lastError: err.message })
361
- })
362
- }
363
-
364
- /** Process a single NDJSON line. Wraps commands in a CommandHandle; emits `message` for everything else. */
365
- private processLine(line: string): void {
366
- let msg: any
367
- try {
368
- msg = JSON.parse(line)
369
- } catch {
370
- return
371
- }
372
-
373
- if (msg.type === 'command') {
374
- const handle = new CommandHandle(msg, (m) => this.send(m))
375
-
376
- this.setState({
377
- commandsReceived: (this.state.get('commandsReceived') ?? 0) + 1,
378
- lastCommandText: handle.text,
379
- })
380
-
381
- this.emit('command', handle)
382
- return
383
- }
384
-
385
- this.emit('message', msg)
386
- }
387
- }
388
-
389
- export default LauncherAppCommandListener