@soederpop/luca 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
4
|
+
import type { Container } from '@soederpop/luca/container'
|
|
5
|
+
import { type AvailableFeatures } from '@soederpop/luca/feature'
|
|
6
|
+
import { features, Feature } from '@soederpop/luca/feature'
|
|
7
|
+
|
|
8
|
+
declare module '@soederpop/luca/feature' {
|
|
9
|
+
interface AvailableFeatures {
|
|
10
|
+
claudeCode: typeof ClaudeCode
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- Stream JSON types from the Claude CLI ---
|
|
15
|
+
|
|
16
|
+
export interface ClaudeInitEvent {
|
|
17
|
+
type: 'system'
|
|
18
|
+
subtype: 'init'
|
|
19
|
+
session_id: string
|
|
20
|
+
cwd: string
|
|
21
|
+
model: string
|
|
22
|
+
tools: string[]
|
|
23
|
+
mcp_servers: string[]
|
|
24
|
+
permissionMode: string
|
|
25
|
+
claude_code_version: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ClaudeAssistantMessage {
|
|
29
|
+
type: 'assistant'
|
|
30
|
+
message: {
|
|
31
|
+
id: string
|
|
32
|
+
model: string
|
|
33
|
+
role: 'assistant'
|
|
34
|
+
content: Array<{ type: 'text'; text: string } | { type: 'tool_use'; id: string; name: string; input: any }>
|
|
35
|
+
stop_reason: string | null
|
|
36
|
+
usage: {
|
|
37
|
+
input_tokens: number
|
|
38
|
+
output_tokens: number
|
|
39
|
+
cache_read_input_tokens?: number
|
|
40
|
+
cache_creation_input_tokens?: number
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
session_id: string
|
|
44
|
+
parent_tool_use_id: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ClaudeToolResult {
|
|
48
|
+
type: 'tool_result'
|
|
49
|
+
tool_use_id: string
|
|
50
|
+
content: string
|
|
51
|
+
session_id: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ClaudeStreamEvent {
|
|
55
|
+
type: 'stream_event'
|
|
56
|
+
event: {
|
|
57
|
+
type: 'message_start' | 'content_block_start' | 'content_block_delta' | 'content_block_stop' | 'message_delta' | 'message_stop'
|
|
58
|
+
index?: number
|
|
59
|
+
delta?: { type: string; text?: string }
|
|
60
|
+
content_block?: { type: string; text?: string }
|
|
61
|
+
message?: any
|
|
62
|
+
usage?: any
|
|
63
|
+
}
|
|
64
|
+
session_id: string
|
|
65
|
+
parent_tool_use_id: string | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ClaudeResultEvent {
|
|
69
|
+
type: 'result'
|
|
70
|
+
subtype: 'success' | 'error'
|
|
71
|
+
is_error: boolean
|
|
72
|
+
result: string
|
|
73
|
+
session_id: string
|
|
74
|
+
duration_ms: number
|
|
75
|
+
num_turns: number
|
|
76
|
+
total_cost_usd: number
|
|
77
|
+
usage: Record<string, any>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ClaudeEvent = ClaudeInitEvent | ClaudeAssistantMessage | ClaudeToolResult | ClaudeStreamEvent | ClaudeResultEvent | { type: string; [key: string]: any }
|
|
81
|
+
|
|
82
|
+
// --- Session types ---
|
|
83
|
+
|
|
84
|
+
export interface ClaudeSession {
|
|
85
|
+
id: string
|
|
86
|
+
sessionId?: string
|
|
87
|
+
status: 'idle' | 'running' | 'completed' | 'error'
|
|
88
|
+
prompt: string
|
|
89
|
+
result?: string
|
|
90
|
+
error?: string
|
|
91
|
+
costUsd: number
|
|
92
|
+
turns: number
|
|
93
|
+
messages: ClaudeAssistantMessage[]
|
|
94
|
+
process?: any
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- MCP server config types ---
|
|
98
|
+
|
|
99
|
+
export interface McpStdioServer {
|
|
100
|
+
type: 'stdio'
|
|
101
|
+
command: string
|
|
102
|
+
args?: string[]
|
|
103
|
+
env?: Record<string, string>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface McpHttpServer {
|
|
107
|
+
type: 'http'
|
|
108
|
+
url: string
|
|
109
|
+
headers?: Record<string, string>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface McpSseServer {
|
|
113
|
+
type: 'sse'
|
|
114
|
+
url: string
|
|
115
|
+
headers?: Record<string, string>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type McpServerConfig = McpStdioServer | McpHttpServer | McpSseServer
|
|
119
|
+
|
|
120
|
+
// --- Feature state and options ---
|
|
121
|
+
|
|
122
|
+
export const ClaudeCodeStateSchema = FeatureStateSchema.extend({
|
|
123
|
+
/** Map of session IDs to ClaudeSession objects */
|
|
124
|
+
sessions: z.record(z.string(), z.any()).describe('Map of session IDs to ClaudeSession objects'),
|
|
125
|
+
/** List of currently running session IDs */
|
|
126
|
+
activeSessions: z.array(z.string()).describe('List of currently running session IDs'),
|
|
127
|
+
/** Whether the Claude CLI binary is available */
|
|
128
|
+
claudeAvailable: z.boolean().describe('Whether the Claude CLI binary is available'),
|
|
129
|
+
/** Detected Claude CLI version string */
|
|
130
|
+
claudeVersion: z.string().optional().describe('Detected Claude CLI version string'),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
export const FileLogLevelSchema = z.enum(['verbose', 'normal', 'minimal']).describe(
|
|
134
|
+
'Log verbosity: verbose=all events including stream deltas, normal=messages+tool calls+results, minimal=init+result/error only'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
export type FileLogLevel = z.infer<typeof FileLogLevelSchema>
|
|
138
|
+
|
|
139
|
+
export const ClaudeCodeOptionsSchema = FeatureOptionsSchema.extend({
|
|
140
|
+
/** Path to the claude CLI binary. Defaults to 'claude'. */
|
|
141
|
+
claudePath: z.string().optional().describe('Path to the claude CLI binary'),
|
|
142
|
+
/** Default model to use for sessions. */
|
|
143
|
+
model: z.string().optional().describe('Default model to use for sessions'),
|
|
144
|
+
/** Default working directory for sessions. */
|
|
145
|
+
cwd: z.string().optional().describe('Default working directory for sessions'),
|
|
146
|
+
/** Default system prompt prepended to all sessions. */
|
|
147
|
+
systemPrompt: z.string().optional().describe('Default system prompt prepended to all sessions'),
|
|
148
|
+
/** Default append system prompt for all sessions. */
|
|
149
|
+
appendSystemPrompt: z.string().optional().describe('Default append system prompt for all sessions'),
|
|
150
|
+
/** Default permission mode. */
|
|
151
|
+
permissionMode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk']).optional().describe('Default permission mode for Claude CLI sessions'),
|
|
152
|
+
/** Default allowed tools. */
|
|
153
|
+
allowedTools: z.array(z.string()).optional().describe('Default allowed tools for sessions'),
|
|
154
|
+
/** Default disallowed tools. */
|
|
155
|
+
disallowedTools: z.array(z.string()).optional().describe('Default disallowed tools for sessions'),
|
|
156
|
+
/** Whether to stream partial messages (token-by-token). Defaults to false. */
|
|
157
|
+
streaming: z.boolean().optional().describe('Whether to stream partial messages token-by-token'),
|
|
158
|
+
/** MCP config file paths to pass to sessions. */
|
|
159
|
+
mcpConfig: z.array(z.string()).optional().describe('MCP config file paths to pass to sessions'),
|
|
160
|
+
/** MCP servers to inject into sessions, keyed by server name. Automatically written to a temp config file. */
|
|
161
|
+
mcpServers: z.record(z.string(), z.any()).optional().describe('MCP server configs keyed by name, injected into sessions via temp config file'),
|
|
162
|
+
/** Path to write a parseable NDJSON session log file. Each line is a JSON object with timestamp, sessionId, event type, and event data. */
|
|
163
|
+
fileLogPath: z.string().optional().describe('Path to write a parseable NDJSON session log file'),
|
|
164
|
+
/** Verbosity level for file logging. Defaults to "normal". */
|
|
165
|
+
fileLogLevel: FileLogLevelSchema.optional().describe('Verbosity level for file logging. Defaults to "normal"'),
|
|
166
|
+
/** Default effort level for Claude reasoning. */
|
|
167
|
+
effort: z.enum(['low', 'medium', 'high']).optional().describe('Default effort level for Claude reasoning'),
|
|
168
|
+
/** Maximum cost budget in USD per session. */
|
|
169
|
+
maxBudgetUsd: z.number().optional().describe('Maximum cost budget in USD per session'),
|
|
170
|
+
/** Fallback model when the primary model is unavailable. */
|
|
171
|
+
fallbackModel: z.string().optional().describe('Fallback model when the primary model is unavailable'),
|
|
172
|
+
/** Default agent to use. */
|
|
173
|
+
agent: z.string().optional().describe('Default agent to use'),
|
|
174
|
+
/** Disable session persistence across runs. */
|
|
175
|
+
noSessionPersistence: z.boolean().optional().describe('Disable session persistence across runs'),
|
|
176
|
+
/** Default tools to make available. */
|
|
177
|
+
tools: z.array(z.string()).optional().describe('Default tools to make available'),
|
|
178
|
+
/** Require strict MCP config validation. */
|
|
179
|
+
strictMcpConfig: z.boolean().optional().describe('Require strict MCP config validation'),
|
|
180
|
+
/** Path to a custom settings file. */
|
|
181
|
+
settingsFile: z.string().optional().describe('Path to a custom settings file'),
|
|
182
|
+
/** Directories containing Claude Code skills (SKILL.md files) to load into sessions. Passed as --add-dir. */
|
|
183
|
+
skillsFolders: z.array(z.string()).optional().describe('Directories containing Claude Code skills to load into sessions'),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
export type ClaudeCodeState = z.infer<typeof ClaudeCodeStateSchema>
|
|
187
|
+
export type ClaudeCodeOptions = z.infer<typeof ClaudeCodeOptionsSchema>
|
|
188
|
+
|
|
189
|
+
export interface RunOptions {
|
|
190
|
+
/** Override model for this session. */
|
|
191
|
+
model?: string
|
|
192
|
+
/** Override working directory. */
|
|
193
|
+
cwd?: string
|
|
194
|
+
/** System prompt for this session. */
|
|
195
|
+
systemPrompt?: string
|
|
196
|
+
/** Append system prompt for this session. */
|
|
197
|
+
appendSystemPrompt?: string
|
|
198
|
+
/** Permission mode override. */
|
|
199
|
+
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk'
|
|
200
|
+
/** Allowed tools override. */
|
|
201
|
+
allowedTools?: string[]
|
|
202
|
+
/** Disallowed tools override. */
|
|
203
|
+
disallowedTools?: string[]
|
|
204
|
+
/** Whether to stream partial messages. */
|
|
205
|
+
streaming?: boolean
|
|
206
|
+
/** Resume a previous session by ID. */
|
|
207
|
+
resumeSessionId?: string
|
|
208
|
+
/** Continue the most recent conversation. */
|
|
209
|
+
continue?: boolean
|
|
210
|
+
/** Additional directories to allow tool access to. */
|
|
211
|
+
addDirs?: string[]
|
|
212
|
+
/** Directories containing Claude Code skills (SKILL.md files) to load into sessions. Merged with addDirs as --add-dir. */
|
|
213
|
+
skillsFolders?: string[]
|
|
214
|
+
/** MCP config file paths. */
|
|
215
|
+
mcpConfig?: string[]
|
|
216
|
+
/** MCP servers to inject, keyed by server name. */
|
|
217
|
+
mcpServers?: Record<string, McpServerConfig>
|
|
218
|
+
/** Skip all permission checks (only for sandboxed environments). */
|
|
219
|
+
dangerouslySkipPermissions?: boolean
|
|
220
|
+
/** Additional arbitrary CLI flags. */
|
|
221
|
+
extraArgs?: string[]
|
|
222
|
+
/** Path to write a parseable NDJSON session log file. Overrides feature-level fileLogPath. */
|
|
223
|
+
fileLogPath?: string
|
|
224
|
+
/** Verbosity level for file logging. Overrides feature-level fileLogLevel. */
|
|
225
|
+
fileLogLevel?: FileLogLevel
|
|
226
|
+
/** Effort level for Claude reasoning. */
|
|
227
|
+
effort?: 'low' | 'medium' | 'high'
|
|
228
|
+
/** Maximum cost budget in USD. */
|
|
229
|
+
maxBudgetUsd?: number
|
|
230
|
+
/** Fallback model when the primary is unavailable. */
|
|
231
|
+
fallbackModel?: string
|
|
232
|
+
/** JSON schema for structured output validation. */
|
|
233
|
+
jsonSchema?: string | object
|
|
234
|
+
/** Agent to use for this session. */
|
|
235
|
+
agent?: string
|
|
236
|
+
/** Resume or fork a specific Claude session by ID. */
|
|
237
|
+
sessionId?: string
|
|
238
|
+
/** Disable session persistence for this run. */
|
|
239
|
+
noSessionPersistence?: boolean
|
|
240
|
+
/** Fork from an existing session instead of resuming. */
|
|
241
|
+
forkSession?: boolean
|
|
242
|
+
/** Tools to make available. */
|
|
243
|
+
tools?: string[]
|
|
244
|
+
/** Require strict MCP config validation. */
|
|
245
|
+
strictMcpConfig?: boolean
|
|
246
|
+
/** Enable debug output. Pass a string for specific debug channels, or true for all. */
|
|
247
|
+
debug?: string | boolean
|
|
248
|
+
/** Path to write debug output to a file. */
|
|
249
|
+
debugFile?: string
|
|
250
|
+
/** Path to a custom settings file. */
|
|
251
|
+
settingsFile?: string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Claude Code CLI wrapper feature. Spawns and manages Claude Code sessions
|
|
256
|
+
* as subprocesses, streaming structured JSON events back through the
|
|
257
|
+
* container's event system.
|
|
258
|
+
*
|
|
259
|
+
* Sessions are long-lived: each call to `run()` spawns a `claude -p` process
|
|
260
|
+
* with `--output-format stream-json`, parses NDJSON from stdout line-by-line,
|
|
261
|
+
* and emits typed events on the feature's event bus.
|
|
262
|
+
*
|
|
263
|
+
* @extends Feature
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* const cc = container.feature('claudeCode')
|
|
268
|
+
*
|
|
269
|
+
* // Listen for events
|
|
270
|
+
* cc.on('session:delta', ({ sessionId, text }) => process.stdout.write(text))
|
|
271
|
+
* cc.on('session:result', ({ sessionId, result }) => console.log('Done:', result))
|
|
272
|
+
*
|
|
273
|
+
* // Run a prompt
|
|
274
|
+
* const session = await cc.run('Explain the architecture of this project')
|
|
275
|
+
* console.log(session.result)
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
|
|
279
|
+
static override stateSchema = ClaudeCodeStateSchema
|
|
280
|
+
static override optionsSchema = ClaudeCodeOptionsSchema
|
|
281
|
+
static override shortcut = 'features.claudeCode' as const
|
|
282
|
+
static override envVars = ['TMPDIR']
|
|
283
|
+
|
|
284
|
+
static attach(container: Container<AvailableFeatures, any>) {
|
|
285
|
+
container.features.register('claudeCode', ClaudeCode)
|
|
286
|
+
return container
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
override get initialState(): ClaudeCodeState {
|
|
290
|
+
return {
|
|
291
|
+
...super.initialState,
|
|
292
|
+
sessions: {},
|
|
293
|
+
activeSessions: [],
|
|
294
|
+
claudeAvailable: false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resolve the path to the claude CLI binary.
|
|
300
|
+
*
|
|
301
|
+
* @returns {string} The path to the claude binary
|
|
302
|
+
*/
|
|
303
|
+
get claudePath(): string {
|
|
304
|
+
return this.options.claudePath || 'claude'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Parsed semver components from the detected CLI version, or undefined if not yet checked.
|
|
309
|
+
*
|
|
310
|
+
* @returns {{ major: number; minor: number; patch: number } | undefined} Parsed version
|
|
311
|
+
*/
|
|
312
|
+
get parsedVersion(): { major: number; minor: number; patch: number } | undefined {
|
|
313
|
+
const ver = this.state.current.claudeVersion
|
|
314
|
+
if (!ver) return undefined
|
|
315
|
+
const match = ver.match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
316
|
+
if (!match) return undefined
|
|
317
|
+
return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10) }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Assert that the detected CLI version meets a minimum major.minor requirement.
|
|
322
|
+
* Throws if the CLI version is below the specified minimum.
|
|
323
|
+
*
|
|
324
|
+
* @param {number} major - Minimum major version
|
|
325
|
+
* @param {number} minor - Minimum minor version
|
|
326
|
+
*/
|
|
327
|
+
assertMinVersion(major: number, minor: number): void {
|
|
328
|
+
const v = this.parsedVersion
|
|
329
|
+
if (!v) throw new Error('Claude CLI version not detected. Call checkAvailability() first.')
|
|
330
|
+
if (v.major < major || (v.major === major && v.minor < minor)) {
|
|
331
|
+
throw new Error(`Claude CLI ${this.state.current.claudeVersion} is below minimum ${major}.${minor}`)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if the Claude CLI is available and capture its version.
|
|
337
|
+
*
|
|
338
|
+
* @returns {Promise<boolean>} Whether the CLI is available
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* const available = await cc.checkAvailability()
|
|
343
|
+
* if (!available) throw new Error('Claude CLI not found')
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
async checkAvailability(): Promise<boolean> {
|
|
347
|
+
try {
|
|
348
|
+
const proc = this.container.feature('proc')
|
|
349
|
+
const result = await proc.spawnAndCapture(this.claudePath, ['--version'])
|
|
350
|
+
const stdout = result.stdout
|
|
351
|
+
const exitCode = result.exitCode
|
|
352
|
+
|
|
353
|
+
if (exitCode === 0) {
|
|
354
|
+
const version = stdout.trim()
|
|
355
|
+
this.setState({ claudeAvailable: true, claudeVersion: version })
|
|
356
|
+
|
|
357
|
+
const v = this.parsedVersion
|
|
358
|
+
if (v && (v.major < 2 || (v.major === 2 && v.minor < 1))) {
|
|
359
|
+
this.emit('session:warning', {
|
|
360
|
+
message: `Claude CLI ${version} is below minimum 2.1. Some features may not work.`
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.setState({ claudeAvailable: false })
|
|
368
|
+
return false
|
|
369
|
+
} catch {
|
|
370
|
+
this.setState({ claudeAvailable: false })
|
|
371
|
+
return false
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Tracks temp MCP config files created for cleanup */
|
|
376
|
+
private mcpTempFiles: string[] = []
|
|
377
|
+
|
|
378
|
+
/** Tracks active file log paths per session */
|
|
379
|
+
private sessionLogPaths: Map<string, { path: string; level: FileLogLevel }> = new Map()
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolve the file log path for a session, checking per-session options then feature-level defaults.
|
|
383
|
+
*
|
|
384
|
+
* @param {RunOptions} options - Per-session options
|
|
385
|
+
* @returns {{ path: string; level: FileLogLevel } | undefined} Log config if logging is enabled
|
|
386
|
+
*/
|
|
387
|
+
private resolveFileLog(options: RunOptions = {}): { path: string; level: FileLogLevel } | undefined {
|
|
388
|
+
const path = options.fileLogPath ?? this.options.fileLogPath
|
|
389
|
+
if (!path) return undefined
|
|
390
|
+
const level = options.fileLogLevel ?? this.options.fileLogLevel ?? 'normal'
|
|
391
|
+
return { path, level }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Write a log entry to the session's NDJSON log file.
|
|
396
|
+
* Each line is a self-contained JSON object with timestamp, sessionId, event type, and data.
|
|
397
|
+
*
|
|
398
|
+
* @param {string} sessionId - The local session ID
|
|
399
|
+
* @param {string} type - Event type label (e.g. 'session:init', 'session:message')
|
|
400
|
+
* @param {any} data - Event payload
|
|
401
|
+
*/
|
|
402
|
+
private async writeLogEntry(sessionId: string, type: string, data: any): Promise<void> {
|
|
403
|
+
const logConfig = this.sessionLogPaths.get(sessionId)
|
|
404
|
+
if (!logConfig) return
|
|
405
|
+
|
|
406
|
+
const entry = JSON.stringify({
|
|
407
|
+
ts: new Date().toISOString(),
|
|
408
|
+
session: sessionId,
|
|
409
|
+
type,
|
|
410
|
+
data
|
|
411
|
+
}) + '\n'
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await this.container.feature('fs').appendFileAsync(logConfig.path, entry)
|
|
415
|
+
} catch (err) {
|
|
416
|
+
this.emit('session:log-error', { sessionId, error: err })
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Determine if an event should be logged based on the configured log level.
|
|
422
|
+
*
|
|
423
|
+
* - verbose: all events (stream deltas, partial messages, everything)
|
|
424
|
+
* - normal: assistant messages, tool results, init, result, errors (no stream_event)
|
|
425
|
+
* - minimal: init and result/error only
|
|
426
|
+
*
|
|
427
|
+
* @param {string} eventType - The Claude event type
|
|
428
|
+
* @param {FileLogLevel} level - The configured log level
|
|
429
|
+
* @returns {boolean} Whether to log this event
|
|
430
|
+
*/
|
|
431
|
+
private shouldLog(eventType: string, level: FileLogLevel): boolean {
|
|
432
|
+
switch (level) {
|
|
433
|
+
case 'verbose':
|
|
434
|
+
return true
|
|
435
|
+
case 'normal':
|
|
436
|
+
return eventType !== 'stream_event'
|
|
437
|
+
case 'minimal':
|
|
438
|
+
return eventType === 'system' || eventType === 'result'
|
|
439
|
+
default:
|
|
440
|
+
return true
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Write an MCP server config map to a temp file suitable for `--mcp-config`.
|
|
446
|
+
*
|
|
447
|
+
* @param {Record<string, McpServerConfig>} servers - Server configs keyed by name
|
|
448
|
+
* @returns {Promise<string>} Path to the generated temp config file
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```typescript
|
|
452
|
+
* const configPath = await cc.writeMcpConfig({
|
|
453
|
+
* 'my-api': { type: 'http', url: 'https://api.example.com/mcp' },
|
|
454
|
+
* 'local-tool': { type: 'stdio', command: 'bun', args: ['run', 'server.ts'] }
|
|
455
|
+
* })
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
async writeMcpConfig(servers: Record<string, McpServerConfig>): Promise<string> {
|
|
459
|
+
const config = { mcpServers: servers }
|
|
460
|
+
const tmpDir = process.env.TMPDIR || '/tmp'
|
|
461
|
+
const tmpPath = `${tmpDir}/luca-mcp-${crypto.randomUUID()}.json`
|
|
462
|
+
await this.container.feature('fs').writeFileAsync(tmpPath, JSON.stringify(config, null, 2))
|
|
463
|
+
this.mcpTempFiles.push(tmpPath)
|
|
464
|
+
return tmpPath
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Build the argument array for a claude CLI invocation.
|
|
469
|
+
*
|
|
470
|
+
* @param {string} prompt - The prompt text
|
|
471
|
+
* @param {RunOptions} options - Session options
|
|
472
|
+
* @returns {Promise<string[]>} CLI arguments
|
|
473
|
+
*/
|
|
474
|
+
private async buildArgs(prompt: string, options: RunOptions = {}): Promise<string[]> {
|
|
475
|
+
const args: string[] = ['-p', '--output-format', 'stream-json', '--verbose']
|
|
476
|
+
|
|
477
|
+
const streaming = options.streaming ?? this.options.streaming ?? false
|
|
478
|
+
if (streaming) {
|
|
479
|
+
args.push('--include-partial-messages')
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const model = options.model ?? this.options.model
|
|
483
|
+
if (model) args.push('--model', model)
|
|
484
|
+
|
|
485
|
+
const systemPrompt = options.systemPrompt ?? this.options.systemPrompt
|
|
486
|
+
if (systemPrompt) args.push('--system-prompt', systemPrompt)
|
|
487
|
+
|
|
488
|
+
const appendSystemPrompt = options.appendSystemPrompt ?? this.options.appendSystemPrompt
|
|
489
|
+
if (appendSystemPrompt) args.push('--append-system-prompt', appendSystemPrompt)
|
|
490
|
+
|
|
491
|
+
const permissionMode = options.permissionMode ?? this.options.permissionMode
|
|
492
|
+
if (permissionMode) args.push('--permission-mode', permissionMode)
|
|
493
|
+
|
|
494
|
+
const allowedTools = options.allowedTools ?? this.options.allowedTools
|
|
495
|
+
if (allowedTools?.length) args.push('--allowed-tools', ...allowedTools)
|
|
496
|
+
|
|
497
|
+
const disallowedTools = options.disallowedTools ?? this.options.disallowedTools
|
|
498
|
+
if (disallowedTools?.length) args.push('--disallowed-tools', ...disallowedTools)
|
|
499
|
+
|
|
500
|
+
// Collect all --mcp-config paths
|
|
501
|
+
const configPaths: string[] = []
|
|
502
|
+
|
|
503
|
+
const mcpConfig = options.mcpConfig ?? this.options.mcpConfig
|
|
504
|
+
if (mcpConfig?.length) configPaths.push(...mcpConfig)
|
|
505
|
+
|
|
506
|
+
// Merge mcpServers from feature-level defaults and per-session overrides
|
|
507
|
+
const defaultServers = this.options.mcpServers as Record<string, McpServerConfig> | undefined
|
|
508
|
+
const sessionServers = options.mcpServers
|
|
509
|
+
const mergedServers = { ...defaultServers, ...sessionServers }
|
|
510
|
+
|
|
511
|
+
if (Object.keys(mergedServers).length > 0) {
|
|
512
|
+
const tmpPath = await this.writeMcpConfig(mergedServers)
|
|
513
|
+
configPaths.push(tmpPath)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (configPaths.length) args.push('--mcp-config', ...configPaths)
|
|
517
|
+
|
|
518
|
+
if (options.resumeSessionId) args.push('--resume', options.resumeSessionId)
|
|
519
|
+
if (options.continue) args.push('--continue')
|
|
520
|
+
if (options.dangerouslySkipPermissions) args.push('--dangerously-skip-permissions')
|
|
521
|
+
|
|
522
|
+
// Merge addDirs and skillsFolders (both feature-level and per-session) into --add-dir
|
|
523
|
+
const addDirs: string[] = [
|
|
524
|
+
...(options.addDirs ?? []),
|
|
525
|
+
...(options.skillsFolders ?? []),
|
|
526
|
+
...(this.options.skillsFolders ?? []),
|
|
527
|
+
]
|
|
528
|
+
if (addDirs.length) {
|
|
529
|
+
args.push('--add-dir', ...addDirs)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// --- New v2.1 flags ---
|
|
533
|
+
const effort = options.effort ?? this.options.effort
|
|
534
|
+
if (effort) args.push('--effort', effort)
|
|
535
|
+
|
|
536
|
+
const maxBudgetUsd = options.maxBudgetUsd ?? this.options.maxBudgetUsd
|
|
537
|
+
if (maxBudgetUsd != null) args.push('--max-budget-usd', String(maxBudgetUsd))
|
|
538
|
+
|
|
539
|
+
const fallbackModel = options.fallbackModel ?? this.options.fallbackModel
|
|
540
|
+
if (fallbackModel) args.push('--fallback-model', fallbackModel)
|
|
541
|
+
|
|
542
|
+
const agent = options.agent ?? this.options.agent
|
|
543
|
+
if (agent) args.push('--agent', agent)
|
|
544
|
+
|
|
545
|
+
const noSessionPersistence = options.noSessionPersistence ?? this.options.noSessionPersistence
|
|
546
|
+
if (noSessionPersistence) args.push('--no-session-persistence')
|
|
547
|
+
|
|
548
|
+
const tools = options.tools ?? this.options.tools
|
|
549
|
+
if (tools?.length) args.push('--tools', ...tools)
|
|
550
|
+
|
|
551
|
+
const strictMcpConfig = options.strictMcpConfig ?? this.options.strictMcpConfig
|
|
552
|
+
if (strictMcpConfig) args.push('--strict-mcp-config')
|
|
553
|
+
|
|
554
|
+
const settingsFile = options.settingsFile ?? this.options.settingsFile
|
|
555
|
+
if (settingsFile) args.push('--settings', settingsFile)
|
|
556
|
+
|
|
557
|
+
// Per-session only flags
|
|
558
|
+
if (options.jsonSchema) {
|
|
559
|
+
const schemaStr = typeof options.jsonSchema === 'string' ? options.jsonSchema : JSON.stringify(options.jsonSchema)
|
|
560
|
+
args.push('--json-schema', schemaStr)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (options.sessionId) args.push('--session-id', options.sessionId)
|
|
564
|
+
if (options.forkSession) args.push('--fork-session')
|
|
565
|
+
|
|
566
|
+
if (options.debug != null) {
|
|
567
|
+
if (typeof options.debug === 'string') {
|
|
568
|
+
args.push('--debug', options.debug)
|
|
569
|
+
} else if (options.debug) {
|
|
570
|
+
args.push('--debug')
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (options.debugFile) args.push('--debug-file', options.debugFile)
|
|
575
|
+
|
|
576
|
+
if (options.extraArgs?.length) {
|
|
577
|
+
args.push(...options.extraArgs)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Prompt is piped via stdin rather than passed as a positional arg,
|
|
581
|
+
// to avoid content like '---' being parsed as CLI flags.
|
|
582
|
+
return args
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Create a unique session ID.
|
|
587
|
+
*
|
|
588
|
+
* @returns {string} A UUID-based session ID
|
|
589
|
+
*/
|
|
590
|
+
private createSessionId(): string {
|
|
591
|
+
return crypto.randomUUID()
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Update a session in state.
|
|
596
|
+
*
|
|
597
|
+
* @param {string} id - The local session ID
|
|
598
|
+
* @param {Partial<ClaudeSession>} update - Fields to merge
|
|
599
|
+
*/
|
|
600
|
+
private updateSession(id: string, update: Partial<ClaudeSession>): void {
|
|
601
|
+
const sessions = { ...this.state.current.sessions }
|
|
602
|
+
const existing = sessions[id]
|
|
603
|
+
if (existing) {
|
|
604
|
+
sessions[id] = { ...existing, ...update }
|
|
605
|
+
this.setState({ sessions })
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Process a parsed JSON event from the Claude CLI stream.
|
|
611
|
+
*
|
|
612
|
+
* @param {string} sessionId - The local session ID
|
|
613
|
+
* @param {ClaudeEvent} event - The parsed event
|
|
614
|
+
*/
|
|
615
|
+
private handleEvent(sessionId: string, event: ClaudeEvent): void {
|
|
616
|
+
this.emit('session:event', { sessionId, event })
|
|
617
|
+
|
|
618
|
+
// File logging
|
|
619
|
+
const logConfig = this.sessionLogPaths.get(sessionId)
|
|
620
|
+
if (logConfig && this.shouldLog(event.type, logConfig.level)) {
|
|
621
|
+
this.writeLogEntry(sessionId, event.type, event)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
switch (event.type) {
|
|
625
|
+
case 'system': {
|
|
626
|
+
const init = event as ClaudeInitEvent
|
|
627
|
+
this.updateSession(sessionId, { sessionId: init.session_id })
|
|
628
|
+
this.emit('session:init', { sessionId, init })
|
|
629
|
+
break
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
case 'stream_event': {
|
|
633
|
+
const streamEvent = event as ClaudeStreamEvent
|
|
634
|
+
if (streamEvent.event.type === 'content_block_delta' && streamEvent.event.delta?.text) {
|
|
635
|
+
this.emit('session:delta', {
|
|
636
|
+
sessionId,
|
|
637
|
+
text: streamEvent.event.delta.text,
|
|
638
|
+
parentToolUseId: streamEvent.parent_tool_use_id
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
this.emit('session:stream', { sessionId, streamEvent })
|
|
642
|
+
break
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
case 'assistant': {
|
|
646
|
+
const msg = event as ClaudeAssistantMessage
|
|
647
|
+
const session = this.state.current.sessions[sessionId]
|
|
648
|
+
if (session) {
|
|
649
|
+
this.updateSession(sessionId, {
|
|
650
|
+
messages: [...session.messages, msg]
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
this.emit('session:message', { sessionId, message: msg })
|
|
654
|
+
break
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
case 'result': {
|
|
658
|
+
const result = event as ClaudeResultEvent
|
|
659
|
+
this.updateSession(sessionId, {
|
|
660
|
+
status: result.is_error ? 'error' : 'completed',
|
|
661
|
+
result: result.result,
|
|
662
|
+
error: result.is_error ? result.result : undefined,
|
|
663
|
+
costUsd: result.total_cost_usd,
|
|
664
|
+
turns: result.num_turns
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
const activeSessions = this.state.current.activeSessions.filter(id => id !== sessionId)
|
|
668
|
+
this.setState({ activeSessions })
|
|
669
|
+
|
|
670
|
+
this.emit('session:result', {
|
|
671
|
+
sessionId,
|
|
672
|
+
result: result.result,
|
|
673
|
+
isError: result.is_error,
|
|
674
|
+
costUsd: result.total_cost_usd,
|
|
675
|
+
turns: result.num_turns,
|
|
676
|
+
durationMs: result.duration_ms
|
|
677
|
+
})
|
|
678
|
+
break
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Run a prompt in a new Claude Code session. Spawns a subprocess,
|
|
685
|
+
* streams NDJSON events, and resolves when the session completes.
|
|
686
|
+
*
|
|
687
|
+
* @param {string} prompt - The instruction/prompt to send
|
|
688
|
+
* @param {RunOptions} [options] - Session configuration overrides
|
|
689
|
+
* @returns {Promise<ClaudeSession>} The completed session with result
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```typescript
|
|
693
|
+
* // Simple one-shot
|
|
694
|
+
* const session = await cc.run('What files are in this project?')
|
|
695
|
+
* console.log(session.result)
|
|
696
|
+
*
|
|
697
|
+
* // With options
|
|
698
|
+
* const session = await cc.run('Refactor the auth module', {
|
|
699
|
+
* model: 'opus',
|
|
700
|
+
* cwd: '/path/to/project',
|
|
701
|
+
* permissionMode: 'acceptEdits',
|
|
702
|
+
* streaming: true
|
|
703
|
+
* })
|
|
704
|
+
*
|
|
705
|
+
* // With injected MCP servers
|
|
706
|
+
* const session = await cc.run('Use the database tools to list tables', {
|
|
707
|
+
* mcpServers: {
|
|
708
|
+
* 'db-tools': { type: 'stdio', command: 'bun', args: ['run', 'db-mcp.ts'] },
|
|
709
|
+
* 'api': { type: 'http', url: 'https://api.example.com/mcp' }
|
|
710
|
+
* }
|
|
711
|
+
* })
|
|
712
|
+
*
|
|
713
|
+
* // Resume a previous session
|
|
714
|
+
* const session = await cc.run('Now add tests for that', {
|
|
715
|
+
* resumeSessionId: previousSession.sessionId
|
|
716
|
+
* })
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
async run(prompt: string, options: RunOptions = {}): Promise<ClaudeSession> {
|
|
720
|
+
const id = this.createSessionId()
|
|
721
|
+
const args = await this.buildArgs(prompt, options)
|
|
722
|
+
const cwd = options.cwd ?? this.options.cwd ?? (this.container as any).cwd
|
|
723
|
+
|
|
724
|
+
// Set up file logging for this session
|
|
725
|
+
const fileLog = this.resolveFileLog(options)
|
|
726
|
+
if (fileLog) {
|
|
727
|
+
this.sessionLogPaths.set(id, fileLog)
|
|
728
|
+
this.writeLogEntry(id, 'session:start', { prompt, cwd, args: [this.claudePath, ...args] })
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const session: ClaudeSession = {
|
|
732
|
+
id,
|
|
733
|
+
status: 'running',
|
|
734
|
+
prompt,
|
|
735
|
+
costUsd: 0,
|
|
736
|
+
turns: 0,
|
|
737
|
+
messages: []
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Register session in state
|
|
741
|
+
const sessions = { ...this.state.current.sessions, [id]: session }
|
|
742
|
+
const activeSessions = [...this.state.current.activeSessions, id]
|
|
743
|
+
this.setState({ sessions, activeSessions })
|
|
744
|
+
|
|
745
|
+
this.emit('session:start', { sessionId: id, prompt })
|
|
746
|
+
|
|
747
|
+
const proc = this.container.feature('proc').spawn(this.claudePath, args, {
|
|
748
|
+
cwd,
|
|
749
|
+
stdout: 'pipe',
|
|
750
|
+
stderr: 'pipe',
|
|
751
|
+
stdin: Buffer.from(prompt),
|
|
752
|
+
environment: { ...process.env },
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
this.updateSession(id, { process: proc })
|
|
756
|
+
await this.consumeStream(id, proc)
|
|
757
|
+
|
|
758
|
+
return this.state.current.sessions[id]!
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Run a prompt without waiting for completion. Returns the session ID
|
|
763
|
+
* immediately so you can subscribe to events.
|
|
764
|
+
*
|
|
765
|
+
* @param {string} prompt - The instruction/prompt to send
|
|
766
|
+
* @param {RunOptions} [options] - Session configuration overrides
|
|
767
|
+
* @returns {string} The session ID to track via events
|
|
768
|
+
*
|
|
769
|
+
* @example
|
|
770
|
+
* ```typescript
|
|
771
|
+
* const sessionId = cc.start('Build a REST API for users')
|
|
772
|
+
*
|
|
773
|
+
* cc.on('session:delta', ({ sessionId: sid, text }) => {
|
|
774
|
+
* if (sid === sessionId) process.stdout.write(text)
|
|
775
|
+
* })
|
|
776
|
+
*
|
|
777
|
+
* cc.on('session:result', ({ sessionId: sid, result }) => {
|
|
778
|
+
* if (sid === sessionId) console.log('\nDone:', result)
|
|
779
|
+
* })
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
782
|
+
async start(prompt: string, options: RunOptions = {}): Promise<string> {
|
|
783
|
+
const id = this.createSessionId()
|
|
784
|
+
const args = await this.buildArgs(prompt, options)
|
|
785
|
+
const cwd = options.cwd ?? this.options.cwd ?? (this.container as any).cwd
|
|
786
|
+
|
|
787
|
+
// Set up file logging for this session
|
|
788
|
+
const fileLog = this.resolveFileLog(options)
|
|
789
|
+
if (fileLog) {
|
|
790
|
+
this.sessionLogPaths.set(id, fileLog)
|
|
791
|
+
this.writeLogEntry(id, 'session:start', { prompt, cwd, args: [this.claudePath, ...args] })
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const session: ClaudeSession = {
|
|
795
|
+
id,
|
|
796
|
+
status: 'running',
|
|
797
|
+
prompt,
|
|
798
|
+
costUsd: 0,
|
|
799
|
+
turns: 0,
|
|
800
|
+
messages: []
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const sessions = { ...this.state.current.sessions, [id]: session }
|
|
804
|
+
const activeSessions = [...this.state.current.activeSessions, id]
|
|
805
|
+
this.setState({ sessions, activeSessions })
|
|
806
|
+
|
|
807
|
+
this.emit('session:start', { sessionId: id, prompt })
|
|
808
|
+
|
|
809
|
+
const proc = this.container.feature('proc').spawn(this.claudePath, args, {
|
|
810
|
+
cwd,
|
|
811
|
+
stdout: 'pipe',
|
|
812
|
+
stderr: 'pipe',
|
|
813
|
+
stdin: Buffer.from(prompt),
|
|
814
|
+
environment: { ...process.env },
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
this.updateSession(id, { process: proc })
|
|
818
|
+
|
|
819
|
+
// Process in background
|
|
820
|
+
this.consumeStream(id, proc)
|
|
821
|
+
|
|
822
|
+
return id
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Consume the stdout stream of a running process in the background.
|
|
827
|
+
*
|
|
828
|
+
* @param {string} sessionId - The local session ID
|
|
829
|
+
* @param {any} proc - The process handle returned by features.proc.spawn()
|
|
830
|
+
*/
|
|
831
|
+
private async consumeStream(sessionId: string, proc: any): Promise<void> {
|
|
832
|
+
if (!proc?.stdout || !proc?.stderr) {
|
|
833
|
+
const error = 'Process streams are not available'
|
|
834
|
+
this.updateSession(sessionId, { status: 'error', error })
|
|
835
|
+
this.emit('session:error', { sessionId, error })
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
let buffer = ''
|
|
840
|
+
let stderr = ''
|
|
841
|
+
|
|
842
|
+
proc.stderr.on('data', (chunk: Buffer | string) => {
|
|
843
|
+
stderr += Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk)
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
const stdoutDone = new Promise<void>((resolve, reject) => {
|
|
847
|
+
proc.stdout.on('data', (chunk: Buffer | string) => {
|
|
848
|
+
buffer += Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk)
|
|
849
|
+
const lines = buffer.split('\n')
|
|
850
|
+
buffer = lines.pop() || ''
|
|
851
|
+
|
|
852
|
+
for (const line of lines) {
|
|
853
|
+
const trimmed = line.trim()
|
|
854
|
+
if (!trimmed) continue
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
const event = JSON.parse(trimmed) as ClaudeEvent
|
|
858
|
+
this.handleEvent(sessionId, event)
|
|
859
|
+
} catch {
|
|
860
|
+
this.emit('session:parse-error', { sessionId, line: trimmed })
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
proc.stdout.on('end', () => {
|
|
866
|
+
if (buffer.trim()) {
|
|
867
|
+
try {
|
|
868
|
+
const event = JSON.parse(buffer.trim()) as ClaudeEvent
|
|
869
|
+
this.handleEvent(sessionId, event)
|
|
870
|
+
} catch {
|
|
871
|
+
// ignore trailing partial data
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
resolve()
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
proc.stdout.on('error', reject)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
const exitCodePromise = new Promise<number>((resolve, reject) => {
|
|
881
|
+
proc.once('error', reject)
|
|
882
|
+
proc.once('close', (code: number | null) => resolve(code ?? 0))
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
await stdoutDone
|
|
887
|
+
} catch (err) {
|
|
888
|
+
this.updateSession(sessionId, {
|
|
889
|
+
status: 'error',
|
|
890
|
+
error: err instanceof Error ? err.message : String(err)
|
|
891
|
+
})
|
|
892
|
+
this.emit('session:error', { sessionId, error: err })
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
let exitCode = 1
|
|
896
|
+
try {
|
|
897
|
+
exitCode = await exitCodePromise
|
|
898
|
+
} catch (err) {
|
|
899
|
+
this.updateSession(sessionId, {
|
|
900
|
+
status: 'error',
|
|
901
|
+
error: err instanceof Error ? err.message : String(err),
|
|
902
|
+
})
|
|
903
|
+
this.emit('session:error', { sessionId, error: err })
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (exitCode !== 0 && this.state.current.sessions[sessionId]?.status !== 'completed') {
|
|
907
|
+
this.updateSession(sessionId, {
|
|
908
|
+
status: 'error',
|
|
909
|
+
error: stderr || `Process exited with code ${exitCode}`
|
|
910
|
+
})
|
|
911
|
+
this.emit('session:error', { sessionId, error: stderr, exitCode })
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Finalize file log
|
|
915
|
+
if (this.sessionLogPaths.has(sessionId)) {
|
|
916
|
+
const finalSession = this.state.current.sessions[sessionId]!
|
|
917
|
+
await this.writeLogEntry(sessionId, 'session:end', {
|
|
918
|
+
status: finalSession.status,
|
|
919
|
+
result: finalSession.result,
|
|
920
|
+
error: finalSession.error,
|
|
921
|
+
costUsd: finalSession.costUsd,
|
|
922
|
+
turns: finalSession.turns,
|
|
923
|
+
messageCount: finalSession.messages.length
|
|
924
|
+
})
|
|
925
|
+
this.sessionLogPaths.delete(sessionId)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Kill a running session's subprocess.
|
|
931
|
+
*
|
|
932
|
+
* @param {string} sessionId - The local session ID to abort
|
|
933
|
+
*
|
|
934
|
+
* @example
|
|
935
|
+
* ```typescript
|
|
936
|
+
* const sessionId = cc.start('Do something long')
|
|
937
|
+
* // ... later
|
|
938
|
+
* cc.abort(sessionId)
|
|
939
|
+
* ```
|
|
940
|
+
*/
|
|
941
|
+
abort(sessionId: string): void {
|
|
942
|
+
const session = this.state.current.sessions[sessionId]
|
|
943
|
+
if (session?.process && session.status === 'running') {
|
|
944
|
+
session.process.kill()
|
|
945
|
+
this.updateSession(sessionId, { status: 'error', error: 'Aborted by user' })
|
|
946
|
+
const activeSessions = this.state.current.activeSessions.filter(id => id !== sessionId)
|
|
947
|
+
this.setState({ activeSessions })
|
|
948
|
+
this.emit('session:abort', { sessionId })
|
|
949
|
+
|
|
950
|
+
if (this.sessionLogPaths.has(sessionId)) {
|
|
951
|
+
this.writeLogEntry(sessionId, 'session:abort', { reason: 'Aborted by user' })
|
|
952
|
+
this.sessionLogPaths.delete(sessionId)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Get a session by its local ID.
|
|
959
|
+
*
|
|
960
|
+
* @param {string} sessionId - The local session ID
|
|
961
|
+
* @returns {ClaudeSession | undefined} The session if it exists
|
|
962
|
+
*
|
|
963
|
+
* @example
|
|
964
|
+
* ```typescript
|
|
965
|
+
* const session = cc.getSession(sessionId)
|
|
966
|
+
* if (session?.status === 'completed') {
|
|
967
|
+
* console.log(session.result)
|
|
968
|
+
* }
|
|
969
|
+
* ```
|
|
970
|
+
*/
|
|
971
|
+
getSession(sessionId: string): ClaudeSession | undefined {
|
|
972
|
+
return this.state.current.sessions[sessionId]
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Wait for a running session to complete.
|
|
977
|
+
*
|
|
978
|
+
* @param {string} sessionId - The local session ID
|
|
979
|
+
* @returns {Promise<ClaudeSession>} The completed session
|
|
980
|
+
*
|
|
981
|
+
* @example
|
|
982
|
+
* ```typescript
|
|
983
|
+
* const id = cc.start('Build something cool')
|
|
984
|
+
* const session = await cc.waitForSession(id)
|
|
985
|
+
* console.log(session.result)
|
|
986
|
+
* ```
|
|
987
|
+
*/
|
|
988
|
+
async waitForSession(sessionId: string): Promise<ClaudeSession> {
|
|
989
|
+
const session = this.state.current.sessions[sessionId]
|
|
990
|
+
if (!session) throw new Error(`Session ${sessionId} not found`)
|
|
991
|
+
if (session.status === 'completed' || session.status === 'error') return session
|
|
992
|
+
|
|
993
|
+
return new Promise((resolve) => {
|
|
994
|
+
const handler = (data: { sessionId: string }) => {
|
|
995
|
+
if (data.sessionId === sessionId) {
|
|
996
|
+
this.off('session:result')
|
|
997
|
+
this.off('session:error')
|
|
998
|
+
resolve(this.state.current.sessions[sessionId]!)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
this.on('session:result', handler)
|
|
1002
|
+
this.on('session:error', handler)
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Get aggregated usage statistics across all sessions, or for a specific session.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} [sessionId] - Optional session ID to get usage for a single session
|
|
1010
|
+
* @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
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* ```typescript
|
|
1014
|
+
* const stats = cc.usage()
|
|
1015
|
+
* console.log(`Total cost: $${stats.totalCostUsd.toFixed(4)}`)
|
|
1016
|
+
* console.log(`Tokens: ${stats.totalInputTokens} in / ${stats.totalOutputTokens} out`)
|
|
1017
|
+
*
|
|
1018
|
+
* // Single session
|
|
1019
|
+
* const sessionStats = cc.usage(sessionId)
|
|
1020
|
+
* ```
|
|
1021
|
+
*/
|
|
1022
|
+
usage(sessionId?: string) {
|
|
1023
|
+
const allSessions = this.state.current.sessions
|
|
1024
|
+
const entries = sessionId
|
|
1025
|
+
? (allSessions[sessionId] ? [allSessions[sessionId]] : [])
|
|
1026
|
+
: Object.values(allSessions)
|
|
1027
|
+
|
|
1028
|
+
let totalCostUsd = 0
|
|
1029
|
+
let totalInputTokens = 0
|
|
1030
|
+
let totalOutputTokens = 0
|
|
1031
|
+
let totalCacheReadTokens = 0
|
|
1032
|
+
let totalCacheCreationTokens = 0
|
|
1033
|
+
let totalTurns = 0
|
|
1034
|
+
const sessions: Array<{ id: string; costUsd: number; turns: number; inputTokens: number; outputTokens: number; status: string }> = []
|
|
1035
|
+
|
|
1036
|
+
for (const session of entries as ClaudeSession[]) {
|
|
1037
|
+
let inputTokens = 0
|
|
1038
|
+
let outputTokens = 0
|
|
1039
|
+
let cacheRead = 0
|
|
1040
|
+
let cacheCreation = 0
|
|
1041
|
+
|
|
1042
|
+
for (const msg of session.messages || []) {
|
|
1043
|
+
const u = msg.message?.usage
|
|
1044
|
+
if (u) {
|
|
1045
|
+
inputTokens += u.input_tokens || 0
|
|
1046
|
+
outputTokens += u.output_tokens || 0
|
|
1047
|
+
cacheRead += u.cache_read_input_tokens || 0
|
|
1048
|
+
cacheCreation += u.cache_creation_input_tokens || 0
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
totalCostUsd += session.costUsd || 0
|
|
1053
|
+
totalInputTokens += inputTokens
|
|
1054
|
+
totalOutputTokens += outputTokens
|
|
1055
|
+
totalCacheReadTokens += cacheRead
|
|
1056
|
+
totalCacheCreationTokens += cacheCreation
|
|
1057
|
+
totalTurns += session.turns || 0
|
|
1058
|
+
|
|
1059
|
+
sessions.push({
|
|
1060
|
+
id: session.id,
|
|
1061
|
+
costUsd: session.costUsd || 0,
|
|
1062
|
+
turns: session.turns || 0,
|
|
1063
|
+
inputTokens,
|
|
1064
|
+
outputTokens,
|
|
1065
|
+
status: session.status,
|
|
1066
|
+
})
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Budget remaining: if maxBudgetUsd is configured, compute what's left
|
|
1070
|
+
const maxBudgetUsd = this.options.maxBudgetUsd
|
|
1071
|
+
const budgetRemainingUsd = maxBudgetUsd != null ? Math.max(0, maxBudgetUsd - totalCostUsd) : undefined
|
|
1072
|
+
const budgetUsedPercent = maxBudgetUsd != null && maxBudgetUsd > 0 ? Math.min(100, (totalCostUsd / maxBudgetUsd) * 100) : undefined
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
totalCostUsd,
|
|
1076
|
+
totalInputTokens,
|
|
1077
|
+
totalOutputTokens,
|
|
1078
|
+
totalCacheReadTokens,
|
|
1079
|
+
totalCacheCreationTokens,
|
|
1080
|
+
totalTurns,
|
|
1081
|
+
sessionCount: sessions.length,
|
|
1082
|
+
maxBudgetUsd: maxBudgetUsd ?? null,
|
|
1083
|
+
budgetRemainingUsd: budgetRemainingUsd ?? null,
|
|
1084
|
+
budgetUsedPercent: budgetUsedPercent ?? null,
|
|
1085
|
+
sessions,
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Clean up any temp MCP config files created during sessions.
|
|
1091
|
+
*/
|
|
1092
|
+
async cleanupMcpTempFiles(): Promise<void> {
|
|
1093
|
+
for (const path of this.mcpTempFiles) {
|
|
1094
|
+
try { await this.container.feature('fs').rm(path) } catch { /* already gone */ }
|
|
1095
|
+
}
|
|
1096
|
+
this.mcpTempFiles = []
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Initialize the feature.
|
|
1101
|
+
*
|
|
1102
|
+
* @param {any} [options] - Enable options
|
|
1103
|
+
* @returns {Promise<this>} The enabled feature
|
|
1104
|
+
*/
|
|
1105
|
+
override async enable(options: any = {}): Promise<this> {
|
|
1106
|
+
await super.enable(options)
|
|
1107
|
+
return this
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export default features.register('claudeCode', ClaudeCode)
|