@soederpop/luca 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,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)