@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,558 @@
1
+ import { z } from 'zod'
2
+ import { randomBytes } from 'crypto'
3
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
4
+ import { Feature, features } from '../feature.js'
5
+ import { WebSocketServer } from 'ws'
6
+
7
+ // --- Message Types ---
8
+
9
+ export const MessageTypes = {
10
+ register: 'register',
11
+ registered: 'registered',
12
+ eval: 'eval',
13
+ evalResult: 'evalResult',
14
+ event: 'event',
15
+ ping: 'ping',
16
+ pong: 'pong',
17
+ disconnect: 'disconnect',
18
+ error: 'error',
19
+ } as const
20
+
21
+ export type MessageType = typeof MessageTypes[keyof typeof MessageTypes]
22
+
23
+ // --- Link Message Envelope ---
24
+
25
+ export interface LinkMessage<T = any> {
26
+ type: MessageType
27
+ id: string
28
+ timestamp: number
29
+ token?: string
30
+ data?: T
31
+ }
32
+
33
+ // --- Registration Data ---
34
+
35
+ export interface RegisterData {
36
+ uuid: string
37
+ url?: string
38
+ capabilities?: string[]
39
+ meta?: Record<string, any>
40
+ }
41
+
42
+ export interface RegisteredData {
43
+ token: string
44
+ hostId: string
45
+ }
46
+
47
+ // --- Eval Data ---
48
+
49
+ export interface EvalData {
50
+ code: string
51
+ context?: Record<string, any>
52
+ requestId: string
53
+ timeout?: number
54
+ }
55
+
56
+ export interface EvalResultData {
57
+ requestId: string
58
+ result?: any
59
+ error?: string
60
+ }
61
+
62
+ // --- Event Data ---
63
+
64
+ export interface EventData {
65
+ eventName: string
66
+ data?: any
67
+ }
68
+
69
+ // --- Connected Container Metadata ---
70
+
71
+ export interface ConnectedContainer {
72
+ uuid: string
73
+ url?: string
74
+ capabilities?: string[]
75
+ meta?: Record<string, any>
76
+ ws: any
77
+ token: string
78
+ missedHeartbeats: number
79
+ }
80
+
81
+ // --- Schemas ---
82
+
83
+ export const ContainerLinkStateSchema = FeatureStateSchema.extend({
84
+ connectionCount: z.number().default(0).describe('Number of currently connected web containers'),
85
+ port: z.number().optional().describe('Port the WebSocket server is listening on'),
86
+ listening: z.boolean().default(false).describe('Whether the WebSocket server is listening'),
87
+ })
88
+ export type ContainerLinkState = z.infer<typeof ContainerLinkStateSchema>
89
+
90
+ export const ContainerLinkOptionsSchema = FeatureOptionsSchema.extend({
91
+ port: z.number().optional().default(8089).describe('Port for the WebSocket server'),
92
+ heartbeatInterval: z.number().optional().default(30000).describe('Interval in ms between heartbeat pings'),
93
+ maxMissedHeartbeats: z.number().optional().default(3).describe('Max missed pongs before disconnecting a client'),
94
+ })
95
+ export type ContainerLinkOptions = z.infer<typeof ContainerLinkOptionsSchema>
96
+
97
+ export const ContainerLinkEventsSchema = FeatureEventsSchema.extend({
98
+ connection: z.tuple([z.string().describe('Container UUID'), z.any().describe('Connection metadata')]).describe('Emitted when a web container connects and registers'),
99
+ disconnection: z.tuple([z.string().describe('Container UUID'), z.string().optional().describe('Reason')]).describe('Emitted when a web container disconnects'),
100
+ event: z.tuple([z.string().describe('Container UUID'), z.string().describe('Event name'), z.any().describe('Event data')]).describe('Emitted when a web container sends a structured event'),
101
+ evalResult: z.tuple([z.string().describe('Request ID'), z.any().describe('Result or error')]).describe('Emitted when an eval result is received'),
102
+ })
103
+
104
+ // --- Pending Eval ---
105
+
106
+ type PendingEval = {
107
+ resolve: (value: any) => void
108
+ reject: (reason: any) => void
109
+ timer: ReturnType<typeof setTimeout>
110
+ }
111
+
112
+ // --- Feature ---
113
+
114
+ /**
115
+ * ContainerLink (Node-side) — WebSocket host for remote web containers.
116
+ *
117
+ * Creates a WebSocket server that web containers connect to. The host can evaluate
118
+ * code in connected web containers and receive structured events back.
119
+ * Trust is strictly one-way: the node side can eval in web containers,
120
+ * but web containers can NEVER eval in the node container.
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const link = container.feature('containerLink', { enable: true, port: 8089 })
125
+ * await link.start()
126
+ *
127
+ * // When a web container connects:
128
+ * link.on('connection', (uuid, meta) => {
129
+ * console.log('Connected:', uuid)
130
+ * })
131
+ *
132
+ * // Eval code in a specific web container
133
+ * const result = await link.eval(uuid, 'document.title')
134
+ *
135
+ * // Broadcast eval to all connected containers
136
+ * const results = await link.broadcast('navigator.userAgent')
137
+ *
138
+ * // Listen for events from web containers
139
+ * link.on('event', (uuid, eventName, data) => {
140
+ * console.log(`Event from ${uuid}: ${eventName}`, data)
141
+ * })
142
+ * ```
143
+ */
144
+ export class ContainerLink extends Feature<ContainerLinkState, ContainerLinkOptions> {
145
+ static override shortcut = 'features.containerLink' as const
146
+ static override stateSchema = ContainerLinkStateSchema
147
+ static override optionsSchema = ContainerLinkOptionsSchema
148
+ static override eventsSchema = ContainerLinkEventsSchema
149
+
150
+ private _wss?: WebSocketServer
151
+ private _connections = new Map<string, ConnectedContainer>()
152
+ private _pendingEvals = new Map<string, PendingEval>()
153
+ private _heartbeatTimer?: ReturnType<typeof setInterval>
154
+
155
+ override get initialState(): ContainerLinkState {
156
+ return {
157
+ ...super.initialState,
158
+ connectionCount: 0,
159
+ listening: false,
160
+ }
161
+ }
162
+
163
+ /** Whether the WebSocket server is currently listening. */
164
+ get isListening(): boolean {
165
+ return this.state.get('listening') || false
166
+ }
167
+
168
+ /** Number of currently connected web containers. */
169
+ get connectionCount(): number {
170
+ return this._connections.size
171
+ }
172
+
173
+ /**
174
+ * Start the WebSocket server and begin accepting connections.
175
+ *
176
+ * @returns This feature instance for chaining
177
+ */
178
+ async start(): Promise<this> {
179
+ if (this._wss) return this
180
+
181
+ const port = this.options.port || 8089
182
+
183
+ return new Promise((resolve) => {
184
+ this._wss = new WebSocketServer({ port }, () => {
185
+ this.setState({ listening: true, port })
186
+ this.startHeartbeat()
187
+ resolve(this)
188
+ })
189
+
190
+ this._wss.on('connection', (ws) => {
191
+ ws.on('message', (raw: Buffer | string) => {
192
+ this.handleMessage(ws, raw)
193
+ })
194
+
195
+ ws.on('close', () => {
196
+ const entry = this.findConnectionByWs(ws)
197
+ if (entry) {
198
+ this._connections.delete(entry.uuid)
199
+ this.setState({ connectionCount: this._connections.size })
200
+ this.emit('disconnection', entry.uuid, 'closed')
201
+ }
202
+ })
203
+
204
+ ws.on('error', () => {
205
+ const entry = this.findConnectionByWs(ws)
206
+ if (entry) {
207
+ this._connections.delete(entry.uuid)
208
+ this.setState({ connectionCount: this._connections.size })
209
+ this.emit('disconnection', entry.uuid, 'error')
210
+ }
211
+ })
212
+ })
213
+
214
+ this._wss.on('error', (err) => {
215
+ this.emit('error', err)
216
+ })
217
+ })
218
+ }
219
+
220
+ /**
221
+ * Stop the WebSocket server and disconnect all clients.
222
+ *
223
+ * @returns This feature instance for chaining
224
+ */
225
+ async stop(): Promise<this> {
226
+ this.stopHeartbeat()
227
+
228
+ // Reject all pending evals
229
+ for (const [, pending] of this._pendingEvals) {
230
+ clearTimeout(pending.timer)
231
+ pending.reject(new Error('ContainerLink stopped'))
232
+ }
233
+ this._pendingEvals.clear()
234
+
235
+ // Disconnect all clients forcefully
236
+ for (const [, conn] of this._connections) {
237
+ try {
238
+ conn.ws.terminate?.()
239
+ conn.ws.close?.()
240
+ } catch { /* ignore */ }
241
+ }
242
+ this._connections.clear()
243
+
244
+ // Close server with timeout to avoid hanging
245
+ if (this._wss) {
246
+ await Promise.race([
247
+ new Promise<void>((resolve) => {
248
+ this._wss!.close(() => resolve())
249
+ }),
250
+ new Promise<void>((resolve) => setTimeout(resolve, 500)),
251
+ ])
252
+ this._wss = undefined
253
+ }
254
+
255
+ this.setState({ listening: false, connectionCount: 0, port: undefined })
256
+ return this
257
+ }
258
+
259
+ /**
260
+ * Evaluate code in a specific connected web container.
261
+ *
262
+ * @param containerId - UUID of the target web container
263
+ * @param code - JavaScript code to evaluate
264
+ * @param context - Optional context variables to inject
265
+ * @param timeout - Timeout in ms (default 10000)
266
+ * @returns The eval result
267
+ */
268
+ async eval<T = any>(containerId: string, code: string, context?: Record<string, any>, timeout = 10000): Promise<T> {
269
+ const conn = this._connections.get(containerId)
270
+ if (!conn) {
271
+ throw new Error(`No connection found for container: ${containerId}`)
272
+ }
273
+
274
+ const requestId = this.createMessage(MessageTypes.eval).id
275
+
276
+ return new Promise<T>((resolve, reject) => {
277
+ const timer = setTimeout(() => {
278
+ this._pendingEvals.delete(requestId)
279
+ reject(new Error(`Eval timed out after ${timeout}ms`))
280
+ }, timeout)
281
+
282
+ this._pendingEvals.set(requestId, { resolve, reject, timer })
283
+
284
+ const msg = this.createMessage<EvalData>(MessageTypes.eval, {
285
+ code,
286
+ context,
287
+ requestId,
288
+ timeout,
289
+ }, conn.token)
290
+
291
+ conn.ws.send(JSON.stringify(msg))
292
+ })
293
+ }
294
+
295
+ /**
296
+ * Evaluate code in all connected web containers.
297
+ *
298
+ * @param code - JavaScript code to evaluate
299
+ * @param context - Optional context variables to inject
300
+ * @param timeout - Timeout in ms (default 10000)
301
+ * @returns Map of containerId → result or Error
302
+ */
303
+ async broadcast<T = any>(code: string, context?: Record<string, any>, timeout = 10000): Promise<Map<string, T | Error>> {
304
+ const results = new Map<string, T | Error>()
305
+ const promises: Promise<void>[] = []
306
+
307
+ for (const [uuid] of this._connections) {
308
+ promises.push(
309
+ this.eval<T>(uuid, code, context, timeout)
310
+ .then((result) => { results.set(uuid, result) })
311
+ .catch((err) => { results.set(uuid, err instanceof Error ? err : new Error(String(err))) })
312
+ )
313
+ }
314
+
315
+ await Promise.all(promises)
316
+ return results
317
+ }
318
+
319
+ /**
320
+ * Get metadata of all connected containers.
321
+ *
322
+ * @returns Array of connection metadata (without ws reference)
323
+ */
324
+ getConnections(): Array<Omit<ConnectedContainer, 'ws' | 'token'>> {
325
+ return Array.from(this._connections.values()).map(({ ws, token, ...rest }) => rest)
326
+ }
327
+
328
+ /**
329
+ * Disconnect a specific web container.
330
+ *
331
+ * @param containerId - UUID of the container to disconnect
332
+ * @param reason - Optional reason string
333
+ */
334
+ disconnect(containerId: string, reason?: string): void {
335
+ const conn = this._connections.get(containerId)
336
+ if (!conn) return
337
+
338
+ try {
339
+ const msg = this.createMessage(MessageTypes.disconnect, { reason: reason || 'disconnected by host' }, conn.token)
340
+ conn.ws.send(JSON.stringify(msg))
341
+ conn.ws.close()
342
+ } catch { /* ignore */ }
343
+
344
+ this._connections.delete(containerId)
345
+ this.setState({ connectionCount: this._connections.size })
346
+ this.emit('disconnection', containerId, reason)
347
+ }
348
+
349
+ // --- Internal ---
350
+
351
+ private handleMessage(ws: any, raw: Buffer | string): void {
352
+ let msg: LinkMessage
353
+ try {
354
+ msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString())
355
+ } catch {
356
+ return // Malformed JSON
357
+ }
358
+
359
+ switch (msg.type) {
360
+ case MessageTypes.register:
361
+ this.handleRegister(ws, msg)
362
+ break
363
+
364
+ case MessageTypes.eval:
365
+ // SECURITY: Web containers can NEVER eval in the node container
366
+ this.sendToWs(ws, this.createMessage(MessageTypes.error, {
367
+ message: 'Eval from web containers is not permitted',
368
+ requestId: (msg.data as any)?.requestId,
369
+ }))
370
+ break
371
+
372
+ case MessageTypes.evalResult:
373
+ this.handleEvalResult(msg)
374
+ break
375
+
376
+ case MessageTypes.event:
377
+ this.handleEvent(ws, msg)
378
+ break
379
+
380
+ case MessageTypes.pong:
381
+ this.handlePong(ws)
382
+ break
383
+
384
+ case MessageTypes.disconnect:
385
+ this.handleClientDisconnect(ws, msg)
386
+ break
387
+
388
+ default:
389
+ break
390
+ }
391
+ }
392
+
393
+ private handleRegister(ws: any, msg: LinkMessage<RegisterData>): void {
394
+ const data = msg.data
395
+ if (!data?.uuid) {
396
+ this.sendToWs(ws, this.createMessage(MessageTypes.error, { message: 'Registration requires uuid' }))
397
+ return
398
+ }
399
+
400
+ const token = this.generateToken()
401
+
402
+ const connection: ConnectedContainer = {
403
+ uuid: data.uuid,
404
+ url: data.url,
405
+ capabilities: data.capabilities,
406
+ meta: data.meta,
407
+ ws,
408
+ token,
409
+ missedHeartbeats: 0,
410
+ }
411
+
412
+ this._connections.set(data.uuid, connection)
413
+ this.setState({ connectionCount: this._connections.size })
414
+
415
+ // Send registered acknowledgment
416
+ this.sendToWs(ws, this.createMessage(MessageTypes.registered, {
417
+ token,
418
+ hostId: this.container.uuid,
419
+ } as any))
420
+
421
+ this.emit('connection', data.uuid, {
422
+ url: data.url,
423
+ capabilities: data.capabilities,
424
+ meta: data.meta,
425
+ })
426
+ }
427
+
428
+ private handleEvalResult(msg: LinkMessage<EvalResultData>): void {
429
+ const data = msg.data
430
+ if (!data?.requestId) return
431
+
432
+ // Validate token
433
+ const conn = this.findConnectionByToken(msg.token)
434
+ if (!conn) return
435
+
436
+ const pending = this._pendingEvals.get(data.requestId)
437
+ if (!pending) return
438
+
439
+ this._pendingEvals.delete(data.requestId)
440
+ clearTimeout(pending.timer)
441
+
442
+ if (data.error) {
443
+ pending.reject(new Error(data.error))
444
+ } else {
445
+ pending.resolve(data.result)
446
+ }
447
+
448
+ this.emit('evalResult', data.requestId, data.error ? new Error(data.error) : data.result)
449
+ }
450
+
451
+ private handleEvent(_ws: any, msg: LinkMessage<EventData>): void {
452
+ const conn = this.findConnectionByToken(msg.token)
453
+ if (!conn) return
454
+
455
+ const data = msg.data
456
+ if (!data?.eventName) return
457
+
458
+ this.emit('event', conn.uuid, data.eventName, data.data)
459
+ }
460
+
461
+ private handlePong(ws: any): void {
462
+ const entry = this.findConnectionByWs(ws)
463
+ if (entry) {
464
+ entry.missedHeartbeats = 0
465
+ }
466
+ }
467
+
468
+ private handleClientDisconnect(ws: any, msg: LinkMessage): void {
469
+ const entry = this.findConnectionByWs(ws)
470
+ if (entry) {
471
+ this._connections.delete(entry.uuid)
472
+ this.setState({ connectionCount: this._connections.size })
473
+ this.emit('disconnection', entry.uuid, msg.data?.reason || 'client disconnect')
474
+ try { ws.close() } catch { /* ignore */ }
475
+ }
476
+ }
477
+
478
+ /** Generate a cryptographically random token for connection auth. */
479
+ generateToken(): string {
480
+ return randomBytes(32).toString('hex')
481
+ }
482
+
483
+ /** Create a link protocol message with a unique ID. */
484
+ private createMessage<T = any>(type: MessageType, data?: T, token?: string): LinkMessage<T> {
485
+ return {
486
+ type,
487
+ id: this.container.utils.uuid(),
488
+ timestamp: Date.now(),
489
+ ...(token != null ? { token } : {}),
490
+ ...(data != null ? { data } : {}),
491
+ }
492
+ }
493
+
494
+ private sendToWs(ws: any, msg: LinkMessage): void {
495
+ try {
496
+ ws.send(JSON.stringify(msg))
497
+ } catch { /* ignore */ }
498
+ }
499
+
500
+ /**
501
+ * Send a message to a specific connected container by UUID.
502
+ *
503
+ * @param containerId - UUID of the target container
504
+ * @param msg - The message to send
505
+ */
506
+ sendTo(containerId: string, msg: LinkMessage): void {
507
+ const conn = this._connections.get(containerId)
508
+ if (!conn) return
509
+ this.sendToWs(conn.ws, msg)
510
+ }
511
+
512
+ private findConnectionByWs(ws: any): ConnectedContainer | undefined {
513
+ for (const conn of this._connections.values()) {
514
+ if (conn.ws === ws) return conn
515
+ }
516
+ return undefined
517
+ }
518
+
519
+ private findConnectionByToken(token?: string): ConnectedContainer | undefined {
520
+ if (!token) return undefined
521
+ for (const conn of this._connections.values()) {
522
+ if (conn.token === token) return conn
523
+ }
524
+ return undefined
525
+ }
526
+
527
+ private startHeartbeat(): void {
528
+ const interval = this.options.heartbeatInterval || 30000
529
+ const maxMissed = this.options.maxMissedHeartbeats || 3
530
+
531
+ this._heartbeatTimer = setInterval(() => {
532
+ for (const [uuid, conn] of this._connections) {
533
+ conn.missedHeartbeats++
534
+
535
+ if (conn.missedHeartbeats > maxMissed) {
536
+ this.disconnect(uuid, 'heartbeat timeout')
537
+ continue
538
+ }
539
+
540
+ this.sendToWs(conn.ws, this.createMessage(MessageTypes.ping))
541
+ }
542
+ }, interval)
543
+
544
+ // Don't keep the process alive just for heartbeats
545
+ if (this._heartbeatTimer?.unref) {
546
+ this._heartbeatTimer.unref()
547
+ }
548
+ }
549
+
550
+ private stopHeartbeat(): void {
551
+ if (this._heartbeatTimer) {
552
+ clearInterval(this._heartbeatTimer)
553
+ this._heartbeatTimer = undefined
554
+ }
555
+ }
556
+ }
557
+
558
+ export default features.register('containerLink', ContainerLink)