luca 3.0.0 → 3.1.0
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/.github/workflows/release.yaml +1 -0
- package/CLAUDE.md +10 -2
- package/README.md +130 -112
- package/assistants/codingAssistant/CORE.md +6 -1
- package/assistants/codingAssistant/hooks.ts +1 -1
- package/assistants/inkbot/hooks.ts +1 -1
- package/assistants/inkbot/tools.ts +1 -1
- package/bun.lock +264 -321
- package/commands/audit-docs.ts +2 -2
- package/commands/build-bootstrap.ts +2 -3
- package/commands/build-python-bridge.ts +2 -3
- package/commands/build-scaffolds.ts +2 -3
- package/commands/bundle-consumer-project.ts +521 -0
- package/commands/generate-api-docs.ts +2 -2
- package/commands/inkbot.ts +2 -2
- package/commands/release.ts +2 -2
- package/commands/social.ts +137 -0
- package/commands/try-all-challenges.ts +3 -3
- package/commands/try-challenge.ts +3 -3
- package/datasets/lora/agentic-loop-session-candidates.jsonl +91 -0
- package/datasets/lora/agentic-loop-session-curation-summary.json +123 -0
- package/datasets/lora/luca-session-candidates.jsonl +29 -0
- package/datasets/lora/luca-session-curation-summary.json +121 -0
- package/datasets/lora/review-batch-1.jsonl +30 -0
- package/datasets/lora/review-manifest.json +41 -0
- package/datasets/lora/review-queue.jsonl +120 -0
- package/datasets/lora/review-schema.json +134 -0
- package/datasets/lora/review-template.jsonl +2 -0
- package/datasets/lora/review-ui.html +725 -0
- package/dist/agi/container.server.d.ts +2 -2
- package/dist/agi/features/assistant.d.ts +2 -2
- package/dist/agi/features/assistants-manager.d.ts +1 -1
- package/dist/agi/features/autonomous-assistant.d.ts +1 -1
- package/dist/agi/features/browser-use.d.ts +1 -1
- package/dist/agi/features/claude-code.d.ts +1 -1
- package/dist/agi/features/conversation-history.d.ts +2 -2
- package/dist/agi/features/conversation.d.ts +1 -1
- package/dist/agi/features/docs-reader.d.ts +1 -1
- package/dist/agi/features/file-tools.d.ts +1 -1
- package/dist/agi/features/luca-coder.d.ts +1 -1
- package/dist/agi/features/openai-codex.d.ts +1 -1
- package/dist/agi/features/skills-library.d.ts +1 -1
- package/dist/clients/civitai/index.d.ts +4 -4
- package/dist/clients/client-template.d.ts +4 -4
- package/dist/clients/comfyui/index.d.ts +2 -2
- package/dist/clients/elevenlabs/index.d.ts +2 -2
- package/dist/clients/openai/index.d.ts +2 -2
- package/dist/clients/supabase/index.d.ts +3 -3
- package/dist/command.d.ts +1 -1
- package/dist/node/container.d.ts +1 -1
- package/dist/node/features/helpers.d.ts +3 -3
- package/dist/node/features/semantic-search.d.ts +1 -1
- package/dist/node/features/vm.d.ts +3 -3
- package/dist/node.d.ts +1 -1
- package/dist/scaffolds/generated.d.ts +1 -1
- package/dist/selector.d.ts +1 -1
- package/features/cipher-social.ts +493 -0
- package/index.html +217 -190
- package/luca.console.ts +1 -1
- package/package.json +7 -2
- package/public/index.html +217 -190
- package/public/slides-ai-native.html +1 -1
- package/public/slides-intro.html +2 -2
- package/scripts/curate-claude-sessions.ts +561 -0
- package/scripts/examples/ask-luca-expert.ts +1 -1
- package/scripts/examples/assistant-questions.ts +1 -1
- package/scripts/examples/excalidraw-expert.ts +1 -1
- package/scripts/examples/file-manager.ts +1 -1
- package/scripts/examples/ideas.ts +1 -1
- package/scripts/examples/interactive-chat.ts +1 -1
- package/scripts/examples/opening-a-web-browser.ts +1 -1
- package/scripts/examples/telegram-bot.ts +1 -1
- package/scripts/examples/using-assistant-with-mcp.ts +1 -1
- package/scripts/examples/using-claude-code.ts +1 -1
- package/scripts/examples/using-contentdb.ts +2 -2
- package/scripts/examples/using-conversations.ts +1 -1
- package/scripts/examples/using-disk-cache.ts +1 -1
- package/scripts/examples/using-docker-shell.ts +1 -1
- package/scripts/examples/using-elevenlabs.ts +1 -1
- package/scripts/examples/using-google-calendar.ts +1 -1
- package/scripts/examples/using-google-docs.ts +1 -1
- package/scripts/examples/using-google-drive.ts +1 -1
- package/scripts/examples/using-google-sheets.ts +1 -1
- package/scripts/examples/using-nlp.ts +1 -1
- package/scripts/examples/using-ollama.ts +1 -1
- package/scripts/examples/using-postgres.ts +1 -1
- package/scripts/examples/using-runpod.ts +1 -1
- package/scripts/examples/using-tts.ts +1 -1
- package/scripts/scaffold.ts +5 -5
- package/scripts/scratch.ts +1 -1
- package/scripts/test-assistant-hooks.ts +1 -1
- package/scripts/test-docs-reader.ts +1 -1
- package/src/agi/container.server.ts +6 -2
- package/src/agi/features/agent-memory.ts +25 -25
- package/src/agi/features/assistant.ts +34 -5
- package/src/agi/features/assistants-manager.ts +122 -6
- package/src/agi/features/autonomous-assistant.ts +1 -1
- package/src/agi/features/browser-use.ts +20 -1
- package/src/agi/features/claude-code.ts +51 -5
- package/src/agi/features/coding-tools.ts +1 -1
- package/src/agi/features/conversation-history.ts +181 -4
- package/src/agi/features/conversation.ts +186 -15
- package/src/agi/features/docs-reader.ts +2 -2
- package/src/agi/features/file-tools.ts +49 -2
- package/src/agi/features/luca-coder.ts +7 -5
- package/src/agi/features/mcp-bridge.ts +532 -0
- package/src/agi/features/openai-codex.ts +2 -2
- package/src/agi/features/skills-library.ts +131 -52
- package/src/agi/lib/token-counter.ts +80 -0
- package/src/bootstrap/generated.ts +56 -57
- package/src/browser.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/cli/cli.ts +2 -2
- package/src/clients/civitai/index.ts +5 -5
- package/src/clients/client-template.ts +4 -4
- package/src/clients/comfyui/index.ts +4 -4
- package/src/clients/elevenlabs/index.ts +4 -4
- package/src/clients/openai/index.ts +7 -7
- package/src/clients/supabase/index.ts +4 -4
- package/src/clients/voicebox/index.ts +4 -4
- package/src/command.ts +2 -1
- package/src/commands/chat.ts +1 -0
- package/src/commands/eval.ts +2 -56
- package/src/commands/introspect.ts +1 -1
- package/src/commands/prompt.ts +41 -9
- package/src/container-describer.ts +8 -1
- package/src/container.ts +13 -0
- package/src/entity.ts +2 -2
- package/src/helper.ts +1 -1
- package/src/introspection/generated.agi.ts +29596 -27654
- package/src/introspection/generated.node.ts +20284 -19247
- package/src/introspection/generated.web.ts +605 -584
- package/src/introspection/scan.ts +11 -6
- package/src/node/container.ts +9 -1
- package/src/node/features/content-db.ts +39 -2
- package/src/node/features/display-result.ts +57 -0
- package/src/node/features/helpers.ts +46 -7
- package/src/node/features/python.ts +25 -19
- package/src/node/features/repl.ts +1 -1
- package/src/node/features/secure-shell.ts +11 -17
- package/src/node/features/semantic-search.ts +2 -2
- package/src/node/features/socket-repl.ts +336 -0
- package/src/node/features/telnyx-assistant-connector.ts +1206 -0
- package/src/node/features/transpiler.ts +2 -3
- package/src/node/features/ui.ts +5 -0
- package/src/node/features/vm.ts +20 -3
- package/src/node.ts +3 -3
- package/src/python/generated.ts +0 -1
- package/src/scaffolds/generated.ts +82 -83
- package/src/selector.ts +1 -1
- package/src/servers/express.ts +1 -1
- package/src/web/features/helpers.ts +22 -0
- package/tsconfig.json +12 -12
- package/docs/CLI.md +0 -335
- package/docs/CNAME +0 -1
- package/docs/README.md +0 -60
- package/docs/TABLE-OF-CONTENTS.md +0 -183
- package/docs/apis/clients/elevenlabs.md +0 -308
- package/docs/apis/clients/graph.md +0 -107
- package/docs/apis/clients/openai.md +0 -429
- package/docs/apis/clients/rest.md +0 -161
- package/docs/apis/clients/websocket.md +0 -174
- package/docs/apis/features/agi/assistant.md +0 -625
- package/docs/apis/features/agi/assistants-manager.md +0 -282
- package/docs/apis/features/agi/auto-assistant.md +0 -279
- package/docs/apis/features/agi/browser-use.md +0 -802
- package/docs/apis/features/agi/claude-code.md +0 -884
- package/docs/apis/features/agi/conversation-history.md +0 -364
- package/docs/apis/features/agi/conversation.md +0 -548
- package/docs/apis/features/agi/docs-reader.md +0 -99
- package/docs/apis/features/agi/file-tools.md +0 -163
- package/docs/apis/features/agi/luca-coder.md +0 -407
- package/docs/apis/features/agi/openai-codex.md +0 -396
- package/docs/apis/features/agi/openapi.md +0 -138
- package/docs/apis/features/agi/semantic-search.md +0 -387
- package/docs/apis/features/agi/skills-library.md +0 -239
- package/docs/apis/features/node/container-link.md +0 -192
- package/docs/apis/features/node/content-db.md +0 -450
- package/docs/apis/features/node/disk-cache.md +0 -379
- package/docs/apis/features/node/dns.md +0 -652
- package/docs/apis/features/node/docker.md +0 -706
- package/docs/apis/features/node/downloader.md +0 -81
- package/docs/apis/features/node/esbuild.md +0 -60
- package/docs/apis/features/node/file-manager.md +0 -191
- package/docs/apis/features/node/fs.md +0 -1217
- package/docs/apis/features/node/git.md +0 -371
- package/docs/apis/features/node/google-auth.md +0 -193
- package/docs/apis/features/node/google-calendar.md +0 -202
- package/docs/apis/features/node/google-docs.md +0 -173
- package/docs/apis/features/node/google-drive.md +0 -246
- package/docs/apis/features/node/google-mail.md +0 -214
- package/docs/apis/features/node/google-sheets.md +0 -194
- package/docs/apis/features/node/grep.md +0 -292
- package/docs/apis/features/node/helpers.md +0 -164
- package/docs/apis/features/node/ink.md +0 -334
- package/docs/apis/features/node/ipc-socket.md +0 -249
- package/docs/apis/features/node/json-tree.md +0 -86
- package/docs/apis/features/node/networking.md +0 -316
- package/docs/apis/features/node/nlp.md +0 -133
- package/docs/apis/features/node/opener.md +0 -97
- package/docs/apis/features/node/os.md +0 -146
- package/docs/apis/features/node/package-finder.md +0 -392
- package/docs/apis/features/node/postgres.md +0 -234
- package/docs/apis/features/node/proc.md +0 -399
- package/docs/apis/features/node/process-manager.md +0 -305
- package/docs/apis/features/node/python.md +0 -604
- package/docs/apis/features/node/redis.md +0 -380
- package/docs/apis/features/node/repl.md +0 -88
- package/docs/apis/features/node/runpod.md +0 -674
- package/docs/apis/features/node/secure-shell.md +0 -176
- package/docs/apis/features/node/semantic-search.md +0 -408
- package/docs/apis/features/node/sqlite.md +0 -233
- package/docs/apis/features/node/telegram.md +0 -279
- package/docs/apis/features/node/transpiler.md +0 -74
- package/docs/apis/features/node/tts.md +0 -133
- package/docs/apis/features/node/ui.md +0 -701
- package/docs/apis/features/node/vault.md +0 -59
- package/docs/apis/features/node/vm.md +0 -75
- package/docs/apis/features/node/yaml-tree.md +0 -85
- package/docs/apis/features/node/yaml.md +0 -176
- package/docs/apis/features/web/asset-loader.md +0 -59
- package/docs/apis/features/web/container-link.md +0 -192
- package/docs/apis/features/web/esbuild.md +0 -54
- package/docs/apis/features/web/helpers.md +0 -164
- package/docs/apis/features/web/network.md +0 -44
- package/docs/apis/features/web/speech.md +0 -69
- package/docs/apis/features/web/vault.md +0 -59
- package/docs/apis/features/web/vm.md +0 -75
- package/docs/apis/features/web/voice.md +0 -84
- package/docs/apis/servers/express.md +0 -171
- package/docs/apis/servers/mcp.md +0 -238
- package/docs/apis/servers/websocket.md +0 -170
- package/docs/bootstrap/CLAUDE.md +0 -101
- package/docs/bootstrap/SKILL.md +0 -341
- package/docs/bootstrap/templates/about-command.ts +0 -41
- package/docs/bootstrap/templates/docs-models.ts +0 -22
- package/docs/bootstrap/templates/docs-readme.md +0 -43
- package/docs/bootstrap/templates/example-feature.ts +0 -53
- package/docs/bootstrap/templates/health-endpoint.ts +0 -15
- package/docs/bootstrap/templates/luca-cli.ts +0 -30
- package/docs/bootstrap/templates/runme.md +0 -54
- package/docs/challenges/caching-proxy.md +0 -16
- package/docs/challenges/content-db-round-trip.md +0 -14
- package/docs/challenges/custom-command.md +0 -9
- package/docs/challenges/file-watcher-pipeline.md +0 -11
- package/docs/challenges/grep-audit-report.md +0 -15
- package/docs/challenges/multi-feature-dashboard.md +0 -14
- package/docs/challenges/process-orchestrator.md +0 -17
- package/docs/challenges/rest-api-server-with-client.md +0 -12
- package/docs/challenges/script-runner-with-vm.md +0 -11
- package/docs/challenges/simple-rest-api.md +0 -15
- package/docs/challenges/websocket-serve-and-client.md +0 -11
- package/docs/challenges/yaml-config-system.md +0 -14
- package/docs/command-system-overhaul.md +0 -94
- package/docs/documentation-audit.md +0 -134
- package/docs/examples/assistant/CORE.md +0 -18
- package/docs/examples/assistant/hooks.ts +0 -3
- package/docs/examples/assistant/tools.ts +0 -10
- package/docs/examples/assistant-hooks-reference.ts +0 -171
- package/docs/examples/assistant-with-process-manager.md +0 -84
- package/docs/examples/content-db.md +0 -77
- package/docs/examples/disk-cache.md +0 -83
- package/docs/examples/docker.md +0 -101
- package/docs/examples/downloader.md +0 -70
- package/docs/examples/entity.md +0 -124
- package/docs/examples/esbuild.md +0 -80
- package/docs/examples/feature-as-tool-provider.md +0 -143
- package/docs/examples/file-manager.md +0 -82
- package/docs/examples/fs.md +0 -83
- package/docs/examples/git.md +0 -85
- package/docs/examples/google-auth.md +0 -88
- package/docs/examples/google-calendar.md +0 -94
- package/docs/examples/google-docs.md +0 -82
- package/docs/examples/google-drive.md +0 -96
- package/docs/examples/google-sheets.md +0 -95
- package/docs/examples/grep.md +0 -85
- package/docs/examples/ink-blocks.md +0 -75
- package/docs/examples/ink-renderer.md +0 -41
- package/docs/examples/ink.md +0 -103
- package/docs/examples/ipc-socket.md +0 -103
- package/docs/examples/json-tree.md +0 -91
- package/docs/examples/networking.md +0 -58
- package/docs/examples/nlp.md +0 -91
- package/docs/examples/opener.md +0 -78
- package/docs/examples/os.md +0 -72
- package/docs/examples/package-finder.md +0 -89
- package/docs/examples/postgres.md +0 -91
- package/docs/examples/proc.md +0 -81
- package/docs/examples/process-manager.md +0 -79
- package/docs/examples/python.md +0 -132
- package/docs/examples/repl.md +0 -93
- package/docs/examples/runpod.md +0 -119
- package/docs/examples/secure-shell.md +0 -92
- package/docs/examples/sqlite.md +0 -86
- package/docs/examples/structured-output-with-assistants.md +0 -144
- package/docs/examples/telegram.md +0 -77
- package/docs/examples/tts.md +0 -86
- package/docs/examples/ui.md +0 -80
- package/docs/examples/vault.md +0 -70
- package/docs/examples/vm.md +0 -86
- package/docs/examples/websocket-ask-and-reply-example.md +0 -128
- package/docs/examples/yaml-tree.md +0 -93
- package/docs/examples/yaml.md +0 -104
- package/docs/ideas/assistant-factory-pattern.md +0 -142
- package/docs/in-memory-fs.md +0 -4
- package/docs/introspection-audit.md +0 -49
- package/docs/introspection.md +0 -164
- package/docs/mcp/readme.md +0 -162
- package/docs/models.ts +0 -41
- package/docs/philosophy.md +0 -86
- package/docs/principles.md +0 -7
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +0 -34
- package/docs/prompts/check-for-undocumented-features.md +0 -27
- package/docs/prompts/mcp-test-easy-command.md +0 -27
- package/docs/scaffolds/client.md +0 -149
- package/docs/scaffolds/command.md +0 -120
- package/docs/scaffolds/endpoint.md +0 -171
- package/docs/scaffolds/feature.md +0 -158
- package/docs/scaffolds/selector.md +0 -91
- package/docs/scaffolds/server.md +0 -196
- package/docs/selectors.md +0 -115
- package/docs/sessions/custom-command/attempt-log-2.md +0 -195
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +0 -728
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +0 -555
- package/docs/sessions/grep-audit-report/attempt-log-1.md +0 -289
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +0 -679
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +0 -1
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +0 -920
- package/docs/sessions/simple-rest-api/attempt-log-1.md +0 -593
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +0 -995
- package/docs/tutorials/00-bootstrap.md +0 -166
- package/docs/tutorials/01-getting-started.md +0 -106
- package/docs/tutorials/02-container.md +0 -210
- package/docs/tutorials/03-scripts.md +0 -194
- package/docs/tutorials/04-features-overview.md +0 -196
- package/docs/tutorials/05-state-and-events.md +0 -171
- package/docs/tutorials/06-servers.md +0 -157
- package/docs/tutorials/07-endpoints.md +0 -198
- package/docs/tutorials/08-commands.md +0 -252
- package/docs/tutorials/09-clients.md +0 -162
- package/docs/tutorials/10-creating-features.md +0 -203
- package/docs/tutorials/11-contentbase.md +0 -191
- package/docs/tutorials/12-assistants.md +0 -215
- package/docs/tutorials/13-introspection.md +0 -157
- package/docs/tutorials/14-type-system.md +0 -174
- package/docs/tutorials/15-project-patterns.md +0 -222
- package/docs/tutorials/16-google-features.md +0 -534
- package/docs/tutorials/17-tui-blocks.md +0 -530
- package/docs/tutorials/18-semantic-search.md +0 -334
- package/docs/tutorials/19-python-sessions.md +0 -401
- package/docs/tutorials/20-browser-esm.md +0 -234
- package/index.ts +0 -1
- package/src/agi/endpoints/ask.ts +0 -60
- package/src/agi/endpoints/conversations/[id].ts +0 -45
- package/src/agi/endpoints/conversations.ts +0 -31
- package/src/agi/endpoints/experts.ts +0 -37
- package/test/assistant-hooks.test.ts +0 -306
- package/test/assistant.test.ts +0 -81
- package/test/bus.test.ts +0 -134
- package/test/clients-servers.test.ts +0 -217
- package/test/command.test.ts +0 -267
- package/test/container-link.test.ts +0 -274
- package/test/conversation.test.ts +0 -220
- package/test/features.test.ts +0 -160
- package/test/fork-and-research.test.ts +0 -450
- package/test/integration.test.ts +0 -787
- package/test/interceptor-chain.test.ts +0 -61
- package/test/node-container.test.ts +0 -121
- package/test/python-session.test.ts +0 -105
- package/test/rate-limit.test.ts +0 -272
- package/test/semantic-search.test.ts +0 -550
- package/test/state.test.ts +0 -121
- package/test/vm-context.test.ts +0 -146
- package/test/vm-loadmodule.test.ts +0 -213
- package/test/websocket-ask.test.ts +0 -101
- package/test-integration/assistant.test.ts +0 -138
- package/test-integration/assistants-manager.test.ts +0 -113
- package/test-integration/claude-code.test.ts +0 -98
- package/test-integration/conversation-history.test.ts +0 -205
- package/test-integration/conversation.test.ts +0 -137
- package/test-integration/elevenlabs.test.ts +0 -55
- package/test-integration/google-services.test.ts +0 -80
- package/test-integration/helpers.ts +0 -89
- package/test-integration/memory.test.ts +0 -204
- package/test-integration/openai-codex.test.ts +0 -93
- package/test-integration/runpod.test.ts +0 -58
- package/test-integration/server-endpoints.test.ts +0 -97
- package/test-integration/telegram.test.ts +0 -46
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature } from "../feature.js";
|
|
4
|
+
import vm from 'vm'
|
|
5
|
+
import { inspect } from 'util'
|
|
6
|
+
|
|
7
|
+
export const SocketReplStateSchema = FeatureStateSchema.extend({
|
|
8
|
+
started: z.boolean().optional().describe('Whether the socket REPL server is running'),
|
|
9
|
+
port: z.number().optional().describe('The port the WebSocket server is listening on'),
|
|
10
|
+
activeClients: z.number().default(0).describe('Number of connected REPL clients'),
|
|
11
|
+
})
|
|
12
|
+
export type SocketReplState = z.infer<typeof SocketReplStateSchema>
|
|
13
|
+
|
|
14
|
+
export const SocketReplOptionsSchema = FeatureOptionsSchema.extend({
|
|
15
|
+
port: z.number().optional().describe('Port for the WebSocket server (default: 8282)'),
|
|
16
|
+
prompt: z.string().optional().describe('The prompt string sent to clients (default: "> ")'),
|
|
17
|
+
historyPath: z.string().optional().describe('Path to the REPL history file for command persistence'),
|
|
18
|
+
})
|
|
19
|
+
export type SocketReplOptions = z.infer<typeof SocketReplOptionsSchema>
|
|
20
|
+
|
|
21
|
+
export const SocketReplEventsSchema = FeatureEventsSchema.extend({
|
|
22
|
+
'client:connected': z.tuple([z.string().describe('Client ID')]).describe('A REPL client connected'),
|
|
23
|
+
'client:disconnected': z.tuple([z.string().describe('Client ID')]).describe('A REPL client disconnected'),
|
|
24
|
+
'eval': z.tuple([z.string().describe('The input expression'), z.string().describe('Client ID')]).describe('An expression was evaluated'),
|
|
25
|
+
'eval:result': z.tuple([z.any().describe('The result'), z.string().describe('Client ID')]).describe('An expression produced a result'),
|
|
26
|
+
'eval:error': z.tuple([z.string().describe('Error message'), z.string().describe('Client ID')]).describe('An expression threw an error'),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Socket REPL — a WebSocket-powered interactive read-eval-print loop.
|
|
31
|
+
*
|
|
32
|
+
* Exposes a REPL session over WebSocket so remote clients (browser, other process,
|
|
33
|
+
* terminal UI) can evaluate expressions in a sandboxed VM context populated with
|
|
34
|
+
* the container and its helpers. Each connected client gets its own session tracking
|
|
35
|
+
* but shares the same VM context. Supports tab completion and async/await.
|
|
36
|
+
*
|
|
37
|
+
* Messages use JSON framing:
|
|
38
|
+
* - Client → Server: `{ type: "eval", input: "expression" }`
|
|
39
|
+
* - Client → Server: `{ type: "complete", partial: "container.fea" }`
|
|
40
|
+
* - Server → Client: `{ type: "prompt", prompt: "> " }`
|
|
41
|
+
* - Server → Client: `{ type: "result", value: "..." }`
|
|
42
|
+
* - Server → Client: `{ type: "error", message: "..." }`
|
|
43
|
+
* - Server → Client: `{ type: "completions", items: ["feature", "features"], partial: "fea" }`
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const socketRepl = container.feature('socketRepl', { enable: true })
|
|
48
|
+
* await socketRepl.start({ port: 8282, context: { myVar: 42 } })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export class SocketRepl<
|
|
52
|
+
T extends SocketReplState = SocketReplState,
|
|
53
|
+
K extends SocketReplOptions = SocketReplOptions
|
|
54
|
+
> extends Feature<T, K> {
|
|
55
|
+
static override shortcut = "features.socketRepl" as const
|
|
56
|
+
static override stateSchema = SocketReplStateSchema
|
|
57
|
+
static override optionsSchema = SocketReplOptionsSchema
|
|
58
|
+
static override eventsSchema = SocketReplEventsSchema
|
|
59
|
+
static { Feature.register(this, 'socketRepl') }
|
|
60
|
+
|
|
61
|
+
_vmContext?: vm.Context
|
|
62
|
+
_wsServer?: any
|
|
63
|
+
_history: string[] = []
|
|
64
|
+
_historyPath?: string
|
|
65
|
+
_clientIds = new WeakMap<any, string>()
|
|
66
|
+
|
|
67
|
+
/** The VM context object used for evaluating expressions. */
|
|
68
|
+
get vmContext() {
|
|
69
|
+
return this._vmContext
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether the REPL server is currently running. */
|
|
73
|
+
get isStarted() {
|
|
74
|
+
return !!this.state.get('started')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start the socket REPL server.
|
|
79
|
+
*
|
|
80
|
+
* Creates a VM context populated with the container and its helpers,
|
|
81
|
+
* starts a WebSocket server, and begins accepting REPL connections.
|
|
82
|
+
*
|
|
83
|
+
* @param options - Configuration for the REPL session
|
|
84
|
+
* @param options.port - Port to listen on (default: 8282)
|
|
85
|
+
* @param options.context - Additional variables to inject into the VM context
|
|
86
|
+
* @param options.historyPath - Custom path for the history file
|
|
87
|
+
* @returns The SocketRepl instance
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const socketRepl = container.feature('socketRepl', { enable: true })
|
|
92
|
+
* await socketRepl.start({
|
|
93
|
+
* port: 8282,
|
|
94
|
+
* context: { db: myDatabase },
|
|
95
|
+
* })
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
async start(options: { port?: number, context?: any, historyPath?: string } = {}) {
|
|
99
|
+
if (this.isStarted) {
|
|
100
|
+
// Merge any new context into the existing VM context
|
|
101
|
+
if (options.context) {
|
|
102
|
+
for (const [k, v] of Object.entries(options.context)) {
|
|
103
|
+
this._vmContext![k] = v
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return this
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const port = options.port || this.options.port || 8282
|
|
110
|
+
|
|
111
|
+
// Set up history file
|
|
112
|
+
const userHistoryPath = options.historyPath || this.options.historyPath
|
|
113
|
+
if (typeof userHistoryPath === 'string') {
|
|
114
|
+
this._historyPath = this.container.paths.resolve(userHistoryPath)
|
|
115
|
+
} else {
|
|
116
|
+
const cwdHash = this.container.utils.hashObject(this.container.cwd)
|
|
117
|
+
this._historyPath = this.container.paths.resolve(
|
|
118
|
+
this.container.feature('os').cacheDir,
|
|
119
|
+
`socket-repl-${cwdHash}.history`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.container.fs.ensureFolder(this.container.paths.dirname(this._historyPath))
|
|
124
|
+
|
|
125
|
+
// Load existing history
|
|
126
|
+
try {
|
|
127
|
+
const content = this.container.fs.readFile(this._historyPath, 'utf-8') as string
|
|
128
|
+
this._history = content.split(/\r?\n/).filter(Boolean)
|
|
129
|
+
} catch {}
|
|
130
|
+
|
|
131
|
+
// Build VM context
|
|
132
|
+
this._vmContext = vm.createContext({
|
|
133
|
+
...this.container.context,
|
|
134
|
+
...options.context,
|
|
135
|
+
setTimeout, setInterval, process, clearInterval, clearTimeout, Buffer, URL, URLSearchParams,
|
|
136
|
+
// @ts-ignore
|
|
137
|
+
client: (...args: any[]) => this.container.client(...args),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Start websocket server
|
|
141
|
+
const ws = this.container.server('websocket', { json: true })
|
|
142
|
+
this._wsServer = ws
|
|
143
|
+
|
|
144
|
+
ws.on('connection', (client: any) => {
|
|
145
|
+
const clientId = this.container.utils.uuid()
|
|
146
|
+
this._clientIds.set(client, clientId)
|
|
147
|
+
this.state.set('activeClients', (this.state.get('activeClients') || 0) + 1)
|
|
148
|
+
this.emit('client:connected', clientId)
|
|
149
|
+
|
|
150
|
+
// Send initial prompt and history
|
|
151
|
+
ws.send(client, {
|
|
152
|
+
type: 'prompt',
|
|
153
|
+
prompt: this.options.prompt || '> ',
|
|
154
|
+
history: this._history.slice(-100),
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
client.on('close', () => {
|
|
158
|
+
const id = this._clientIds.get(client) || 'unknown'
|
|
159
|
+
const count = this.state.get('activeClients') || 1
|
|
160
|
+
this.state.set('activeClients', Math.max(0, count - 1))
|
|
161
|
+
this.emit('client:disconnected', id)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
ws.on('message', async (data: any, client: any) => {
|
|
166
|
+
const clientId = this._clientIds.get(client) || 'unknown'
|
|
167
|
+
|
|
168
|
+
if (data.type === 'eval') {
|
|
169
|
+
await this._handleEval(data.input, client, clientId)
|
|
170
|
+
} else if (data.type === 'complete') {
|
|
171
|
+
this._handleComplete(data.partial, client)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await ws.start({ port })
|
|
176
|
+
|
|
177
|
+
this.state.set('started', true)
|
|
178
|
+
this.state.set('port', port)
|
|
179
|
+
|
|
180
|
+
return this
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Stop the socket REPL server and disconnect all clients.
|
|
185
|
+
*/
|
|
186
|
+
async stop() {
|
|
187
|
+
if (!this.isStarted || !this._wsServer) return this
|
|
188
|
+
|
|
189
|
+
await this._wsServer.stop()
|
|
190
|
+
this._wsServer = undefined
|
|
191
|
+
this.state.set('started', false)
|
|
192
|
+
this.state.set('activeClients', 0)
|
|
193
|
+
|
|
194
|
+
return this
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Evaluate an expression and send the result back to the client. */
|
|
198
|
+
private async _handleEval(input: string, ws: any, clientId: string) {
|
|
199
|
+
const trimmed = (input || '').trim()
|
|
200
|
+
if (!trimmed) {
|
|
201
|
+
this._sendPrompt(ws)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this._saveHistory(trimmed)
|
|
206
|
+
this.emit('eval', trimmed, clientId)
|
|
207
|
+
const ctx = this._vmContext!
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Wrap top-level await in an async IIFE so vm.Script can handle it
|
|
211
|
+
const code = /\bawait\b/.test(trimmed)
|
|
212
|
+
? `(async () => { return (${trimmed}); })()`
|
|
213
|
+
: trimmed
|
|
214
|
+
const script = new vm.Script(code)
|
|
215
|
+
let result = script.runInContext(ctx)
|
|
216
|
+
|
|
217
|
+
if (result && typeof result.then === 'function') {
|
|
218
|
+
result = await result
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
ctx._ = result
|
|
222
|
+
|
|
223
|
+
const display = this._formatResult(result)
|
|
224
|
+
this.emit('eval:result', result, clientId)
|
|
225
|
+
|
|
226
|
+
this._wsServer.send(ws, {
|
|
227
|
+
type: 'result',
|
|
228
|
+
value: display,
|
|
229
|
+
})
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
this.emit('eval:error', err.message, clientId)
|
|
232
|
+
this._wsServer.send(ws, {
|
|
233
|
+
type: 'error',
|
|
234
|
+
message: err.message,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this._sendPrompt(ws)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Handle tab completion requests. */
|
|
242
|
+
private _handleComplete(partial: string, ws: any) {
|
|
243
|
+
const ctx = this._vmContext!
|
|
244
|
+
if (!ctx) return
|
|
245
|
+
|
|
246
|
+
// Dot-notation completion: e.g. container.fea
|
|
247
|
+
const dotMatch = partial.match(/([a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*)\.([a-zA-Z_$][\w$]*)?$/)
|
|
248
|
+
if (dotMatch) {
|
|
249
|
+
const objPath = dotMatch[1]!
|
|
250
|
+
const fragment = dotMatch[2] || ''
|
|
251
|
+
try {
|
|
252
|
+
const obj = new vm.Script(objPath).runInContext(ctx)
|
|
253
|
+
if (obj != null && typeof obj === 'object') {
|
|
254
|
+
const all: string[] = Object.keys(obj)
|
|
255
|
+
let proto = Object.getPrototypeOf(obj)
|
|
256
|
+
while (proto && proto !== Object.prototype) {
|
|
257
|
+
all.push(...Object.getOwnPropertyNames(proto))
|
|
258
|
+
proto = Object.getPrototypeOf(proto)
|
|
259
|
+
}
|
|
260
|
+
const items = [...new Set(all)]
|
|
261
|
+
.filter(p => p.startsWith(fragment) && p !== 'constructor')
|
|
262
|
+
.sort()
|
|
263
|
+
this._wsServer.send(ws, { type: 'completions', items, partial: fragment, prefix: objPath + '.' })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Top-level identifiers
|
|
270
|
+
const idMatch = partial.match(/([a-zA-Z_$][\w$]*)$/)
|
|
271
|
+
const fragment = idMatch ? idMatch[1]! : ''
|
|
272
|
+
const items = Object.keys(ctx).filter(k => k.startsWith(fragment)).sort()
|
|
273
|
+
this._wsServer.send(ws, { type: 'completions', items, partial: fragment, prefix: '' })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Format a result value to a string suitable for sending over the wire. */
|
|
277
|
+
private _formatResult(value: any): string {
|
|
278
|
+
if (value === undefined) return 'undefined'
|
|
279
|
+
if (value === null) return 'null'
|
|
280
|
+
if (typeof value !== 'object') return String(value)
|
|
281
|
+
|
|
282
|
+
const hasCustomInspect = typeof value[Symbol.for('nodejs.util.inspect.custom')] === 'function'
|
|
283
|
+
const ctorName = value.constructor?.name
|
|
284
|
+
const BUILTIN_TYPES = new Set(['Object', 'Array', 'Map', 'Set', 'Date', 'RegExp', 'Promise', 'Error', 'Number', 'String', 'Boolean'])
|
|
285
|
+
const isClassInstance = ctorName && !BUILTIN_TYPES.has(ctorName)
|
|
286
|
+
|
|
287
|
+
if (hasCustomInspect || !isClassInstance) {
|
|
288
|
+
return inspect(value, { colors: false, depth: 4 })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Class instances: show clean data
|
|
292
|
+
const data: Record<string, any> = {}
|
|
293
|
+
for (const [k, v] of Object.entries(value)) {
|
|
294
|
+
if (k.startsWith('_') || typeof v === 'function') continue
|
|
295
|
+
data[k] = v
|
|
296
|
+
}
|
|
297
|
+
const body = inspect(data, { colors: false, depth: 3 })
|
|
298
|
+
|
|
299
|
+
const methods: string[] = []
|
|
300
|
+
const getters: string[] = []
|
|
301
|
+
for (const [k, v] of Object.entries(value)) {
|
|
302
|
+
if (k.startsWith('_')) continue
|
|
303
|
+
if (typeof v === 'function') methods.push(k)
|
|
304
|
+
}
|
|
305
|
+
let proto = Object.getPrototypeOf(value)
|
|
306
|
+
while (proto && proto !== Object.prototype) {
|
|
307
|
+
for (const k of Object.getOwnPropertyNames(proto)) {
|
|
308
|
+
if (k === 'constructor' || k.startsWith('_')) continue
|
|
309
|
+
const desc = Object.getOwnPropertyDescriptor(proto, k)
|
|
310
|
+
if (!desc) continue
|
|
311
|
+
if (desc.get && !getters.includes(k)) getters.push(k)
|
|
312
|
+
else if (typeof desc.value === 'function' && !methods.includes(k)) methods.push(k)
|
|
313
|
+
}
|
|
314
|
+
proto = Object.getPrototypeOf(proto)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const parts = [`${ctorName} ${body}`]
|
|
318
|
+
if (getters.length) parts.push(` getters: ${getters.sort().join(', ')}`)
|
|
319
|
+
if (methods.length) parts.push(` methods: ${methods.sort().map(m => m + '()').join(', ')}`)
|
|
320
|
+
return parts.join('\n')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private _sendPrompt(ws: any) {
|
|
324
|
+
this._wsServer?.send(ws, { type: 'prompt', prompt: this.options.prompt || '> ' })
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private _saveHistory(line: string) {
|
|
328
|
+
if (!this._historyPath || !line.trim()) return
|
|
329
|
+
this._history.push(line)
|
|
330
|
+
try {
|
|
331
|
+
this.container.fs.appendFile(this._historyPath, line + '\n')
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default SocketRepl
|