@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.
- package/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- 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)
|