luca 2.0.0 → 3.0.2

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