@soederpop/luca 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,388 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import { Feature, features } 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
+
205
+ private _server?: NetServer
206
+ private _client?: ClientConnection
207
+
208
+ override get initialState(): LauncherAppCommandListenerState {
209
+ return {
210
+ ...super.initialState,
211
+ listening: false,
212
+ clientConnected: false,
213
+ commandsReceived: 0,
214
+ }
215
+ }
216
+
217
+ /** Whether the IPC server is currently listening. */
218
+ get isListening(): boolean {
219
+ return this.state.get('listening') || false
220
+ }
221
+
222
+ /** Whether the native app client is currently connected. */
223
+ get isClientConnected(): boolean {
224
+ return this.state.get('clientConnected') || false
225
+ }
226
+
227
+ override async enable(options: any = {}): Promise<this> {
228
+ await super.enable(options)
229
+
230
+ if (this.options.autoListen) {
231
+ this.listen()
232
+ }
233
+
234
+ return this
235
+ }
236
+
237
+ /**
238
+ * Start listening on the Unix domain socket for the native app to connect.
239
+ * Fire-and-forget — binds the socket and returns immediately. Sits quietly
240
+ * until the native app connects; does nothing visible if it never does.
241
+ *
242
+ * @param socketPath - Override the configured socket path
243
+ * @returns This feature instance for chaining
244
+ */
245
+ listen(socketPath?: string): this {
246
+ if (this._server) return this
247
+
248
+ socketPath = socketPath || this.options.socketPath || DEFAULT_SOCKET_PATH
249
+
250
+ const dir = dirname(socketPath)
251
+ if (!existsSync(dir)) {
252
+ try {
253
+ mkdirSync(dir, { recursive: true })
254
+ } catch (error: any) {
255
+ this.setState({ lastError: `Failed to create socket directory ${dir}: ${error?.message || String(error)}` })
256
+ return this
257
+ }
258
+ }
259
+
260
+ if (existsSync(socketPath)) {
261
+ try {
262
+ unlinkSync(socketPath)
263
+ } catch (error: any) {
264
+ this.setState({ lastError: `Failed to remove stale socket at ${socketPath}: ${error?.message || String(error)}` })
265
+ return this
266
+ }
267
+ }
268
+
269
+ const server = new NetServer((socket) => {
270
+ this.handleClientConnect(socket)
271
+ })
272
+
273
+ server.on('error', (err) => {
274
+ this.setState({ lastError: err.message })
275
+ })
276
+
277
+ const finalPath = socketPath
278
+ server.listen(finalPath, () => {
279
+ this._server = server
280
+ this.setState({ listening: true, socketPath: finalPath })
281
+ this.emit('listening')
282
+ })
283
+
284
+ return this
285
+ }
286
+
287
+ /**
288
+ * Stop the IPC server and clean up all connections.
289
+ *
290
+ * @returns This feature instance for chaining
291
+ */
292
+ async stop(): Promise<this> {
293
+ if (this._client) {
294
+ this._client.socket.destroy()
295
+ this._client = undefined
296
+ }
297
+
298
+ const socketPath = this.state.get('socketPath')
299
+
300
+ if (this._server) {
301
+ await new Promise<void>((resolve) => {
302
+ this._server!.close(() => resolve())
303
+ })
304
+ this._server = undefined
305
+ }
306
+
307
+ if (socketPath && existsSync(socketPath)) {
308
+ try { unlinkSync(socketPath) } catch { /* ignore */ }
309
+ }
310
+
311
+ this.setState({ listening: false, clientConnected: false, socketPath: undefined })
312
+ return this
313
+ }
314
+
315
+ /**
316
+ * Write an NDJSON message to the connected app client.
317
+ *
318
+ * @param msg - The message object to send (will be JSON-serialized + newline)
319
+ * @returns True if the message was written, false if no client is connected
320
+ */
321
+ send(msg: Record<string, any>): boolean {
322
+ if (!this._client) return false
323
+ this._client.socket.write(JSON.stringify(msg) + '\n')
324
+ return true
325
+ }
326
+
327
+ // --- Private ---
328
+
329
+ /** Handle a new client connection from the native app. */
330
+ private handleClientConnect(socket: Socket): void {
331
+ const client: ClientConnection = { socket, buffer: '' }
332
+
333
+ if (this._client) {
334
+ this._client.socket.destroy()
335
+ }
336
+ this._client = client
337
+
338
+ this.setState({ clientConnected: true })
339
+ this.emit('clientConnected', socket)
340
+
341
+ socket.on('data', (chunk) => {
342
+ client.buffer += chunk.toString()
343
+ const lines = client.buffer.split('\n')
344
+ client.buffer = lines.pop() || ''
345
+ for (const line of lines) {
346
+ if (line.trim()) this.processLine(line)
347
+ }
348
+ })
349
+
350
+ socket.on('close', () => {
351
+ if (this._client === client) {
352
+ this._client = undefined
353
+ this.setState({ clientConnected: false })
354
+ this.emit('clientDisconnected')
355
+ }
356
+ })
357
+
358
+ socket.on('error', (err) => {
359
+ this.setState({ lastError: err.message })
360
+ })
361
+ }
362
+
363
+ /** Process a single NDJSON line. Wraps commands in a CommandHandle; emits `message` for everything else. */
364
+ private processLine(line: string): void {
365
+ let msg: any
366
+ try {
367
+ msg = JSON.parse(line)
368
+ } catch {
369
+ return
370
+ }
371
+
372
+ if (msg.type === 'command') {
373
+ const handle = new CommandHandle(msg, (m) => this.send(m))
374
+
375
+ this.setState({
376
+ commandsReceived: (this.state.get('commandsReceived') ?? 0) + 1,
377
+ lastCommandText: handle.text,
378
+ })
379
+
380
+ this.emit('command', handle)
381
+ return
382
+ }
383
+
384
+ this.emit('message', msg)
385
+ }
386
+ }
387
+
388
+ export default features.register('launcherAppCommandListener', LauncherAppCommandListener)