luca 2.0.0 → 3.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/.github/workflows/release.yaml +170 -0
- package/AGENTS.md +99 -0
- package/CLAUDE.md +123 -0
- package/CNAME +1 -0
- package/README.md +275 -9
- package/RUNME.md +56 -0
- package/assistants/codingAssistant/ABOUT.md +5 -0
- package/assistants/codingAssistant/CORE.md +33 -0
- package/assistants/codingAssistant/hooks.ts +21 -0
- package/assistants/codingAssistant/tools.ts +12 -0
- package/assistants/inkbot/ABOUT.md +16 -0
- package/assistants/inkbot/CORE.md +330 -0
- package/assistants/inkbot/hooks.ts +6 -0
- package/assistants/inkbot/tools.ts +53 -0
- package/assistants/researcher/ABOUT.md +5 -0
- package/assistants/researcher/CORE.md +46 -0
- package/assistants/researcher/hooks.ts +16 -0
- package/assistants/researcher/tools.ts +237 -0
- package/bun.lock +2667 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-bootstrap.ts +117 -0
- package/commands/build-python-bridge.ts +42 -0
- package/commands/build-scaffolds.ts +175 -0
- package/commands/bundle-consumer-project.ts +521 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/inkbot.ts +874 -0
- package/commands/release.ts +80 -0
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/dist/agi/container.server.d.ts +63 -0
- package/dist/agi/container.server.d.ts.map +1 -0
- package/dist/agi/endpoints/ask.d.ts +20 -0
- package/dist/agi/endpoints/ask.d.ts.map +1 -0
- package/dist/agi/endpoints/conversations/[id].d.ts +27 -0
- package/dist/agi/endpoints/conversations/[id].d.ts.map +1 -0
- package/dist/agi/endpoints/conversations.d.ts +18 -0
- package/dist/agi/endpoints/conversations.d.ts.map +1 -0
- package/dist/agi/endpoints/experts.d.ts +8 -0
- package/dist/agi/endpoints/experts.d.ts.map +1 -0
- package/dist/agi/feature.d.ts +9 -0
- package/dist/agi/feature.d.ts.map +1 -0
- package/dist/agi/features/assistant.d.ts +509 -0
- package/dist/agi/features/assistant.d.ts.map +1 -0
- package/dist/agi/features/assistants-manager.d.ts +236 -0
- package/dist/agi/features/assistants-manager.d.ts.map +1 -0
- package/dist/agi/features/autonomous-assistant.d.ts +281 -0
- package/dist/agi/features/autonomous-assistant.d.ts.map +1 -0
- package/dist/agi/features/browser-use.d.ts +479 -0
- package/dist/agi/features/browser-use.d.ts.map +1 -0
- package/dist/agi/features/claude-code.d.ts +824 -0
- package/dist/agi/features/claude-code.d.ts.map +1 -0
- package/dist/agi/features/conversation-history.d.ts +245 -0
- package/dist/agi/features/conversation-history.d.ts.map +1 -0
- package/dist/agi/features/conversation.d.ts +464 -0
- package/dist/agi/features/conversation.d.ts.map +1 -0
- package/dist/agi/features/docs-reader.d.ts +72 -0
- package/dist/agi/features/docs-reader.d.ts.map +1 -0
- package/dist/agi/features/file-tools.d.ts +110 -0
- package/dist/agi/features/file-tools.d.ts.map +1 -0
- package/dist/agi/features/luca-coder.d.ts +323 -0
- package/dist/agi/features/luca-coder.d.ts.map +1 -0
- package/dist/agi/features/openai-codex.d.ts +381 -0
- package/dist/agi/features/openai-codex.d.ts.map +1 -0
- package/dist/agi/features/openapi.d.ts +200 -0
- package/dist/agi/features/openapi.d.ts.map +1 -0
- package/dist/agi/features/skills-library.d.ts +167 -0
- package/dist/agi/features/skills-library.d.ts.map +1 -0
- package/dist/agi/index.d.ts +5 -0
- package/dist/agi/index.d.ts.map +1 -0
- package/dist/agi/lib/interceptor-chain.d.ts +44 -0
- package/dist/agi/lib/interceptor-chain.d.ts.map +1 -0
- package/dist/agi/lib/token-counter.d.ts +13 -0
- package/dist/agi/lib/token-counter.d.ts.map +1 -0
- package/dist/bootstrap/generated.d.ts +5 -0
- package/dist/bootstrap/generated.d.ts.map +1 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/bus.d.ts +29 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/cli/build-info.d.ts +4 -0
- package/dist/cli/build-info.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +3 -12
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/clients/civitai/index.d.ts +472 -0
- package/dist/clients/civitai/index.d.ts.map +1 -0
- package/dist/clients/client-template.d.ts +30 -0
- package/dist/clients/client-template.d.ts.map +1 -0
- package/dist/clients/comfyui/index.d.ts +281 -0
- package/dist/clients/comfyui/index.d.ts.map +1 -0
- package/dist/clients/elevenlabs/index.d.ts +197 -0
- package/dist/clients/elevenlabs/index.d.ts.map +1 -0
- package/dist/clients/graph.d.ts +64 -0
- package/dist/clients/graph.d.ts.map +1 -0
- package/dist/clients/openai/index.d.ts +247 -0
- package/dist/clients/openai/index.d.ts.map +1 -0
- package/dist/clients/rest.d.ts +92 -0
- package/dist/clients/rest.d.ts.map +1 -0
- package/dist/clients/supabase/index.d.ts +176 -0
- package/dist/clients/supabase/index.d.ts.map +1 -0
- package/dist/clients/websocket.d.ts +127 -0
- package/dist/clients/websocket.d.ts.map +1 -0
- package/dist/command.d.ts +163 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/commands/bootstrap.d.ts +20 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/chat.d.ts +37 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/code.d.ts +28 -0
- package/dist/commands/code.d.ts.map +1 -0
- package/dist/commands/console.d.ts +22 -0
- package/dist/commands/console.d.ts.map +1 -0
- package/dist/commands/describe.d.ts +50 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/eval.d.ts +23 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/help.d.ts +25 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/introspect.d.ts +24 -0
- package/dist/commands/introspect.d.ts.map +1 -0
- package/dist/commands/mcp.d.ts +35 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/prompt.d.ts +38 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/run.d.ts +24 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/sandbox-mcp.d.ts +34 -0
- package/dist/commands/sandbox-mcp.d.ts.map +1 -0
- package/dist/commands/save-api-docs.d.ts +21 -0
- package/dist/commands/save-api-docs.d.ts.map +1 -0
- package/dist/commands/scaffold.d.ts +24 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/select.d.ts +22 -0
- package/dist/commands/select.d.ts.map +1 -0
- package/dist/commands/serve.d.ts +29 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/container-describer.d.ts +144 -0
- package/dist/container-describer.d.ts.map +1 -0
- package/dist/container.d.ts +451 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/endpoint.d.ts +113 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/feature.d.ts +47 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/graft.d.ts +29 -0
- package/dist/graft.d.ts.map +1 -0
- package/dist/hash-object.d.ts +8 -0
- package/dist/hash-object.d.ts.map +1 -0
- package/dist/helper.d.ts +209 -0
- package/dist/helper.d.ts.map +1 -0
- package/dist/introspection/generated.node.d.ts +44623 -0
- package/dist/introspection/generated.node.d.ts.map +1 -0
- package/dist/introspection/generated.web.d.ts +1412 -0
- package/dist/introspection/generated.web.d.ts.map +1 -0
- package/dist/introspection/index.d.ts +156 -0
- package/dist/introspection/index.d.ts.map +1 -0
- package/dist/introspection/scan.d.ts +147 -0
- package/dist/introspection/scan.d.ts.map +1 -0
- package/dist/node/container.d.ts +256 -0
- package/dist/node/container.d.ts.map +1 -0
- package/dist/node/feature.d.ts +9 -0
- package/dist/node/feature.d.ts.map +1 -0
- package/dist/node/features/container-link.d.ts +213 -0
- package/dist/node/features/container-link.d.ts.map +1 -0
- package/dist/node/features/content-db.d.ts +354 -0
- package/dist/node/features/content-db.d.ts.map +1 -0
- package/dist/node/features/disk-cache.d.ts +236 -0
- package/dist/node/features/disk-cache.d.ts.map +1 -0
- package/dist/node/features/dns.d.ts +511 -0
- package/dist/node/features/dns.d.ts.map +1 -0
- package/dist/node/features/docker.d.ts +485 -0
- package/dist/node/features/docker.d.ts.map +1 -0
- package/dist/node/features/downloader.d.ts +73 -0
- package/dist/node/features/downloader.d.ts.map +1 -0
- package/dist/node/features/figlet-fonts.d.ts +4 -0
- package/dist/node/features/figlet-fonts.d.ts.map +1 -0
- package/dist/node/features/file-manager.d.ts +177 -0
- package/dist/node/features/file-manager.d.ts.map +1 -0
- package/dist/node/features/fs.d.ts +635 -0
- package/dist/node/features/fs.d.ts.map +1 -0
- package/dist/node/features/git.d.ts +329 -0
- package/dist/node/features/git.d.ts.map +1 -0
- package/dist/node/features/google-auth.d.ts +200 -0
- package/dist/node/features/google-auth.d.ts.map +1 -0
- package/dist/node/features/google-calendar.d.ts +194 -0
- package/dist/node/features/google-calendar.d.ts.map +1 -0
- package/dist/node/features/google-docs.d.ts +138 -0
- package/dist/node/features/google-docs.d.ts.map +1 -0
- package/dist/node/features/google-drive.d.ts +202 -0
- package/dist/node/features/google-drive.d.ts.map +1 -0
- package/dist/node/features/google-mail.d.ts +221 -0
- package/dist/node/features/google-mail.d.ts.map +1 -0
- package/dist/node/features/google-sheets.d.ts +157 -0
- package/dist/node/features/google-sheets.d.ts.map +1 -0
- package/dist/node/features/grep.d.ts +207 -0
- package/dist/node/features/grep.d.ts.map +1 -0
- package/dist/node/features/helpers.d.ts +236 -0
- package/dist/node/features/helpers.d.ts.map +1 -0
- package/dist/node/features/ink.d.ts +332 -0
- package/dist/node/features/ink.d.ts.map +1 -0
- package/dist/node/features/ipc-socket.d.ts +298 -0
- package/dist/node/features/ipc-socket.d.ts.map +1 -0
- package/dist/node/features/json-tree.d.ts +140 -0
- package/dist/node/features/json-tree.d.ts.map +1 -0
- package/dist/node/features/networking.d.ts +373 -0
- package/dist/node/features/networking.d.ts.map +1 -0
- package/dist/node/features/nlp.d.ts +125 -0
- package/dist/node/features/nlp.d.ts.map +1 -0
- package/dist/node/features/opener.d.ts +93 -0
- package/dist/node/features/opener.d.ts.map +1 -0
- package/dist/node/features/os.d.ts +168 -0
- package/dist/node/features/os.d.ts.map +1 -0
- package/dist/node/features/package-finder.d.ts +419 -0
- package/dist/node/features/package-finder.d.ts.map +1 -0
- package/dist/node/features/postgres.d.ts +173 -0
- package/dist/node/features/postgres.d.ts.map +1 -0
- package/dist/node/features/proc.d.ts +285 -0
- package/dist/node/features/proc.d.ts.map +1 -0
- package/dist/node/features/process-manager.d.ts +427 -0
- package/dist/node/features/process-manager.d.ts.map +1 -0
- package/dist/node/features/python.d.ts +477 -0
- package/dist/node/features/python.d.ts.map +1 -0
- package/dist/node/features/redis.d.ts +247 -0
- package/dist/node/features/redis.d.ts.map +1 -0
- package/dist/node/features/repl.d.ts +84 -0
- package/dist/node/features/repl.d.ts.map +1 -0
- package/dist/node/features/runpod.d.ts +527 -0
- package/dist/node/features/runpod.d.ts.map +1 -0
- package/dist/node/features/secure-shell.d.ts +145 -0
- package/dist/node/features/secure-shell.d.ts.map +1 -0
- package/dist/node/features/semantic-search.d.ts +207 -0
- package/dist/node/features/semantic-search.d.ts.map +1 -0
- package/dist/node/features/sqlite.d.ts +180 -0
- package/dist/node/features/sqlite.d.ts.map +1 -0
- package/dist/node/features/telegram.d.ts +173 -0
- package/dist/node/features/telegram.d.ts.map +1 -0
- package/dist/node/features/transpiler.d.ts +51 -0
- package/dist/node/features/transpiler.d.ts.map +1 -0
- package/dist/node/features/tts.d.ts +108 -0
- package/dist/node/features/tts.d.ts.map +1 -0
- package/dist/node/features/ui.d.ts +562 -0
- package/dist/node/features/ui.d.ts.map +1 -0
- package/dist/node/features/vault.d.ts +90 -0
- package/dist/node/features/vault.d.ts.map +1 -0
- package/dist/node/features/vm.d.ts +285 -0
- package/dist/node/features/vm.d.ts.map +1 -0
- package/dist/node/features/yaml-tree.d.ts +118 -0
- package/dist/node/features/yaml-tree.d.ts.map +1 -0
- package/dist/node/features/yaml.d.ts +127 -0
- package/dist/node/features/yaml.d.ts.map +1 -0
- package/dist/node.d.ts +67 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/python/generated.d.ts +2 -0
- package/dist/python/generated.d.ts.map +1 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/registry.d.ts +97 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/scaffolds/generated.d.ts +13 -0
- package/dist/scaffolds/generated.d.ts.map +1 -0
- package/dist/scaffolds/template.d.ts +11 -0
- package/dist/scaffolds/template.d.ts.map +1 -0
- package/dist/schemas/base.d.ts +254 -0
- package/dist/schemas/base.d.ts.map +1 -0
- package/dist/selector.d.ts +130 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/servers/express.d.ts +104 -0
- package/dist/servers/express.d.ts.map +1 -0
- package/dist/servers/mcp.d.ts +201 -0
- package/dist/servers/mcp.d.ts.map +1 -0
- package/dist/servers/socket.d.ts +121 -0
- package/dist/servers/socket.d.ts.map +1 -0
- package/dist/state.d.ts +24 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/web/clients/socket.d.ts +37 -0
- package/dist/web/clients/socket.d.ts.map +1 -0
- package/dist/web/container.d.ts +55 -0
- package/dist/web/container.d.ts.map +1 -0
- package/dist/web/extension.d.ts +4 -0
- package/dist/web/extension.d.ts.map +1 -0
- package/dist/web/feature.d.ts +8 -0
- package/dist/web/feature.d.ts.map +1 -0
- package/dist/web/features/asset-loader.d.ts +35 -0
- package/dist/web/features/asset-loader.d.ts.map +1 -0
- package/dist/web/features/container-link.d.ts +167 -0
- package/dist/web/features/container-link.d.ts.map +1 -0
- package/dist/web/features/esbuild.d.ts +51 -0
- package/dist/web/features/esbuild.d.ts.map +1 -0
- package/dist/web/features/helpers.d.ts +140 -0
- package/dist/web/features/helpers.d.ts.map +1 -0
- package/dist/web/features/network.d.ts +69 -0
- package/dist/web/features/network.d.ts.map +1 -0
- package/dist/web/features/speech.d.ts +71 -0
- package/dist/web/features/speech.d.ts.map +1 -0
- package/dist/web/features/vault.d.ts +62 -0
- package/dist/web/features/vault.d.ts.map +1 -0
- package/dist/web/features/vm.d.ts +48 -0
- package/dist/web/features/vm.d.ts.map +1 -0
- package/dist/web/features/voice-recognition.d.ts +96 -0
- package/dist/web/features/voice-recognition.d.ts.map +1 -0
- package/dist/web/shims/isomorphic-vm.d.ts +22 -0
- package/dist/web/shims/isomorphic-vm.d.ts.map +1 -0
- package/index.html +1457 -0
- package/index.ts +1 -0
- package/install.sh +84 -0
- package/luca.cli.ts +16 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +219 -58
- package/public/index.html +1457 -0
- package/public/slides-ai-native.html +902 -0
- package/public/slides-intro.html +974 -0
- package/pyproject.toml +7 -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/using-assistant-with-mcp.ts +555 -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 +11 -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/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/stamp-build.sh +12 -0
- package/scripts/test-assistant-hooks.ts +13 -0
- package/scripts/test-docs-reader.ts +10 -0
- package/scripts/test-linux-binary.sh +80 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +156 -0
- package/src/agi/feature.ts +13 -0
- package/src/agi/features/agent-memory.ts +694 -0
- package/src/agi/features/assistant.ts +1653 -0
- package/src/agi/features/assistants-manager.ts +534 -0
- package/src/agi/features/autonomous-assistant.ts +431 -0
- package/src/agi/features/browser-use.ts +672 -0
- package/src/agi/features/claude-code.ts +1584 -0
- package/src/agi/features/coding-tools.ts +175 -0
- package/src/agi/features/conversation-history.ts +672 -0
- package/src/agi/features/conversation.ts +1494 -0
- package/src/agi/features/docs-reader.ts +167 -0
- package/src/agi/features/file-tools.ts +340 -0
- package/src/agi/features/luca-coder.ts +641 -0
- package/src/agi/features/mcp-bridge.ts +532 -0
- package/src/agi/features/openai-codex.ts +651 -0
- package/src/agi/features/openapi.ts +445 -0
- package/src/agi/features/skills-library.ts +557 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/interceptor-chain.ts +89 -0
- package/src/agi/lib/token-counter.ts +202 -0
- package/src/bootstrap/generated.ts +9791 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +122 -0
- package/src/cli/build-info.ts +4 -0
- package/src/cli/cli.ts +355 -0
- package/src/client.ts +170 -0
- package/src/clients/civitai/index.ts +537 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +604 -0
- package/src/clients/elevenlabs/index.ts +317 -0
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +456 -0
- package/src/clients/rest.ts +207 -0
- package/src/clients/supabase/index.ts +357 -0
- package/src/clients/voicebox/index.ts +300 -0
- package/src/clients/websocket.ts +251 -0
- package/src/command.ts +506 -0
- package/src/commands/bootstrap.ts +244 -0
- package/src/commands/chat.ts +309 -0
- package/src/commands/code.ts +371 -0
- package/src/commands/console.ts +189 -0
- package/src/commands/describe.ts +243 -0
- package/src/commands/eval.ts +67 -0
- package/src/commands/help.ts +240 -0
- package/src/commands/index.ts +19 -0
- package/src/commands/introspect.ts +218 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +1014 -0
- package/src/commands/run.ts +278 -0
- package/src/commands/sandbox-mcp.ts +343 -0
- package/src/commands/save-api-docs.ts +51 -0
- package/src/commands/scaffold.ts +225 -0
- package/src/commands/select.ts +99 -0
- package/src/commands/serve.ts +208 -0
- package/src/container-describer.ts +1091 -0
- package/src/container.ts +1199 -0
- package/src/endpoint.ts +365 -0
- package/src/entity.ts +173 -0
- package/src/feature.ts +118 -0
- package/src/graft.ts +181 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +849 -0
- package/src/introspection/generated.agi.ts +41200 -0
- package/src/introspection/generated.node.ts +28773 -0
- package/src/introspection/generated.web.ts +2272 -0
- package/src/introspection/index.ts +296 -0
- package/src/introspection/scan.ts +1136 -0
- package/src/node/container.ts +409 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +559 -0
- package/src/node/features/content-db.ts +849 -0
- package/src/node/features/disk-cache.ts +388 -0
- package/src/node/features/display-result.ts +57 -0
- package/src/node/features/dns.ts +669 -0
- package/src/node/features/docker.ts +921 -0
- package/src/node/features/downloader.ts +79 -0
- package/src/node/features/figlet-fonts.ts +600 -0
- package/src/node/features/file-manager.ts +535 -0
- package/src/node/features/fs.ts +1050 -0
- package/src/node/features/git.ts +592 -0
- package/src/node/features/google-auth.ts +504 -0
- package/src/node/features/google-calendar.ts +306 -0
- package/src/node/features/google-docs.ts +412 -0
- package/src/node/features/google-drive.ts +346 -0
- package/src/node/features/google-mail.ts +540 -0
- package/src/node/features/google-sheets.ts +286 -0
- package/src/node/features/grep.ts +427 -0
- package/src/node/features/helpers.ts +762 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +649 -0
- package/src/node/features/json-tree.ts +170 -0
- package/src/node/features/networking.ts +961 -0
- package/src/node/features/nlp.ts +212 -0
- package/src/node/features/opener.ts +180 -0
- package/src/node/features/os.ts +403 -0
- package/src/node/features/package-finder.ts +540 -0
- package/src/node/features/postgres.ts +289 -0
- package/src/node/features/proc.ts +503 -0
- package/src/node/features/process-manager.ts +844 -0
- package/src/node/features/python.ts +912 -0
- package/src/node/features/redis.ts +446 -0
- package/src/node/features/repl.ts +212 -0
- package/src/node/features/runpod.ts +811 -0
- package/src/node/features/secure-shell.ts +261 -0
- package/src/node/features/semantic-search.ts +935 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +343 -0
- package/src/node/features/transpiler.ts +160 -0
- package/src/node/features/tts.ts +185 -0
- package/src/node/features/ui.ts +791 -0
- package/src/node/features/vault.ts +153 -0
- package/src/node/features/vm.ts +462 -0
- package/src/node/features/yaml-tree.ts +148 -0
- package/src/node/features/yaml.ts +133 -0
- package/src/node.ts +76 -0
- package/src/python/bridge.py +220 -0
- package/src/python/generated.ts +226 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +210 -0
- package/src/scaffolds/generated.ts +1814 -0
- package/src/scaffolds/template.ts +46 -0
- package/src/schemas/base.ts +296 -0
- package/src/selector.ts +352 -0
- package/src/server.ts +229 -0
- package/src/servers/express.ts +283 -0
- package/src/servers/mcp.ts +802 -0
- package/src/servers/socket.ts +258 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +99 -0
- package/src/web/container.ts +75 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +72 -0
- package/src/web/features/container-link.ts +382 -0
- package/src/web/features/esbuild.ts +93 -0
- package/src/web/features/helpers.ts +291 -0
- package/src/web/features/network.ts +85 -0
- package/src/web/features/speech.ts +104 -0
- package/src/web/features/vault.ts +207 -0
- package/src/web/features/vm.ts +85 -0
- package/src/web/features/voice-recognition.ts +161 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
- package/LICENSE +0 -21
- package/dist/cli/cli.js +0 -48
- package/dist/cli/common.d.ts +0 -2
- package/dist/cli/common.js +0 -6
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -5
- package/dist/cli/run.d.ts +0 -1
- package/dist/cli/run.js +0 -38
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.js +0 -32
- package/dist/core/read.d.ts +0 -2
- package/dist/core/read.js +0 -29
- package/dist/core/request.d.ts +0 -1
- package/dist/core/request.js +0 -2
- package/dist/core/write.d.ts +0 -2
- package/dist/core/write.js +0 -21
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -5
- package/dist/utils/common.d.ts +0 -9
- package/dist/utils/common.js +0 -57
- package/dist/utils/consts.d.ts +0 -3
- package/dist/utils/consts.js +0 -11
- package/dist/utils/dict.d.ts +0 -1
- package/dist/utils/dict.js +0 -7
- package/dist/utils/index.d.ts +0 -5
- package/dist/utils/index.js +0 -21
- package/dist/utils/log.d.ts +0 -1
- package/dist/utils/log.js +0 -5
- package/dist/utils/types.d.ts +0 -1
- package/dist/utils/types.js +0 -2
- package/dist/utils/utils.test.d.ts +0 -1
- package/dist/utils/utils.test.js +0 -7
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
4
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
5
|
+
import { Feature } from '../feature.js'
|
|
6
|
+
|
|
7
|
+
declare module 'luca/feature' {
|
|
8
|
+
interface AvailableFeatures {
|
|
9
|
+
claudeCode: typeof ClaudeCode
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// --- Stream JSON types from the Claude CLI ---
|
|
14
|
+
|
|
15
|
+
export interface ClaudeInitEvent {
|
|
16
|
+
type: 'system'
|
|
17
|
+
subtype: 'init'
|
|
18
|
+
session_id: string
|
|
19
|
+
cwd: string
|
|
20
|
+
model: string
|
|
21
|
+
tools: string[]
|
|
22
|
+
mcp_servers: string[]
|
|
23
|
+
permissionMode: string
|
|
24
|
+
claude_code_version: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ClaudeAssistantMessage {
|
|
28
|
+
type: 'assistant'
|
|
29
|
+
message: {
|
|
30
|
+
id: string
|
|
31
|
+
model: string
|
|
32
|
+
role: 'assistant'
|
|
33
|
+
content: Array<{ type: 'text'; text: string } | { type: 'tool_use'; id: string; name: string; input: any }>
|
|
34
|
+
stop_reason: string | null
|
|
35
|
+
usage: {
|
|
36
|
+
input_tokens: number
|
|
37
|
+
output_tokens: number
|
|
38
|
+
cache_read_input_tokens?: number
|
|
39
|
+
cache_creation_input_tokens?: number
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
session_id: string
|
|
43
|
+
parent_tool_use_id: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ClaudeToolResult {
|
|
47
|
+
type: 'tool_result'
|
|
48
|
+
tool_use_id: string
|
|
49
|
+
content: string
|
|
50
|
+
session_id: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ClaudeStreamEvent {
|
|
54
|
+
type: 'stream_event'
|
|
55
|
+
event: {
|
|
56
|
+
type: 'message_start' | 'content_block_start' | 'content_block_delta' | 'content_block_stop' | 'message_delta' | 'message_stop'
|
|
57
|
+
index?: number
|
|
58
|
+
delta?: { type: string; text?: string }
|
|
59
|
+
content_block?: { type: string; text?: string }
|
|
60
|
+
message?: any
|
|
61
|
+
usage?: any
|
|
62
|
+
}
|
|
63
|
+
session_id: string
|
|
64
|
+
parent_tool_use_id: string | null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ClaudeResultEvent {
|
|
68
|
+
type: 'result'
|
|
69
|
+
subtype: 'success' | 'error'
|
|
70
|
+
is_error: boolean
|
|
71
|
+
result: string
|
|
72
|
+
session_id: string
|
|
73
|
+
duration_ms: number
|
|
74
|
+
num_turns: number
|
|
75
|
+
total_cost_usd: number
|
|
76
|
+
usage: Record<string, any>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ClaudeEvent = ClaudeInitEvent | ClaudeAssistantMessage | ClaudeToolResult | ClaudeStreamEvent | ClaudeResultEvent | { type: string; [key: string]: any }
|
|
80
|
+
|
|
81
|
+
// --- Session types ---
|
|
82
|
+
|
|
83
|
+
export interface ClaudeSession {
|
|
84
|
+
id: string
|
|
85
|
+
sessionId?: string
|
|
86
|
+
status: 'idle' | 'running' | 'completed' | 'error'
|
|
87
|
+
prompt: string
|
|
88
|
+
result?: string
|
|
89
|
+
error?: string
|
|
90
|
+
costUsd: number
|
|
91
|
+
turns: number
|
|
92
|
+
messages: ClaudeAssistantMessage[]
|
|
93
|
+
process?: any
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- MCP server config types ---
|
|
97
|
+
|
|
98
|
+
export interface McpStdioServer {
|
|
99
|
+
type: 'stdio'
|
|
100
|
+
command: string
|
|
101
|
+
args?: string[]
|
|
102
|
+
env?: Record<string, string>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface McpHttpServer {
|
|
106
|
+
type: 'http'
|
|
107
|
+
url: string
|
|
108
|
+
headers?: Record<string, string>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface McpSseServer {
|
|
112
|
+
type: 'sse'
|
|
113
|
+
url: string
|
|
114
|
+
headers?: Record<string, string>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type McpServerConfig = McpStdioServer | McpHttpServer | McpSseServer
|
|
118
|
+
|
|
119
|
+
// --- Feature state and options ---
|
|
120
|
+
|
|
121
|
+
export const ClaudeCodeStateSchema = FeatureStateSchema.extend({
|
|
122
|
+
/** Map of session IDs to ClaudeSession objects */
|
|
123
|
+
sessions: z.record(z.string(), z.any()).describe('Map of session IDs to ClaudeSession objects'),
|
|
124
|
+
/** List of currently running session IDs */
|
|
125
|
+
activeSessions: z.array(z.string()).describe('List of currently running session IDs'),
|
|
126
|
+
/** Whether the Claude CLI binary is available */
|
|
127
|
+
claudeAvailable: z.boolean().describe('Whether the Claude CLI binary is available'),
|
|
128
|
+
/** Detected Claude CLI version string */
|
|
129
|
+
claudeVersion: z.string().optional().describe('Detected Claude CLI version string'),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
export const FileLogLevelSchema = z.enum(['verbose', 'normal', 'minimal']).describe(
|
|
133
|
+
'Log verbosity: verbose=all events including stream deltas, normal=messages+tool calls+results, minimal=init+result/error only'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
export type FileLogLevel = z.infer<typeof FileLogLevelSchema>
|
|
137
|
+
|
|
138
|
+
export const ClaudeCodeOptionsSchema = FeatureOptionsSchema.extend({
|
|
139
|
+
/** Claude CLI session ID to resume by default. When set, subsequent run()/start() calls will resume this session unless overridden. */
|
|
140
|
+
session: z.string().optional().describe('Claude CLI session ID to resume by default'),
|
|
141
|
+
/** Path to the claude CLI binary. Defaults to 'claude'. */
|
|
142
|
+
claudePath: z.string().optional().describe('Path to the claude CLI binary'),
|
|
143
|
+
/** Default model to use for sessions. */
|
|
144
|
+
model: z.string().optional().describe('Default model to use for sessions'),
|
|
145
|
+
/** Default working directory for sessions. */
|
|
146
|
+
cwd: z.string().optional().describe('Default working directory for sessions'),
|
|
147
|
+
/** Default system prompt prepended to all sessions. */
|
|
148
|
+
systemPrompt: z.string().optional().describe('Default system prompt prepended to all sessions'),
|
|
149
|
+
/** Default append system prompt for all sessions. */
|
|
150
|
+
appendSystemPrompt: z.string().optional().describe('Default append system prompt for all sessions'),
|
|
151
|
+
/** Default permission mode. */
|
|
152
|
+
permissionMode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk']).optional().describe('Default permission mode for Claude CLI sessions'),
|
|
153
|
+
/** Default allowed tools. */
|
|
154
|
+
allowedTools: z.array(z.string()).optional().describe('Default allowed tools for sessions'),
|
|
155
|
+
/** Default disallowed tools. */
|
|
156
|
+
disallowedTools: z.array(z.string()).optional().describe('Default disallowed tools for sessions'),
|
|
157
|
+
/** Whether to stream partial messages (token-by-token). Defaults to false. */
|
|
158
|
+
streaming: z.boolean().optional().describe('Whether to stream partial messages token-by-token'),
|
|
159
|
+
/** MCP config file paths to pass to sessions. */
|
|
160
|
+
mcpConfig: z.array(z.string()).optional().describe('MCP config file paths to pass to sessions'),
|
|
161
|
+
/** MCP servers to inject into sessions, keyed by server name. Automatically written to a temp config file. */
|
|
162
|
+
mcpServers: z.record(z.string(), z.any()).optional().describe('MCP server configs keyed by name, injected into sessions via temp config file'),
|
|
163
|
+
/** Path to write a parseable NDJSON session log file. Each line is a JSON object with timestamp, sessionId, event type, and event data. */
|
|
164
|
+
fileLogPath: z.string().optional().describe('Path to write a parseable NDJSON session log file'),
|
|
165
|
+
/** Verbosity level for file logging. Defaults to "normal". */
|
|
166
|
+
fileLogLevel: FileLogLevelSchema.optional().describe('Verbosity level for file logging. Defaults to "normal"'),
|
|
167
|
+
/** Default effort level for Claude reasoning. */
|
|
168
|
+
effort: z.enum(['low', 'medium', 'high']).optional().describe('Default effort level for Claude reasoning'),
|
|
169
|
+
/** Maximum cost budget in USD per session. */
|
|
170
|
+
maxBudgetUsd: z.number().optional().describe('Maximum cost budget in USD per session'),
|
|
171
|
+
/** Fallback model when the primary model is unavailable. */
|
|
172
|
+
fallbackModel: z.string().optional().describe('Fallback model when the primary model is unavailable'),
|
|
173
|
+
/** Default agent to use. */
|
|
174
|
+
agent: z.string().optional().describe('Default agent to use'),
|
|
175
|
+
/** Disable session persistence across runs. */
|
|
176
|
+
noSessionPersistence: z.boolean().optional().describe('Disable session persistence across runs'),
|
|
177
|
+
/** Default tools to make available. */
|
|
178
|
+
tools: z.array(z.string()).optional().describe('Default tools to make available'),
|
|
179
|
+
/** Require strict MCP config validation. */
|
|
180
|
+
strictMcpConfig: z.boolean().optional().describe('Require strict MCP config validation'),
|
|
181
|
+
/** Path to a custom settings file. */
|
|
182
|
+
settingsFile: z.string().optional().describe('Path to a custom settings file'),
|
|
183
|
+
/** Directories containing Claude Code skills (SKILL.md files) to load into sessions. Passed as --add-dir. */
|
|
184
|
+
skillsFolders: z.array(z.string()).optional().describe('Directories containing Claude Code skills to load into sessions'),
|
|
185
|
+
/** Launch Claude Code with a Chrome browser tool. */
|
|
186
|
+
chrome: z.boolean().optional().describe('Launch Claude Code with a Chrome browser tool'),
|
|
187
|
+
/** Base URL for the Anthropic API. Injected as ANTHROPIC_BASE_URL env var. */
|
|
188
|
+
baseURL: z.string().optional().describe('Base URL for the Anthropic API, injected as ANTHROPIC_BASE_URL'),
|
|
189
|
+
/** Auth token for the Anthropic API. Injected as ANTHROPIC_AUTH_TOKEN env var. */
|
|
190
|
+
authToken: z.string().optional().describe('Auth token for the Anthropic API, injected as ANTHROPIC_AUTH_TOKEN'),
|
|
191
|
+
/** Use local models. Sets baseURL and model from LOCAL_CHAT_ENDPOINT and LOCAL_CODER_MODEL env vars. */
|
|
192
|
+
local: z.boolean().optional().describe('Use local models, sets baseURL to LOCAL_CHAT_ENDPOINT and model to LOCAL_CODER_MODEL'),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
export const ClaudeCodeEventsSchema = FeatureEventsSchema.extend({
|
|
196
|
+
'session:start': z.tuple([z.object({ sessionId: z.string(), prompt: z.string() })]).describe('Fired when a new Claude Code session is spawned'),
|
|
197
|
+
'session:init': z.tuple([z.object({ sessionId: z.string(), init: z.any() })]).describe('Fired when the CLI emits its init system event'),
|
|
198
|
+
'session:event': z.tuple([z.object({ sessionId: z.string(), event: z.any() })]).describe('Fired for every parsed JSON event from the CLI stream'),
|
|
199
|
+
'session:stream': z.tuple([z.object({ sessionId: z.string(), streamEvent: z.any() })]).describe('Fired for stream_event type events from the CLI'),
|
|
200
|
+
'session:delta': z.tuple([z.object({ sessionId: z.string(), text: z.string(), role: z.string() })]).describe('Fired for each text delta from an assistant message'),
|
|
201
|
+
'session:message': z.tuple([z.object({ sessionId: z.string(), message: z.any() })]).describe('Fired when a complete assistant message is received'),
|
|
202
|
+
'session:result': z.tuple([z.object({ sessionId: z.string(), result: z.string() })]).describe('Fired when a session completes with a final result'),
|
|
203
|
+
'session:error': z.tuple([z.object({ sessionId: z.string(), error: z.any(), exitCode: z.number().optional() })]).describe('Fired when a session encounters an error'),
|
|
204
|
+
'session:abort': z.tuple([z.object({ sessionId: z.string() })]).describe('Fired when a session is aborted by the user'),
|
|
205
|
+
'session:warning': z.tuple([z.object({ sessionId: z.string(), message: z.string() })]).describe('Fired when the log reader encounters a warning'),
|
|
206
|
+
'session:log-error': z.tuple([z.object({ sessionId: z.string(), error: z.any() })]).describe('Fired when the log reader encounters an error'),
|
|
207
|
+
'session:parse-error': z.tuple([z.object({ sessionId: z.string(), line: z.string() })]).describe('Fired when a JSON line from the CLI cannot be parsed'),
|
|
208
|
+
}).describe('ClaudeCode events')
|
|
209
|
+
|
|
210
|
+
export type ClaudeCodeState = z.infer<typeof ClaudeCodeStateSchema>
|
|
211
|
+
export type ClaudeCodeOptions = z.infer<typeof ClaudeCodeOptionsSchema>
|
|
212
|
+
|
|
213
|
+
export interface RunOptions {
|
|
214
|
+
/** Override model for this session. */
|
|
215
|
+
model?: string
|
|
216
|
+
/** Override working directory. */
|
|
217
|
+
cwd?: string
|
|
218
|
+
/** System prompt for this session. */
|
|
219
|
+
systemPrompt?: string
|
|
220
|
+
/** Append system prompt for this session. */
|
|
221
|
+
appendSystemPrompt?: string
|
|
222
|
+
/** Permission mode override. */
|
|
223
|
+
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk'
|
|
224
|
+
/** Allowed tools override. */
|
|
225
|
+
allowedTools?: string[]
|
|
226
|
+
/** Disallowed tools override. */
|
|
227
|
+
disallowedTools?: string[]
|
|
228
|
+
/** Whether to stream partial messages. */
|
|
229
|
+
streaming?: boolean
|
|
230
|
+
/** Resume a previous session by ID. */
|
|
231
|
+
resumeSessionId?: string
|
|
232
|
+
/** Continue the most recent conversation. */
|
|
233
|
+
continue?: boolean
|
|
234
|
+
/** Additional directories to allow tool access to. */
|
|
235
|
+
addDirs?: string[]
|
|
236
|
+
/** Directories containing Claude Code skills (SKILL.md files) to load into sessions. Merged with addDirs as --add-dir. */
|
|
237
|
+
skillsFolders?: string[]
|
|
238
|
+
/** MCP config file paths. */
|
|
239
|
+
mcpConfig?: string[]
|
|
240
|
+
/** MCP servers to inject, keyed by server name. */
|
|
241
|
+
mcpServers?: Record<string, McpServerConfig>
|
|
242
|
+
/** Skip all permission checks (only for sandboxed environments). */
|
|
243
|
+
dangerouslySkipPermissions?: boolean
|
|
244
|
+
/** Additional arbitrary CLI flags. */
|
|
245
|
+
extraArgs?: string[]
|
|
246
|
+
/** Path to write a parseable NDJSON session log file. Overrides feature-level fileLogPath. */
|
|
247
|
+
fileLogPath?: string
|
|
248
|
+
/** Verbosity level for file logging. Overrides feature-level fileLogLevel. */
|
|
249
|
+
fileLogLevel?: FileLogLevel
|
|
250
|
+
/** Effort level for Claude reasoning. */
|
|
251
|
+
effort?: 'low' | 'medium' | 'high'
|
|
252
|
+
/** Maximum cost budget in USD. */
|
|
253
|
+
maxBudgetUsd?: number
|
|
254
|
+
/** Fallback model when the primary is unavailable. */
|
|
255
|
+
fallbackModel?: string
|
|
256
|
+
/** JSON schema for structured output validation. */
|
|
257
|
+
jsonSchema?: string | object
|
|
258
|
+
/** Agent to use for this session. */
|
|
259
|
+
agent?: string
|
|
260
|
+
/** Resume or fork a specific Claude session by ID. */
|
|
261
|
+
sessionId?: string
|
|
262
|
+
/** Disable session persistence for this run. */
|
|
263
|
+
noSessionPersistence?: boolean
|
|
264
|
+
/** Fork from an existing session instead of resuming. */
|
|
265
|
+
forkSession?: boolean
|
|
266
|
+
/** Tools to make available. */
|
|
267
|
+
tools?: string[]
|
|
268
|
+
/** Require strict MCP config validation. */
|
|
269
|
+
strictMcpConfig?: boolean
|
|
270
|
+
/** Enable debug output. Pass a string for specific debug channels, or true for all. */
|
|
271
|
+
debug?: string | boolean
|
|
272
|
+
/** Path to write debug output to a file. */
|
|
273
|
+
debugFile?: string
|
|
274
|
+
/** Path to a custom settings file. */
|
|
275
|
+
settingsFile?: string
|
|
276
|
+
/** Launch Claude Code with a Chrome browser tool. */
|
|
277
|
+
chrome?: boolean
|
|
278
|
+
/** Base URL for the Anthropic API. Injected as ANTHROPIC_BASE_URL in the subprocess env. */
|
|
279
|
+
baseURL?: string
|
|
280
|
+
/** Auth token for the Anthropic API. Injected as ANTHROPIC_AUTH_TOKEN in the subprocess env. */
|
|
281
|
+
authToken?: string
|
|
282
|
+
/** Use local models. Sets baseURL to LOCAL_CHAT_ENDPOINT (or http://localhost:1234) and model to LOCAL_CODER_MODEL (or qwen/qwen3.6-27b). */
|
|
283
|
+
local?: boolean
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Claude Code CLI wrapper feature. Spawns and manages Claude Code sessions
|
|
288
|
+
* as subprocesses, streaming structured JSON events back through the
|
|
289
|
+
* container's event system.
|
|
290
|
+
*
|
|
291
|
+
* Sessions are long-lived: each call to `run()` spawns a `claude -p` process
|
|
292
|
+
* with `--output-format stream-json`, parses NDJSON from stdout line-by-line,
|
|
293
|
+
* and emits typed events on the feature's event bus.
|
|
294
|
+
*
|
|
295
|
+
* @extends Feature
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const cc = container.feature('claudeCode')
|
|
300
|
+
*
|
|
301
|
+
* // Listen for events
|
|
302
|
+
* cc.on('session:delta', ({ sessionId, text }) => process.stdout.write(text))
|
|
303
|
+
* cc.on('session:result', ({ sessionId, result }) => console.log('Done:', result))
|
|
304
|
+
*
|
|
305
|
+
* // Run a prompt
|
|
306
|
+
* const session = await cc.run('Explain the architecture of this project')
|
|
307
|
+
* console.log(session.result)
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
|
|
311
|
+
static override stateSchema = ClaudeCodeStateSchema
|
|
312
|
+
static override optionsSchema = ClaudeCodeOptionsSchema
|
|
313
|
+
static override eventsSchema = ClaudeCodeEventsSchema
|
|
314
|
+
static override shortcut = 'features.claudeCode' as const
|
|
315
|
+
static override envVars = ['TMPDIR']
|
|
316
|
+
|
|
317
|
+
static { Feature.register(this, 'claudeCode') }
|
|
318
|
+
|
|
319
|
+
override get initialState(): ClaudeCodeState {
|
|
320
|
+
return {
|
|
321
|
+
...super.initialState,
|
|
322
|
+
sessions: {},
|
|
323
|
+
activeSessions: [],
|
|
324
|
+
claudeAvailable: false
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolve the path to the claude CLI binary.
|
|
330
|
+
*
|
|
331
|
+
* @returns {string} The path to the claude binary
|
|
332
|
+
*/
|
|
333
|
+
private _resolvedClaudePath: string | null = null
|
|
334
|
+
|
|
335
|
+
get claudePath(): string {
|
|
336
|
+
if (this.options.claudePath) return this.options.claudePath
|
|
337
|
+
if (this._resolvedClaudePath) return this._resolvedClaudePath
|
|
338
|
+
try {
|
|
339
|
+
this._resolvedClaudePath = this.container.feature('proc').resolveRealPath('claude')
|
|
340
|
+
} catch {
|
|
341
|
+
this._resolvedClaudePath = 'claude'
|
|
342
|
+
}
|
|
343
|
+
return this._resolvedClaudePath
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parsed semver components from the detected CLI version, or undefined if not yet checked.
|
|
348
|
+
*
|
|
349
|
+
* @returns {{ major: number; minor: number; patch: number } | undefined} Parsed version
|
|
350
|
+
*/
|
|
351
|
+
get parsedVersion(): { major: number; minor: number; patch: number } | undefined {
|
|
352
|
+
const ver = this.state.current.claudeVersion
|
|
353
|
+
if (!ver) return undefined
|
|
354
|
+
const match = ver.match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
355
|
+
if (!match) return undefined
|
|
356
|
+
return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10) }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Assert that the detected CLI version meets a minimum major.minor requirement.
|
|
361
|
+
* Throws if the CLI version is below the specified minimum.
|
|
362
|
+
*
|
|
363
|
+
* @param {number} major - Minimum major version
|
|
364
|
+
* @param {number} minor - Minimum minor version
|
|
365
|
+
*/
|
|
366
|
+
assertMinVersion(major: number, minor: number): void {
|
|
367
|
+
const v = this.parsedVersion
|
|
368
|
+
if (!v) throw new Error('Claude CLI version not detected. Call checkAvailability() first.')
|
|
369
|
+
if (v.major < major || (v.major === major && v.minor < minor)) {
|
|
370
|
+
throw new Error(`Claude CLI ${this.state.current.claudeVersion} is below minimum ${major}.${minor}`)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if the Claude CLI is available and capture its version.
|
|
376
|
+
*
|
|
377
|
+
* @returns {Promise<boolean>} Whether the CLI is available
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* const available = await cc.checkAvailability()
|
|
382
|
+
* if (!available) throw new Error('Claude CLI not found')
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
async checkAvailability(): Promise<boolean> {
|
|
386
|
+
try {
|
|
387
|
+
const proc = this.container.feature('proc')
|
|
388
|
+
const result = await proc.spawnAndCapture(this.claudePath, ['--version'])
|
|
389
|
+
const stdout = result.stdout
|
|
390
|
+
const exitCode = result.exitCode
|
|
391
|
+
|
|
392
|
+
if (exitCode === 0) {
|
|
393
|
+
const version = stdout.trim()
|
|
394
|
+
this.setState({ claudeAvailable: true, claudeVersion: version })
|
|
395
|
+
|
|
396
|
+
const v = this.parsedVersion
|
|
397
|
+
if (v && (v.major < 2 || (v.major === 2 && v.minor < 1))) {
|
|
398
|
+
this.emit('session:warning', {
|
|
399
|
+
message: `Claude CLI ${version} is below minimum 2.1. Some features may not work.`
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.setState({ claudeAvailable: false })
|
|
407
|
+
return false
|
|
408
|
+
} catch {
|
|
409
|
+
this.setState({ claudeAvailable: false })
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Tracks temp MCP config files created for cleanup */
|
|
415
|
+
private mcpTempFiles: string[] = []
|
|
416
|
+
|
|
417
|
+
/** Tracks active file log paths per session */
|
|
418
|
+
private sessionLogPaths: Map<string, { path: string; level: FileLogLevel }> = new Map()
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolve the file log path for a session, checking per-session options then feature-level defaults.
|
|
422
|
+
*
|
|
423
|
+
* @param {RunOptions} options - Per-session options
|
|
424
|
+
* @returns {{ path: string; level: FileLogLevel } | undefined} Log config if logging is enabled
|
|
425
|
+
*/
|
|
426
|
+
private resolveFileLog(options: RunOptions = {}): { path: string; level: FileLogLevel } | undefined {
|
|
427
|
+
const path = options.fileLogPath ?? this.options.fileLogPath
|
|
428
|
+
if (!path) return undefined
|
|
429
|
+
const level = options.fileLogLevel ?? this.options.fileLogLevel ?? 'normal'
|
|
430
|
+
return { path, level }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Write a log entry to the session's NDJSON log file.
|
|
435
|
+
* Each line is a self-contained JSON object with timestamp, sessionId, event type, and data.
|
|
436
|
+
*
|
|
437
|
+
* @param {string} sessionId - The local session ID
|
|
438
|
+
* @param {string} type - Event type label (e.g. 'session:init', 'session:message')
|
|
439
|
+
* @param {any} data - Event payload
|
|
440
|
+
*/
|
|
441
|
+
private async writeLogEntry(sessionId: string, type: string, data: any): Promise<void> {
|
|
442
|
+
const logConfig = this.sessionLogPaths.get(sessionId)
|
|
443
|
+
if (!logConfig) return
|
|
444
|
+
|
|
445
|
+
const entry = JSON.stringify({
|
|
446
|
+
ts: new Date().toISOString(),
|
|
447
|
+
session: sessionId,
|
|
448
|
+
type,
|
|
449
|
+
data
|
|
450
|
+
}) + '\n'
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
await this.container.feature('fs').appendFileAsync(logConfig.path, entry)
|
|
454
|
+
} catch (err) {
|
|
455
|
+
this.emit('session:log-error', { sessionId, error: err })
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Determine if an event should be logged based on the configured log level.
|
|
461
|
+
*
|
|
462
|
+
* - verbose: all events (stream deltas, partial messages, everything)
|
|
463
|
+
* - normal: assistant messages, tool results, init, result, errors (no stream_event)
|
|
464
|
+
* - minimal: init and result/error only
|
|
465
|
+
*
|
|
466
|
+
* @param {string} eventType - The Claude event type
|
|
467
|
+
* @param {FileLogLevel} level - The configured log level
|
|
468
|
+
* @returns {boolean} Whether to log this event
|
|
469
|
+
*/
|
|
470
|
+
private shouldLog(eventType: string, level: FileLogLevel): boolean {
|
|
471
|
+
switch (level) {
|
|
472
|
+
case 'verbose':
|
|
473
|
+
return true
|
|
474
|
+
case 'normal':
|
|
475
|
+
return eventType !== 'stream_event'
|
|
476
|
+
case 'minimal':
|
|
477
|
+
return eventType === 'system' || eventType === 'result'
|
|
478
|
+
default:
|
|
479
|
+
return true
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Write an MCP server config map to a temp file suitable for `--mcp-config`.
|
|
485
|
+
*
|
|
486
|
+
* @param {Record<string, McpServerConfig>} servers - Server configs keyed by name
|
|
487
|
+
* @returns {Promise<string>} Path to the generated temp config file
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* const configPath = await cc.writeMcpConfig({
|
|
492
|
+
* 'my-api': { type: 'http', url: 'https://api.example.com/mcp' },
|
|
493
|
+
* 'local-tool': { type: 'stdio', command: 'bun', args: ['run', 'server.ts'] }
|
|
494
|
+
* })
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
async writeMcpConfig(servers: Record<string, McpServerConfig>): Promise<string> {
|
|
498
|
+
const config = { mcpServers: servers }
|
|
499
|
+
const tmpDir = process.env.TMPDIR || '/tmp'
|
|
500
|
+
const tmpPath = `${tmpDir}/luca-mcp-${crypto.randomUUID()}.json`
|
|
501
|
+
await this.container.feature('fs').writeFileAsync(tmpPath, JSON.stringify(config, null, 2))
|
|
502
|
+
this.mcpTempFiles.push(tmpPath)
|
|
503
|
+
return tmpPath
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Build the argument array for a claude CLI invocation.
|
|
508
|
+
*
|
|
509
|
+
* @param {string} prompt - The prompt text
|
|
510
|
+
* @param {RunOptions} options - Session options
|
|
511
|
+
* @returns {Promise<string[]>} CLI arguments
|
|
512
|
+
*/
|
|
513
|
+
private async buildArgs(prompt: string, options: RunOptions = {}): Promise<string[]> {
|
|
514
|
+
const args: string[] = ['-p', '--output-format', 'stream-json', '--verbose']
|
|
515
|
+
|
|
516
|
+
const streaming = options.streaming ?? this.options.streaming ?? false
|
|
517
|
+
if (streaming) {
|
|
518
|
+
args.push('--include-partial-messages')
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const isLocal = options.local ?? this.options.local
|
|
522
|
+
const model = options.model ?? this.options.model ?? (isLocal ? (process.env.LOCAL_CODER_MODEL || 'qwen/qwen3.6-27b') : undefined)
|
|
523
|
+
if (model) args.push('--model', model)
|
|
524
|
+
|
|
525
|
+
const systemPrompt = options.systemPrompt ?? this.options.systemPrompt
|
|
526
|
+
if (systemPrompt) args.push('--system-prompt', systemPrompt)
|
|
527
|
+
|
|
528
|
+
const appendSystemPrompt = options.appendSystemPrompt ?? this.options.appendSystemPrompt
|
|
529
|
+
if (appendSystemPrompt) args.push('--append-system-prompt', appendSystemPrompt)
|
|
530
|
+
|
|
531
|
+
const permissionMode = options.permissionMode ?? this.options.permissionMode
|
|
532
|
+
if (permissionMode) args.push('--permission-mode', permissionMode)
|
|
533
|
+
|
|
534
|
+
const allowedTools = options.allowedTools ?? this.options.allowedTools
|
|
535
|
+
if (allowedTools?.length) args.push('--allowed-tools', ...allowedTools)
|
|
536
|
+
|
|
537
|
+
const disallowedTools = options.disallowedTools ?? this.options.disallowedTools
|
|
538
|
+
if (disallowedTools?.length) args.push('--disallowed-tools', ...disallowedTools)
|
|
539
|
+
|
|
540
|
+
// Collect all --mcp-config paths
|
|
541
|
+
const configPaths: string[] = []
|
|
542
|
+
|
|
543
|
+
const mcpConfig = options.mcpConfig ?? this.options.mcpConfig
|
|
544
|
+
if (mcpConfig?.length) configPaths.push(...mcpConfig)
|
|
545
|
+
|
|
546
|
+
// Merge mcpServers from feature-level defaults and per-session overrides
|
|
547
|
+
const defaultServers = this.options.mcpServers as Record<string, McpServerConfig> | undefined
|
|
548
|
+
const sessionServers = options.mcpServers
|
|
549
|
+
const mergedServers = { ...defaultServers, ...sessionServers }
|
|
550
|
+
|
|
551
|
+
if (Object.keys(mergedServers).length > 0) {
|
|
552
|
+
const tmpPath = await this.writeMcpConfig(mergedServers)
|
|
553
|
+
configPaths.push(tmpPath)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (configPaths.length) args.push('--mcp-config', ...configPaths)
|
|
557
|
+
|
|
558
|
+
const resumeSessionId = options.resumeSessionId ?? (options.sessionId ? undefined : this.options.session)
|
|
559
|
+
if (resumeSessionId) args.push('--resume', resumeSessionId)
|
|
560
|
+
if (options.continue) args.push('--continue')
|
|
561
|
+
if (options.dangerouslySkipPermissions) args.push('--dangerously-skip-permissions')
|
|
562
|
+
|
|
563
|
+
// Merge addDirs and skillsFolders (both feature-level and per-session) into --add-dir
|
|
564
|
+
const addDirs: string[] = [
|
|
565
|
+
...(options.addDirs ?? []),
|
|
566
|
+
...(options.skillsFolders ?? []),
|
|
567
|
+
...(this.options.skillsFolders ?? []),
|
|
568
|
+
]
|
|
569
|
+
if (addDirs.length) {
|
|
570
|
+
args.push('--add-dir', ...addDirs)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// --- New v2.1 flags ---
|
|
574
|
+
const effort = options.effort ?? this.options.effort
|
|
575
|
+
if (effort) args.push('--effort', effort)
|
|
576
|
+
|
|
577
|
+
const maxBudgetUsd = options.maxBudgetUsd ?? this.options.maxBudgetUsd
|
|
578
|
+
if (maxBudgetUsd != null) args.push('--max-budget-usd', String(maxBudgetUsd))
|
|
579
|
+
|
|
580
|
+
const fallbackModel = options.fallbackModel ?? this.options.fallbackModel
|
|
581
|
+
if (fallbackModel) args.push('--fallback-model', fallbackModel)
|
|
582
|
+
|
|
583
|
+
const agent = options.agent ?? this.options.agent
|
|
584
|
+
if (agent) args.push('--agent', agent)
|
|
585
|
+
|
|
586
|
+
const noSessionPersistence = options.noSessionPersistence ?? this.options.noSessionPersistence
|
|
587
|
+
if (noSessionPersistence) args.push('--no-session-persistence')
|
|
588
|
+
|
|
589
|
+
const tools = options.tools ?? this.options.tools
|
|
590
|
+
if (tools?.length) args.push('--tools', ...tools)
|
|
591
|
+
|
|
592
|
+
const strictMcpConfig = options.strictMcpConfig ?? this.options.strictMcpConfig
|
|
593
|
+
if (strictMcpConfig) args.push('--strict-mcp-config')
|
|
594
|
+
|
|
595
|
+
const settingsFile = options.settingsFile ?? this.options.settingsFile
|
|
596
|
+
if (settingsFile) args.push('--settings', settingsFile)
|
|
597
|
+
|
|
598
|
+
// Per-session only flags
|
|
599
|
+
if (options.jsonSchema) {
|
|
600
|
+
const schemaStr = typeof options.jsonSchema === 'string' ? options.jsonSchema : JSON.stringify(options.jsonSchema)
|
|
601
|
+
args.push('--json-schema', schemaStr)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (options.sessionId) args.push('--session-id', options.sessionId)
|
|
605
|
+
if (options.forkSession) args.push('--fork-session')
|
|
606
|
+
|
|
607
|
+
if (options.debug != null) {
|
|
608
|
+
if (typeof options.debug === 'string') {
|
|
609
|
+
args.push('--debug', options.debug)
|
|
610
|
+
} else if (options.debug) {
|
|
611
|
+
args.push('--debug')
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (options.debugFile) args.push('--debug-file', options.debugFile)
|
|
616
|
+
|
|
617
|
+
const chrome = options.chrome ?? this.options.chrome
|
|
618
|
+
if (chrome) args.push('--chrome')
|
|
619
|
+
|
|
620
|
+
if (options.extraArgs?.length) {
|
|
621
|
+
args.push(...options.extraArgs)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Prompt is piped via stdin rather than passed as a positional arg,
|
|
625
|
+
// to avoid content like '---' being parsed as CLI flags.
|
|
626
|
+
return args
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Build the environment object for a claude CLI invocation.
|
|
631
|
+
* Injects ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN when baseURL/authToken are set,
|
|
632
|
+
* or when local mode is enabled.
|
|
633
|
+
*
|
|
634
|
+
* @param {RunOptions} options - Session options
|
|
635
|
+
* @returns {Record<string, string>} Environment variables
|
|
636
|
+
*/
|
|
637
|
+
private buildEnv(options: RunOptions = {}): Record<string, string> {
|
|
638
|
+
const env = { ...process.env }
|
|
639
|
+
const isLocal = options.local ?? this.options.local
|
|
640
|
+
|
|
641
|
+
if (isLocal) {
|
|
642
|
+
const baseURL = process.env.LOCAL_CHAT_ENDPOINT || 'http://localhost:1234'
|
|
643
|
+
env.ANTHROPIC_BASE_URL = baseURL
|
|
644
|
+
if (!options.authToken) {
|
|
645
|
+
env.ANTHROPIC_AUTH_TOKEN = process.env.LOCAL_CHAT_AUTH_TOKEN || 'sk-anticropic-00000000000000000000001'
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const baseURL = options.baseURL
|
|
650
|
+
if (baseURL) {
|
|
651
|
+
env.ANTHROPIC_BASE_URL = baseURL
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const authToken = options.authToken
|
|
655
|
+
if (authToken) {
|
|
656
|
+
env.ANTHROPIC_AUTH_TOKEN = authToken
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return env
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Create a unique session ID.
|
|
664
|
+
*
|
|
665
|
+
* @returns {string} A UUID-based session ID
|
|
666
|
+
*/
|
|
667
|
+
private createSessionId(): string {
|
|
668
|
+
return crypto.randomUUID()
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Update a session in state.
|
|
673
|
+
*
|
|
674
|
+
* @param {string} id - The local session ID
|
|
675
|
+
* @param {Partial<ClaudeSession>} update - Fields to merge
|
|
676
|
+
*/
|
|
677
|
+
private updateSession(id: string, update: Partial<ClaudeSession>): void {
|
|
678
|
+
const sessions = { ...this.state.current.sessions }
|
|
679
|
+
const existing = sessions[id]
|
|
680
|
+
if (existing) {
|
|
681
|
+
sessions[id] = { ...existing, ...update }
|
|
682
|
+
this.setState({ sessions })
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Process a parsed JSON event from the Claude CLI stream.
|
|
688
|
+
*
|
|
689
|
+
* @param {string} sessionId - The local session ID
|
|
690
|
+
* @param {ClaudeEvent} event - The parsed event
|
|
691
|
+
*/
|
|
692
|
+
private handleEvent(sessionId: string, event: ClaudeEvent): void {
|
|
693
|
+
this.emit('session:event', { sessionId, event })
|
|
694
|
+
|
|
695
|
+
// File logging
|
|
696
|
+
const logConfig = this.sessionLogPaths.get(sessionId)
|
|
697
|
+
if (logConfig && this.shouldLog(event.type, logConfig.level)) {
|
|
698
|
+
this.writeLogEntry(sessionId, event.type, event)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
switch (event.type) {
|
|
702
|
+
case 'system': {
|
|
703
|
+
const init = event as ClaudeInitEvent
|
|
704
|
+
this.updateSession(sessionId, { sessionId: init.session_id })
|
|
705
|
+
this.emit('session:init', { sessionId, init })
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
case 'stream_event': {
|
|
710
|
+
const streamEvent = event as ClaudeStreamEvent
|
|
711
|
+
if (streamEvent.event.type === 'content_block_delta' && streamEvent.event.delta?.text) {
|
|
712
|
+
this.emit('session:delta', {
|
|
713
|
+
sessionId,
|
|
714
|
+
text: streamEvent.event.delta.text,
|
|
715
|
+
parentToolUseId: streamEvent.parent_tool_use_id
|
|
716
|
+
})
|
|
717
|
+
}
|
|
718
|
+
this.emit('session:stream', { sessionId, streamEvent })
|
|
719
|
+
break
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
case 'assistant': {
|
|
723
|
+
const msg = event as ClaudeAssistantMessage
|
|
724
|
+
const session = this.state.current.sessions[sessionId]
|
|
725
|
+
if (session) {
|
|
726
|
+
this.updateSession(sessionId, {
|
|
727
|
+
messages: [...session.messages, msg]
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
this.emit('session:message', { sessionId, message: msg })
|
|
731
|
+
break
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
case 'result': {
|
|
735
|
+
const result = event as ClaudeResultEvent
|
|
736
|
+
this.updateSession(sessionId, {
|
|
737
|
+
status: result.is_error ? 'error' : 'completed',
|
|
738
|
+
result: result.result,
|
|
739
|
+
error: result.is_error ? result.result : undefined,
|
|
740
|
+
costUsd: result.total_cost_usd,
|
|
741
|
+
turns: result.num_turns
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
const activeSessions = this.state.current.activeSessions.filter(id => id !== sessionId)
|
|
745
|
+
this.setState({ activeSessions })
|
|
746
|
+
|
|
747
|
+
this.emit('session:result', {
|
|
748
|
+
sessionId,
|
|
749
|
+
result: result.result,
|
|
750
|
+
isError: result.is_error,
|
|
751
|
+
costUsd: result.total_cost_usd,
|
|
752
|
+
turns: result.num_turns,
|
|
753
|
+
durationMs: result.duration_ms
|
|
754
|
+
})
|
|
755
|
+
break
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Run a prompt in a new Claude Code session. Spawns a subprocess,
|
|
762
|
+
* streams NDJSON events, and resolves when the session completes.
|
|
763
|
+
*
|
|
764
|
+
* @param {string} prompt - The instruction/prompt to send
|
|
765
|
+
* @param {RunOptions} [options] - Session configuration overrides
|
|
766
|
+
* @returns {Promise<ClaudeSession>} The completed session with result
|
|
767
|
+
*
|
|
768
|
+
* @example
|
|
769
|
+
* ```typescript
|
|
770
|
+
* // Simple one-shot
|
|
771
|
+
* const session = await cc.run('What files are in this project?')
|
|
772
|
+
* console.log(session.result)
|
|
773
|
+
*
|
|
774
|
+
* // With options
|
|
775
|
+
* const session = await cc.run('Refactor the auth module', {
|
|
776
|
+
* model: 'opus',
|
|
777
|
+
* cwd: '/path/to/project',
|
|
778
|
+
* permissionMode: 'acceptEdits',
|
|
779
|
+
* streaming: true
|
|
780
|
+
* })
|
|
781
|
+
*
|
|
782
|
+
* // With injected MCP servers
|
|
783
|
+
* const session = await cc.run('Use the database tools to list tables', {
|
|
784
|
+
* mcpServers: {
|
|
785
|
+
* 'db-tools': { type: 'stdio', command: 'bun', args: ['run', 'db-mcp.ts'] },
|
|
786
|
+
* 'api': { type: 'http', url: 'https://api.example.com/mcp' }
|
|
787
|
+
* }
|
|
788
|
+
* })
|
|
789
|
+
*
|
|
790
|
+
* // Resume a previous session
|
|
791
|
+
* const session = await cc.run('Now add tests for that', {
|
|
792
|
+
* resumeSessionId: previousSession.sessionId
|
|
793
|
+
* })
|
|
794
|
+
* ```
|
|
795
|
+
*/
|
|
796
|
+
async run(prompt: string, options: RunOptions = {}): Promise<ClaudeSession> {
|
|
797
|
+
const id = this.createSessionId()
|
|
798
|
+
const args = await this.buildArgs(prompt, options)
|
|
799
|
+
const cwd = options.cwd ?? this.options.cwd ?? (this.container as any).cwd
|
|
800
|
+
|
|
801
|
+
// Set up file logging for this session
|
|
802
|
+
const fileLog = this.resolveFileLog(options)
|
|
803
|
+
if (fileLog) {
|
|
804
|
+
this.sessionLogPaths.set(id, fileLog)
|
|
805
|
+
this.writeLogEntry(id, 'session:start', { prompt, cwd, args: [this.claudePath, ...args] })
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const session: ClaudeSession = {
|
|
809
|
+
id,
|
|
810
|
+
status: 'running',
|
|
811
|
+
prompt,
|
|
812
|
+
costUsd: 0,
|
|
813
|
+
turns: 0,
|
|
814
|
+
messages: []
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Register session in state
|
|
818
|
+
const sessions = { ...this.state.current.sessions, [id]: session }
|
|
819
|
+
const activeSessions = [...this.state.current.activeSessions, id]
|
|
820
|
+
this.setState({ sessions, activeSessions })
|
|
821
|
+
|
|
822
|
+
this.emit('session:start', { sessionId: id, prompt })
|
|
823
|
+
|
|
824
|
+
const proc = this.container.feature('proc').spawn(this.claudePath, args, {
|
|
825
|
+
cwd,
|
|
826
|
+
stdout: 'pipe',
|
|
827
|
+
stderr: 'pipe',
|
|
828
|
+
stdin: Buffer.from(prompt),
|
|
829
|
+
environment: this.buildEnv(options),
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
this.updateSession(id, { process: proc })
|
|
833
|
+
await this.consumeStream(id, proc)
|
|
834
|
+
|
|
835
|
+
return this.state.current.sessions[id]!
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Run a prompt without waiting for completion. Returns the session ID
|
|
840
|
+
* immediately so you can subscribe to events.
|
|
841
|
+
*
|
|
842
|
+
* @param {string} prompt - The instruction/prompt to send
|
|
843
|
+
* @param {RunOptions} [options] - Session configuration overrides
|
|
844
|
+
* @returns {string} The session ID to track via events
|
|
845
|
+
*
|
|
846
|
+
* @example
|
|
847
|
+
* ```typescript
|
|
848
|
+
* const sessionId = cc.start('Build a REST API for users')
|
|
849
|
+
*
|
|
850
|
+
* cc.on('session:delta', ({ sessionId: sid, text }) => {
|
|
851
|
+
* if (sid === sessionId) process.stdout.write(text)
|
|
852
|
+
* })
|
|
853
|
+
*
|
|
854
|
+
* cc.on('session:result', ({ sessionId: sid, result }) => {
|
|
855
|
+
* if (sid === sessionId) console.log('\nDone:', result)
|
|
856
|
+
* })
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
async start(prompt: string, options: RunOptions = {}): Promise<string> {
|
|
860
|
+
const id = this.createSessionId()
|
|
861
|
+
const args = await this.buildArgs(prompt, options)
|
|
862
|
+
const cwd = options.cwd ?? this.options.cwd ?? (this.container as any).cwd
|
|
863
|
+
|
|
864
|
+
// Set up file logging for this session
|
|
865
|
+
const fileLog = this.resolveFileLog(options)
|
|
866
|
+
if (fileLog) {
|
|
867
|
+
this.sessionLogPaths.set(id, fileLog)
|
|
868
|
+
this.writeLogEntry(id, 'session:start', { prompt, cwd, args: [this.claudePath, ...args] })
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const session: ClaudeSession = {
|
|
872
|
+
id,
|
|
873
|
+
status: 'running',
|
|
874
|
+
prompt,
|
|
875
|
+
costUsd: 0,
|
|
876
|
+
turns: 0,
|
|
877
|
+
messages: []
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const sessions = { ...this.state.current.sessions, [id]: session }
|
|
881
|
+
const activeSessions = [...this.state.current.activeSessions, id]
|
|
882
|
+
this.setState({ sessions, activeSessions })
|
|
883
|
+
|
|
884
|
+
this.emit('session:start', { sessionId: id, prompt })
|
|
885
|
+
|
|
886
|
+
const proc = this.container.feature('proc').spawn(this.claudePath, args, {
|
|
887
|
+
cwd,
|
|
888
|
+
stdout: 'pipe',
|
|
889
|
+
stderr: 'pipe',
|
|
890
|
+
stdin: Buffer.from(prompt),
|
|
891
|
+
environment: this.buildEnv(options),
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
this.updateSession(id, { process: proc })
|
|
895
|
+
|
|
896
|
+
// Process in background
|
|
897
|
+
this.consumeStream(id, proc)
|
|
898
|
+
|
|
899
|
+
return id
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Consume the stdout stream of a running process in the background.
|
|
904
|
+
*
|
|
905
|
+
* @param {string} sessionId - The local session ID
|
|
906
|
+
* @param {any} proc - The process handle returned by features.proc.spawn()
|
|
907
|
+
*/
|
|
908
|
+
private async consumeStream(sessionId: string, proc: any): Promise<void> {
|
|
909
|
+
if (!proc?.stdout || !proc?.stderr) {
|
|
910
|
+
const error = 'Process streams are not available'
|
|
911
|
+
this.updateSession(sessionId, { status: 'error', error })
|
|
912
|
+
this.emit('session:error', { sessionId, error })
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let buffer = ''
|
|
917
|
+
let stderr = ''
|
|
918
|
+
|
|
919
|
+
proc.stderr.on('data', (chunk: Buffer | string) => {
|
|
920
|
+
stderr += Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk)
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
const stdoutDone = new Promise<void>((resolve, reject) => {
|
|
924
|
+
proc.stdout.on('data', (chunk: Buffer | string) => {
|
|
925
|
+
buffer += Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk)
|
|
926
|
+
const lines = buffer.split('\n')
|
|
927
|
+
buffer = lines.pop() || ''
|
|
928
|
+
|
|
929
|
+
for (const line of lines) {
|
|
930
|
+
const trimmed = line.trim()
|
|
931
|
+
if (!trimmed) continue
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
const event = JSON.parse(trimmed) as ClaudeEvent
|
|
935
|
+
this.handleEvent(sessionId, event)
|
|
936
|
+
} catch {
|
|
937
|
+
this.emit('session:parse-error', { sessionId, line: trimmed })
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
proc.stdout.on('end', () => {
|
|
943
|
+
if (buffer.trim()) {
|
|
944
|
+
try {
|
|
945
|
+
const event = JSON.parse(buffer.trim()) as ClaudeEvent
|
|
946
|
+
this.handleEvent(sessionId, event)
|
|
947
|
+
} catch {
|
|
948
|
+
// ignore trailing partial data
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
resolve()
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
proc.stdout.on('error', reject)
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
const exitCodePromise = new Promise<number>((resolve, reject) => {
|
|
958
|
+
proc.once('error', reject)
|
|
959
|
+
proc.once('close', (code: number | null) => resolve(code ?? 0))
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
try {
|
|
963
|
+
await stdoutDone
|
|
964
|
+
} catch (err) {
|
|
965
|
+
this.updateSession(sessionId, {
|
|
966
|
+
status: 'error',
|
|
967
|
+
error: err instanceof Error ? err.message : String(err)
|
|
968
|
+
})
|
|
969
|
+
this.emit('session:error', { sessionId, error: err })
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
let exitCode = 1
|
|
973
|
+
try {
|
|
974
|
+
exitCode = await exitCodePromise
|
|
975
|
+
} catch (err) {
|
|
976
|
+
this.updateSession(sessionId, {
|
|
977
|
+
status: 'error',
|
|
978
|
+
error: err instanceof Error ? err.message : String(err),
|
|
979
|
+
})
|
|
980
|
+
this.emit('session:error', { sessionId, error: err })
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (exitCode !== 0 && this.state.current.sessions[sessionId]?.status !== 'completed') {
|
|
984
|
+
this.updateSession(sessionId, {
|
|
985
|
+
status: 'error',
|
|
986
|
+
error: stderr || `Process exited with code ${exitCode}`
|
|
987
|
+
})
|
|
988
|
+
this.emit('session:error', { sessionId, error: stderr, exitCode })
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Finalize file log
|
|
992
|
+
if (this.sessionLogPaths.has(sessionId)) {
|
|
993
|
+
const finalSession = this.state.current.sessions[sessionId]!
|
|
994
|
+
await this.writeLogEntry(sessionId, 'session:end', {
|
|
995
|
+
status: finalSession.status,
|
|
996
|
+
result: finalSession.result,
|
|
997
|
+
error: finalSession.error,
|
|
998
|
+
costUsd: finalSession.costUsd,
|
|
999
|
+
turns: finalSession.turns,
|
|
1000
|
+
messageCount: finalSession.messages.length
|
|
1001
|
+
})
|
|
1002
|
+
this.sessionLogPaths.delete(sessionId)
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Kill a running session's subprocess.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} sessionId - The local session ID to abort
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* ```typescript
|
|
1013
|
+
* const sessionId = cc.start('Do something long')
|
|
1014
|
+
* // ... later
|
|
1015
|
+
* cc.abort(sessionId)
|
|
1016
|
+
* ```
|
|
1017
|
+
*/
|
|
1018
|
+
abort(sessionId: string): void {
|
|
1019
|
+
const session = this.state.current.sessions[sessionId]
|
|
1020
|
+
if (session?.process && session.status === 'running') {
|
|
1021
|
+
session.process.kill()
|
|
1022
|
+
this.updateSession(sessionId, { status: 'error', error: 'Aborted by user' })
|
|
1023
|
+
const activeSessions = this.state.current.activeSessions.filter(id => id !== sessionId)
|
|
1024
|
+
this.setState({ activeSessions })
|
|
1025
|
+
this.emit('session:abort', { sessionId })
|
|
1026
|
+
|
|
1027
|
+
if (this.sessionLogPaths.has(sessionId)) {
|
|
1028
|
+
this.writeLogEntry(sessionId, 'session:abort', { reason: 'Aborted by user' })
|
|
1029
|
+
this.sessionLogPaths.delete(sessionId)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Get a session by its local ID.
|
|
1036
|
+
*
|
|
1037
|
+
* @param {string} sessionId - The local session ID
|
|
1038
|
+
* @returns {ClaudeSession | undefined} The session if it exists
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```typescript
|
|
1042
|
+
* const session = cc.getSession(sessionId)
|
|
1043
|
+
* if (session?.status === 'completed') {
|
|
1044
|
+
* console.log(session.result)
|
|
1045
|
+
* }
|
|
1046
|
+
* ```
|
|
1047
|
+
*/
|
|
1048
|
+
getSession(sessionId: string): ClaudeSession | undefined {
|
|
1049
|
+
return this.state.current.sessions[sessionId]
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Wait for a running session to complete.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {string} sessionId - The local session ID
|
|
1056
|
+
* @returns {Promise<ClaudeSession>} The completed session
|
|
1057
|
+
*
|
|
1058
|
+
* @example
|
|
1059
|
+
* ```typescript
|
|
1060
|
+
* const id = cc.start('Build something cool')
|
|
1061
|
+
* const session = await cc.waitForSession(id)
|
|
1062
|
+
* console.log(session.result)
|
|
1063
|
+
* ```
|
|
1064
|
+
*/
|
|
1065
|
+
async waitForSession(sessionId: string): Promise<ClaudeSession> {
|
|
1066
|
+
const session = this.state.current.sessions[sessionId]
|
|
1067
|
+
if (!session) throw new Error(`Session ${sessionId} not found`)
|
|
1068
|
+
if (session.status === 'completed' || session.status === 'error') return session
|
|
1069
|
+
|
|
1070
|
+
return new Promise((resolve) => {
|
|
1071
|
+
const handler = (data: { sessionId: string }) => {
|
|
1072
|
+
if (data.sessionId === sessionId) {
|
|
1073
|
+
this.off('session:result')
|
|
1074
|
+
this.off('session:error')
|
|
1075
|
+
resolve(this.state.current.sessions[sessionId]!)
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
this.on('session:result', handler)
|
|
1079
|
+
this.on('session:error', handler)
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Get aggregated usage statistics across all sessions, or for a specific session.
|
|
1085
|
+
*
|
|
1086
|
+
* @param {string} [sessionId] - Optional session ID to get usage for a single session
|
|
1087
|
+
* @returns {{ totalCostUsd: number; totalInputTokens: number; totalOutputTokens: number; totalCacheReadTokens: number; totalCacheCreationTokens: number; totalTurns: number; sessionCount: number; sessions: Array<{ id: string; costUsd: number; turns: number; inputTokens: number; outputTokens: number; status: string }> }} Usage statistics
|
|
1088
|
+
*
|
|
1089
|
+
* @example
|
|
1090
|
+
* ```typescript
|
|
1091
|
+
* const stats = cc.usage()
|
|
1092
|
+
* console.log(`Total cost: $${stats.totalCostUsd.toFixed(4)}`)
|
|
1093
|
+
* console.log(`Tokens: ${stats.totalInputTokens} in / ${stats.totalOutputTokens} out`)
|
|
1094
|
+
*
|
|
1095
|
+
* // Single session
|
|
1096
|
+
* const sessionStats = cc.usage(sessionId)
|
|
1097
|
+
* ```
|
|
1098
|
+
*/
|
|
1099
|
+
usage(sessionId?: string) {
|
|
1100
|
+
const allSessions = this.state.current.sessions
|
|
1101
|
+
const entries = sessionId
|
|
1102
|
+
? (allSessions[sessionId] ? [allSessions[sessionId]] : [])
|
|
1103
|
+
: Object.values(allSessions)
|
|
1104
|
+
|
|
1105
|
+
let totalCostUsd = 0
|
|
1106
|
+
let totalInputTokens = 0
|
|
1107
|
+
let totalOutputTokens = 0
|
|
1108
|
+
let totalCacheReadTokens = 0
|
|
1109
|
+
let totalCacheCreationTokens = 0
|
|
1110
|
+
let totalTurns = 0
|
|
1111
|
+
const sessions: Array<{ id: string; costUsd: number; turns: number; inputTokens: number; outputTokens: number; status: string }> = []
|
|
1112
|
+
|
|
1113
|
+
for (const session of entries as ClaudeSession[]) {
|
|
1114
|
+
let inputTokens = 0
|
|
1115
|
+
let outputTokens = 0
|
|
1116
|
+
let cacheRead = 0
|
|
1117
|
+
let cacheCreation = 0
|
|
1118
|
+
|
|
1119
|
+
for (const msg of session.messages || []) {
|
|
1120
|
+
const u = msg.message?.usage
|
|
1121
|
+
if (u) {
|
|
1122
|
+
inputTokens += u.input_tokens || 0
|
|
1123
|
+
outputTokens += u.output_tokens || 0
|
|
1124
|
+
cacheRead += u.cache_read_input_tokens || 0
|
|
1125
|
+
cacheCreation += u.cache_creation_input_tokens || 0
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
totalCostUsd += session.costUsd || 0
|
|
1130
|
+
totalInputTokens += inputTokens
|
|
1131
|
+
totalOutputTokens += outputTokens
|
|
1132
|
+
totalCacheReadTokens += cacheRead
|
|
1133
|
+
totalCacheCreationTokens += cacheCreation
|
|
1134
|
+
totalTurns += session.turns || 0
|
|
1135
|
+
|
|
1136
|
+
sessions.push({
|
|
1137
|
+
id: session.id,
|
|
1138
|
+
costUsd: session.costUsd || 0,
|
|
1139
|
+
turns: session.turns || 0,
|
|
1140
|
+
inputTokens,
|
|
1141
|
+
outputTokens,
|
|
1142
|
+
status: session.status,
|
|
1143
|
+
})
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Budget remaining: if maxBudgetUsd is configured, compute what's left
|
|
1147
|
+
const maxBudgetUsd = this.options.maxBudgetUsd
|
|
1148
|
+
const budgetRemainingUsd = maxBudgetUsd != null ? Math.max(0, maxBudgetUsd - totalCostUsd) : undefined
|
|
1149
|
+
const budgetUsedPercent = maxBudgetUsd != null && maxBudgetUsd > 0 ? Math.min(100, (totalCostUsd / maxBudgetUsd) * 100) : undefined
|
|
1150
|
+
|
|
1151
|
+
return {
|
|
1152
|
+
totalCostUsd,
|
|
1153
|
+
totalInputTokens,
|
|
1154
|
+
totalOutputTokens,
|
|
1155
|
+
totalCacheReadTokens,
|
|
1156
|
+
totalCacheCreationTokens,
|
|
1157
|
+
totalTurns,
|
|
1158
|
+
sessionCount: sessions.length,
|
|
1159
|
+
maxBudgetUsd: maxBudgetUsd ?? null,
|
|
1160
|
+
budgetRemainingUsd: budgetRemainingUsd ?? null,
|
|
1161
|
+
budgetUsedPercent: budgetUsedPercent ?? null,
|
|
1162
|
+
sessions,
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* The Claude CLI session ID of the most recently initialized session,
|
|
1168
|
+
* or the session set via the `session` option. Useful for resuming later.
|
|
1169
|
+
*
|
|
1170
|
+
* @returns {string | undefined} The Claude CLI session ID
|
|
1171
|
+
*
|
|
1172
|
+
* @example
|
|
1173
|
+
* ```typescript
|
|
1174
|
+
* const cc = container.feature('claudeCode')
|
|
1175
|
+
* await cc.run('Do something')
|
|
1176
|
+
* console.log(cc.sessionId) // the Claude CLI session ID
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
get sessionId(): string | undefined {
|
|
1180
|
+
// Check if a default session was set via options
|
|
1181
|
+
if (this.options.session) return this.options.session
|
|
1182
|
+
|
|
1183
|
+
// Find the most recently created session that has a Claude CLI sessionId
|
|
1184
|
+
const sessions = Object.values(this.state.current.sessions) as ClaudeSession[]
|
|
1185
|
+
if (sessions.length === 0) return undefined
|
|
1186
|
+
|
|
1187
|
+
// Return the last session's Claude CLI session ID
|
|
1188
|
+
const last = sessions[sessions.length - 1]
|
|
1189
|
+
return last?.sessionId
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Export session history as a readable markdown document.
|
|
1194
|
+
* Reads from a raw JSONL file (Claude CLI session log or this feature's NDJSON log)
|
|
1195
|
+
* so it works independently of in-memory state.
|
|
1196
|
+
*
|
|
1197
|
+
* Can also accept a local session ID to export from in-memory state as a fallback.
|
|
1198
|
+
*
|
|
1199
|
+
* @param {string} [source] - Path to a JSONL file, a local session ID, or omit for the most recent session
|
|
1200
|
+
* @returns {Promise<string>} Markdown-formatted session history
|
|
1201
|
+
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* ```typescript
|
|
1204
|
+
* // From a JSONL file (works without any prior state)
|
|
1205
|
+
* const md = await cc.sessionHistoryToMarkdown('/path/to/session.jsonl')
|
|
1206
|
+
*
|
|
1207
|
+
* // From the most recent in-memory session
|
|
1208
|
+
* const md = await cc.sessionHistoryToMarkdown()
|
|
1209
|
+
*
|
|
1210
|
+
* // From a specific local session ID
|
|
1211
|
+
* const md = await cc.sessionHistoryToMarkdown(localSessionId)
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
async sessionHistoryToMarkdown(source?: string): Promise<string> {
|
|
1215
|
+
// If source looks like a file path, read JSONL from disk
|
|
1216
|
+
if (source && (source.includes('/') || source.endsWith('.jsonl'))) {
|
|
1217
|
+
return this.jsonlToMarkdown(source)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Otherwise try to resolve from in-memory state
|
|
1221
|
+
const sessionId = source || this.findLastSessionId()
|
|
1222
|
+
if (!sessionId) throw new Error('No session found. Pass a JSONL file path or run a session first.')
|
|
1223
|
+
|
|
1224
|
+
const session = this.state.current.sessions[sessionId] as ClaudeSession | undefined
|
|
1225
|
+
if (!session) throw new Error(`Session ${sessionId} not found in state. Pass a JSONL file path instead.`)
|
|
1226
|
+
|
|
1227
|
+
return this.sessionToMarkdown(session)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Find the local ID of the most recent session.
|
|
1232
|
+
*/
|
|
1233
|
+
private findLastSessionId(): string | undefined {
|
|
1234
|
+
const ids = Object.keys(this.state.current.sessions)
|
|
1235
|
+
return ids.length > 0 ? ids[ids.length - 1] : undefined
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Parse a JSONL file and convert its events to markdown.
|
|
1240
|
+
*/
|
|
1241
|
+
private async jsonlToMarkdown(filePath: string): Promise<string> {
|
|
1242
|
+
const fs = this.container.feature('fs')
|
|
1243
|
+
const content = await fs.readFileAsync(filePath, 'utf-8')
|
|
1244
|
+
const lines = content.split('\n').filter((l: string) => l.trim())
|
|
1245
|
+
|
|
1246
|
+
const events: ClaudeEvent[] = []
|
|
1247
|
+
for (const line of lines) {
|
|
1248
|
+
try {
|
|
1249
|
+
const parsed = JSON.parse(line)
|
|
1250
|
+
// Support both raw Claude events and our wrapper format (which has a .data field)
|
|
1251
|
+
events.push(parsed.data ?? parsed)
|
|
1252
|
+
} catch {
|
|
1253
|
+
// skip malformed lines
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return this.eventsToMarkdown(events, filePath)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Convert a ClaudeSession (from state) to markdown.
|
|
1262
|
+
*/
|
|
1263
|
+
private sessionToMarkdown(session: ClaudeSession): string {
|
|
1264
|
+
const lines: string[] = []
|
|
1265
|
+
|
|
1266
|
+
lines.push(`# Session: ${session.id}`)
|
|
1267
|
+
if (session.sessionId) lines.push(`**Claude Session ID:** \`${session.sessionId}\``)
|
|
1268
|
+
lines.push(`**Status:** ${session.status}`)
|
|
1269
|
+
if (session.costUsd) lines.push(`**Cost:** $${session.costUsd.toFixed(4)}`)
|
|
1270
|
+
if (session.turns) lines.push(`**Turns:** ${session.turns}`)
|
|
1271
|
+
lines.push('')
|
|
1272
|
+
|
|
1273
|
+
lines.push(`## Prompt`)
|
|
1274
|
+
lines.push('')
|
|
1275
|
+
lines.push(session.prompt)
|
|
1276
|
+
lines.push('')
|
|
1277
|
+
|
|
1278
|
+
if (session.messages.length > 0) {
|
|
1279
|
+
lines.push(`## Conversation`)
|
|
1280
|
+
lines.push('')
|
|
1281
|
+
|
|
1282
|
+
for (const msg of session.messages) {
|
|
1283
|
+
this.renderAssistantMessage(lines, msg)
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (session.result) {
|
|
1288
|
+
lines.push(`## Result`)
|
|
1289
|
+
lines.push('')
|
|
1290
|
+
lines.push(session.result)
|
|
1291
|
+
lines.push('')
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (session.error) {
|
|
1295
|
+
lines.push(`## Error`)
|
|
1296
|
+
lines.push('')
|
|
1297
|
+
lines.push(`\`\`\`\n${session.error}\n\`\`\``)
|
|
1298
|
+
lines.push('')
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return lines.join('\n')
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Convert raw Claude events to markdown.
|
|
1306
|
+
*/
|
|
1307
|
+
private eventsToMarkdown(events: ClaudeEvent[], source: string): string {
|
|
1308
|
+
const lines: string[] = []
|
|
1309
|
+
let sessionId: string | undefined
|
|
1310
|
+
let model: string | undefined
|
|
1311
|
+
let prompt: string | undefined
|
|
1312
|
+
let costUsd: number | undefined
|
|
1313
|
+
let turns: number | undefined
|
|
1314
|
+
let durationMs: number | undefined
|
|
1315
|
+
|
|
1316
|
+
// Extract metadata from init and result events
|
|
1317
|
+
for (const event of events) {
|
|
1318
|
+
if (event.type === 'system' && (event as any).subtype === 'init') {
|
|
1319
|
+
const init = event as ClaudeInitEvent
|
|
1320
|
+
sessionId = init.session_id
|
|
1321
|
+
model = init.model
|
|
1322
|
+
}
|
|
1323
|
+
if (event.type === 'result') {
|
|
1324
|
+
const result = event as ClaudeResultEvent
|
|
1325
|
+
costUsd = result.total_cost_usd
|
|
1326
|
+
turns = result.num_turns
|
|
1327
|
+
durationMs = result.duration_ms
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
lines.push(`# Session History`)
|
|
1332
|
+
lines.push(`**Source:** \`${source}\``)
|
|
1333
|
+
if (sessionId) lines.push(`**Session ID:** \`${sessionId}\``)
|
|
1334
|
+
if (model) lines.push(`**Model:** ${model}`)
|
|
1335
|
+
if (costUsd != null) lines.push(`**Cost:** $${costUsd.toFixed(4)}`)
|
|
1336
|
+
if (turns != null) lines.push(`**Turns:** ${turns}`)
|
|
1337
|
+
if (durationMs != null) lines.push(`**Duration:** ${(durationMs / 1000).toFixed(1)}s`)
|
|
1338
|
+
lines.push('')
|
|
1339
|
+
|
|
1340
|
+
lines.push(`## Conversation`)
|
|
1341
|
+
lines.push('')
|
|
1342
|
+
|
|
1343
|
+
for (const event of events) {
|
|
1344
|
+
if (event.type === 'assistant') {
|
|
1345
|
+
this.renderAssistantMessage(lines, event as ClaudeAssistantMessage)
|
|
1346
|
+
} else if (event.type === 'tool_result') {
|
|
1347
|
+
const tr = event as ClaudeToolResult
|
|
1348
|
+
lines.push(`<details>`)
|
|
1349
|
+
lines.push(`<summary>Tool Result (${tr.tool_use_id})</summary>`)
|
|
1350
|
+
lines.push('')
|
|
1351
|
+
lines.push('```')
|
|
1352
|
+
lines.push(tr.content.length > 2000 ? tr.content.slice(0, 2000) + '\n... (truncated)' : tr.content)
|
|
1353
|
+
lines.push('```')
|
|
1354
|
+
lines.push(`</details>`)
|
|
1355
|
+
lines.push('')
|
|
1356
|
+
} else if (event.type === 'result') {
|
|
1357
|
+
const result = event as ClaudeResultEvent
|
|
1358
|
+
lines.push(`## Result`)
|
|
1359
|
+
lines.push('')
|
|
1360
|
+
if (result.is_error) {
|
|
1361
|
+
lines.push(`**Error:**`)
|
|
1362
|
+
lines.push(`\`\`\`\n${result.result}\n\`\`\``)
|
|
1363
|
+
} else {
|
|
1364
|
+
lines.push(result.result)
|
|
1365
|
+
}
|
|
1366
|
+
lines.push('')
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
return lines.join('\n')
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Render a single assistant message to markdown lines.
|
|
1375
|
+
*/
|
|
1376
|
+
private renderAssistantMessage(lines: string[], msg: ClaudeAssistantMessage): void {
|
|
1377
|
+
lines.push(`### Assistant`)
|
|
1378
|
+
if (msg.message?.usage) {
|
|
1379
|
+
const u = msg.message.usage
|
|
1380
|
+
lines.push(`*${u.input_tokens} in / ${u.output_tokens} out tokens*`)
|
|
1381
|
+
}
|
|
1382
|
+
lines.push('')
|
|
1383
|
+
|
|
1384
|
+
for (const block of msg.message?.content || []) {
|
|
1385
|
+
if (block.type === 'text') {
|
|
1386
|
+
lines.push(block.text)
|
|
1387
|
+
lines.push('')
|
|
1388
|
+
} else if (block.type === 'tool_use') {
|
|
1389
|
+
lines.push(`**Tool Use:** \`${block.name}\``)
|
|
1390
|
+
lines.push('```json')
|
|
1391
|
+
lines.push(JSON.stringify(block.input, null, 2))
|
|
1392
|
+
lines.push('```')
|
|
1393
|
+
lines.push('')
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* List all Claude Code processes currently registered in ~/.claude/sessions/.
|
|
1400
|
+
* Returns each session's metadata along with whether the process is still alive.
|
|
1401
|
+
*
|
|
1402
|
+
* @returns {Promise<Array<{ pid: number; sessionId: string; cwd: string; startedAt: number; kind: string; entrypoint: string; alive: boolean }>>}
|
|
1403
|
+
*
|
|
1404
|
+
* @example
|
|
1405
|
+
* const sessions = await cc.listProcessSessions()
|
|
1406
|
+
* for (const s of sessions) {
|
|
1407
|
+
* console.log(`[${s.alive ? 'LIVE' : 'dead'}] PID ${s.pid} in ${s.cwd}`)
|
|
1408
|
+
* }
|
|
1409
|
+
*/
|
|
1410
|
+
async listProcessSessions(): Promise<Array<{
|
|
1411
|
+
pid: number
|
|
1412
|
+
sessionId: string
|
|
1413
|
+
cwd: string
|
|
1414
|
+
startedAt: number
|
|
1415
|
+
kind: string
|
|
1416
|
+
entrypoint: string
|
|
1417
|
+
alive: boolean
|
|
1418
|
+
}>> {
|
|
1419
|
+
const fs = this.container.feature('fs')
|
|
1420
|
+
const proc = this.container.feature('proc')
|
|
1421
|
+
const home = this.container.feature('os').homedir
|
|
1422
|
+
const sessionsDir = `${home}/.claude/sessions`
|
|
1423
|
+
|
|
1424
|
+
let files: string[]
|
|
1425
|
+
try {
|
|
1426
|
+
files = await fs.readdir(sessionsDir)
|
|
1427
|
+
} catch {
|
|
1428
|
+
return []
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const jsonFiles = files.filter((f: string) => f.endsWith('.json'))
|
|
1432
|
+
|
|
1433
|
+
const results = await Promise.all(jsonFiles.map(async (file: string) => {
|
|
1434
|
+
try {
|
|
1435
|
+
const raw = await fs.readFile(`${sessionsDir}/${file}`, 'utf8')
|
|
1436
|
+
const data = JSON.parse(raw)
|
|
1437
|
+
let alive = false
|
|
1438
|
+
try {
|
|
1439
|
+
await proc.exec(`kill -0 ${data.pid}`)
|
|
1440
|
+
alive = true
|
|
1441
|
+
} catch {
|
|
1442
|
+
alive = false
|
|
1443
|
+
}
|
|
1444
|
+
return { ...data, alive }
|
|
1445
|
+
} catch {
|
|
1446
|
+
return null
|
|
1447
|
+
}
|
|
1448
|
+
}))
|
|
1449
|
+
|
|
1450
|
+
return results.filter(Boolean)
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Read a single process session by PID from ~/.claude/sessions/<pid>.json.
|
|
1455
|
+
*
|
|
1456
|
+
* @param {number} pid - The process ID
|
|
1457
|
+
* @returns {Promise<{ pid: number; sessionId: string; cwd: string; startedAt: number; kind: string; entrypoint: string } | null>}
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* const session = await cc.getProcessSession(12345)
|
|
1461
|
+
* console.log(session?.cwd)
|
|
1462
|
+
*/
|
|
1463
|
+
async getProcessSession(pid: number): Promise<{
|
|
1464
|
+
pid: number
|
|
1465
|
+
sessionId: string
|
|
1466
|
+
cwd: string
|
|
1467
|
+
startedAt: number
|
|
1468
|
+
kind: string
|
|
1469
|
+
entrypoint: string
|
|
1470
|
+
} | null> {
|
|
1471
|
+
const fs = this.container.feature('fs')
|
|
1472
|
+
const home = this.container.feature('os').homedir
|
|
1473
|
+
try {
|
|
1474
|
+
const raw = await fs.readFile(`${home}/.claude/sessions/${pid}.json`, 'utf8')
|
|
1475
|
+
return JSON.parse(raw)
|
|
1476
|
+
} catch {
|
|
1477
|
+
return null
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Read the conversation history for a Claude Code session from its JSONL file in
|
|
1483
|
+
* ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl.
|
|
1484
|
+
*
|
|
1485
|
+
* Returns an array of parsed message objects (user, assistant, tool_use, tool_result).
|
|
1486
|
+
*
|
|
1487
|
+
* @param {string} sessionId - The Claude CLI session ID (from listProcessSessions or getProcessSession)
|
|
1488
|
+
* @param {string} cwd - The working directory of the session (used to locate the project folder)
|
|
1489
|
+
* @returns {Promise<any[]>} Array of parsed JSONL records
|
|
1490
|
+
*
|
|
1491
|
+
* @example
|
|
1492
|
+
* const sessions = await cc.listProcessSessions()
|
|
1493
|
+
* const s = sessions[0]
|
|
1494
|
+
* const history = await cc.getConversationHistory(s.sessionId, s.cwd)
|
|
1495
|
+
* console.log(history.length, 'turns')
|
|
1496
|
+
*/
|
|
1497
|
+
async getConversationHistory(sessionId: string, cwd?: string): Promise<any[]> {
|
|
1498
|
+
const fs = this.container.feature('fs')
|
|
1499
|
+
const home = this.container.feature('os').homedir
|
|
1500
|
+
const resolvedCwd = cwd ?? this.options.cwd ?? (this.container as any).cwd
|
|
1501
|
+
const encodedCwd = resolvedCwd.replace(/[\\/]/g, '-').replace(/@/g, '-')
|
|
1502
|
+
const filePath = `${home}/.claude/projects/${encodedCwd}/${sessionId}.jsonl`
|
|
1503
|
+
try {
|
|
1504
|
+
const raw = await fs.readFile(filePath, 'utf8')
|
|
1505
|
+
return raw
|
|
1506
|
+
.split('\n')
|
|
1507
|
+
.filter((line: string) => line.trim())
|
|
1508
|
+
.map((line: string) => JSON.parse(line))
|
|
1509
|
+
} catch {
|
|
1510
|
+
return []
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* List all conversation sessions stored for a given working directory.
|
|
1516
|
+
* Reads ~/.claude/projects/<encoded-cwd>/ and returns metadata for each .jsonl file.
|
|
1517
|
+
*
|
|
1518
|
+
* @param {string} cwd - The working directory path to look up
|
|
1519
|
+
* @returns {Promise<Array<{ sessionId: string; filePath: string; messageCount: number }>>}
|
|
1520
|
+
*
|
|
1521
|
+
* @example
|
|
1522
|
+
* const sessions = await cc.listSessionsForCwd('/Users/me/my-project')
|
|
1523
|
+
* for (const s of sessions) {
|
|
1524
|
+
* console.log(s.sessionId, s.messageCount, 'messages')
|
|
1525
|
+
* }
|
|
1526
|
+
*/
|
|
1527
|
+
async listSessionsForCwd(cwd?: string): Promise<Array<{
|
|
1528
|
+
sessionId: string
|
|
1529
|
+
filePath: string
|
|
1530
|
+
messageCount: number
|
|
1531
|
+
}>> {
|
|
1532
|
+
const fs = this.container.feature('fs')
|
|
1533
|
+
const home = this.container.feature('os').homedir
|
|
1534
|
+
const resolvedCwd = cwd ?? this.options.cwd ?? (this.container as any).cwd
|
|
1535
|
+
const encodedCwd = resolvedCwd.replace(/[\\/]/g, '-').replace(/@/g, '-')
|
|
1536
|
+
const projectDir = `${home}/.claude/projects/${encodedCwd}`
|
|
1537
|
+
|
|
1538
|
+
let files: string[]
|
|
1539
|
+
try {
|
|
1540
|
+
files = await fs.readdir(projectDir)
|
|
1541
|
+
} catch {
|
|
1542
|
+
return []
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const jsonlFiles = files.filter((f: string) => f.endsWith('.jsonl'))
|
|
1546
|
+
|
|
1547
|
+
const results = await Promise.all(jsonlFiles.map(async (file: string) => {
|
|
1548
|
+
const sessionId = file.replace(/\.jsonl$/, '')
|
|
1549
|
+
const filePath = `${projectDir}/${file}`
|
|
1550
|
+
try {
|
|
1551
|
+
const raw = await fs.readFile(filePath, 'utf8')
|
|
1552
|
+
const messageCount = raw.split('\n').filter((l: string) => l.trim()).length
|
|
1553
|
+
return { sessionId, filePath, messageCount }
|
|
1554
|
+
} catch {
|
|
1555
|
+
return { sessionId, filePath, messageCount: 0 }
|
|
1556
|
+
}
|
|
1557
|
+
}))
|
|
1558
|
+
|
|
1559
|
+
return results
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Clean up any temp MCP config files created during sessions.
|
|
1564
|
+
*/
|
|
1565
|
+
async cleanupMcpTempFiles(): Promise<void> {
|
|
1566
|
+
for (const path of this.mcpTempFiles) {
|
|
1567
|
+
try { await this.container.feature('fs').rm(path) } catch { /* already gone */ }
|
|
1568
|
+
}
|
|
1569
|
+
this.mcpTempFiles = []
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Initialize the feature.
|
|
1574
|
+
*
|
|
1575
|
+
* @param {any} [options] - Enable options
|
|
1576
|
+
* @returns {Promise<this>} The enabled feature
|
|
1577
|
+
*/
|
|
1578
|
+
override async enable(options: any = {}): Promise<this> {
|
|
1579
|
+
await super.enable(options)
|
|
1580
|
+
return this
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
export default ClaudeCode
|