lorenz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (630) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +13 -0
  3. package/README.md +774 -0
  4. package/RELEASE-MANIFEST.json +211 -0
  5. package/apps/cli/bin/lorenz.js +25 -0
  6. package/apps/cli/dist/bin/cli.d.ts +3 -0
  7. package/apps/cli/dist/bin/cli.d.ts.map +1 -0
  8. package/apps/cli/dist/bin/cli.js +4 -0
  9. package/apps/cli/dist/bin/cli.js.map +1 -0
  10. package/apps/cli/dist/daemon.d.ts +76 -0
  11. package/apps/cli/dist/daemon.d.ts.map +1 -0
  12. package/apps/cli/dist/daemon.js +189 -0
  13. package/apps/cli/dist/daemon.js.map +1 -0
  14. package/apps/cli/dist/doctor.d.ts +40 -0
  15. package/apps/cli/dist/doctor.d.ts.map +1 -0
  16. package/apps/cli/dist/doctor.js +590 -0
  17. package/apps/cli/dist/doctor.js.map +1 -0
  18. package/apps/cli/dist/index.d.ts +32 -0
  19. package/apps/cli/dist/index.d.ts.map +1 -0
  20. package/apps/cli/dist/index.js +26 -0
  21. package/apps/cli/dist/index.js.map +1 -0
  22. package/apps/cli/dist/main.d.ts +40 -0
  23. package/apps/cli/dist/main.d.ts.map +1 -0
  24. package/apps/cli/dist/main.js +259 -0
  25. package/apps/cli/dist/main.js.map +1 -0
  26. package/apps/cli/dist/runs.d.ts +31 -0
  27. package/apps/cli/dist/runs.d.ts.map +1 -0
  28. package/apps/cli/dist/runs.js +281 -0
  29. package/apps/cli/dist/runs.js.map +1 -0
  30. package/apps/cli/dist/workerDriverLoader.d.ts +64 -0
  31. package/apps/cli/dist/workerDriverLoader.d.ts.map +1 -0
  32. package/apps/cli/dist/workerDriverLoader.js +211 -0
  33. package/apps/cli/dist/workerDriverLoader.js.map +1 -0
  34. package/apps/cli/package.json +57 -0
  35. package/apps/symphony-dashboard/dist/assets/index-B3owF3jd.css +1 -0
  36. package/apps/symphony-dashboard/dist/assets/index-DQ6XlL0d.js +227 -0
  37. package/apps/symphony-dashboard/dist/index.html +18 -0
  38. package/bin/lorenz +16 -0
  39. package/extensions/docker-worker/dist/index.d.ts +92 -0
  40. package/extensions/docker-worker/dist/index.d.ts.map +1 -0
  41. package/extensions/docker-worker/dist/index.js +283 -0
  42. package/extensions/docker-worker/dist/index.js.map +1 -0
  43. package/extensions/docker-worker/package.json +14 -0
  44. package/extensions/jira-tracker/dist/client.d.ts +50 -0
  45. package/extensions/jira-tracker/dist/client.d.ts.map +1 -0
  46. package/extensions/jira-tracker/dist/client.js +619 -0
  47. package/extensions/jira-tracker/dist/client.js.map +1 -0
  48. package/extensions/jira-tracker/dist/index.d.ts +5 -0
  49. package/extensions/jira-tracker/dist/index.d.ts.map +1 -0
  50. package/extensions/jira-tracker/dist/index.js +5 -0
  51. package/extensions/jira-tracker/dist/index.js.map +1 -0
  52. package/extensions/jira-tracker/dist/options.d.ts +38 -0
  53. package/extensions/jira-tracker/dist/options.d.ts.map +1 -0
  54. package/extensions/jira-tracker/dist/options.js +61 -0
  55. package/extensions/jira-tracker/dist/options.js.map +1 -0
  56. package/extensions/jira-tracker/dist/provider.d.ts +6 -0
  57. package/extensions/jira-tracker/dist/provider.d.ts.map +1 -0
  58. package/extensions/jira-tracker/dist/provider.js +178 -0
  59. package/extensions/jira-tracker/dist/provider.js.map +1 -0
  60. package/extensions/jira-tracker/dist/register.d.ts +10 -0
  61. package/extensions/jira-tracker/dist/register.d.ts.map +1 -0
  62. package/extensions/jira-tracker/dist/register.js +15 -0
  63. package/extensions/jira-tracker/dist/register.js.map +1 -0
  64. package/extensions/jira-tracker/package.json +16 -0
  65. package/extensions/linear-tracker/dist/client.d.ts +82 -0
  66. package/extensions/linear-tracker/dist/client.d.ts.map +1 -0
  67. package/extensions/linear-tracker/dist/client.js +622 -0
  68. package/extensions/linear-tracker/dist/client.js.map +1 -0
  69. package/extensions/linear-tracker/dist/index.d.ts +8 -0
  70. package/extensions/linear-tracker/dist/index.d.ts.map +1 -0
  71. package/extensions/linear-tracker/dist/index.js +7 -0
  72. package/extensions/linear-tracker/dist/index.js.map +1 -0
  73. package/extensions/linear-tracker/dist/options.d.ts +32 -0
  74. package/extensions/linear-tracker/dist/options.d.ts.map +1 -0
  75. package/extensions/linear-tracker/dist/options.js +59 -0
  76. package/extensions/linear-tracker/dist/options.js.map +1 -0
  77. package/extensions/linear-tracker/dist/provider.d.ts +4 -0
  78. package/extensions/linear-tracker/dist/provider.d.ts.map +1 -0
  79. package/extensions/linear-tracker/dist/provider.js +58 -0
  80. package/extensions/linear-tracker/dist/provider.js.map +1 -0
  81. package/extensions/linear-tracker/dist/register.d.ts +11 -0
  82. package/extensions/linear-tracker/dist/register.d.ts.map +1 -0
  83. package/extensions/linear-tracker/dist/register.js +19 -0
  84. package/extensions/linear-tracker/dist/register.js.map +1 -0
  85. package/extensions/linear-tracker/dist/toolOps.d.ts +8 -0
  86. package/extensions/linear-tracker/dist/toolOps.d.ts.map +1 -0
  87. package/extensions/linear-tracker/dist/toolOps.js +160 -0
  88. package/extensions/linear-tracker/dist/toolOps.js.map +1 -0
  89. package/extensions/linear-tracker/dist/tools.d.ts +7 -0
  90. package/extensions/linear-tracker/dist/tools.d.ts.map +1 -0
  91. package/extensions/linear-tracker/dist/tools.js +210 -0
  92. package/extensions/linear-tracker/dist/tools.js.map +1 -0
  93. package/extensions/linear-tracker/package.json +18 -0
  94. package/extensions/local-tracker/dist/boardStore.d.ts +116 -0
  95. package/extensions/local-tracker/dist/boardStore.d.ts.map +1 -0
  96. package/extensions/local-tracker/dist/boardStore.js +475 -0
  97. package/extensions/local-tracker/dist/boardStore.js.map +1 -0
  98. package/extensions/local-tracker/dist/client.d.ts +14 -0
  99. package/extensions/local-tracker/dist/client.d.ts.map +1 -0
  100. package/extensions/local-tracker/dist/client.js +27 -0
  101. package/extensions/local-tracker/dist/client.js.map +1 -0
  102. package/extensions/local-tracker/dist/index.d.ts +7 -0
  103. package/extensions/local-tracker/dist/index.d.ts.map +1 -0
  104. package/extensions/local-tracker/dist/index.js +7 -0
  105. package/extensions/local-tracker/dist/index.js.map +1 -0
  106. package/extensions/local-tracker/dist/options.d.ts +31 -0
  107. package/extensions/local-tracker/dist/options.d.ts.map +1 -0
  108. package/extensions/local-tracker/dist/options.js +69 -0
  109. package/extensions/local-tracker/dist/options.js.map +1 -0
  110. package/extensions/local-tracker/dist/provider.d.ts +9 -0
  111. package/extensions/local-tracker/dist/provider.d.ts.map +1 -0
  112. package/extensions/local-tracker/dist/provider.js +35 -0
  113. package/extensions/local-tracker/dist/provider.js.map +1 -0
  114. package/extensions/local-tracker/dist/register.d.ts +11 -0
  115. package/extensions/local-tracker/dist/register.d.ts.map +1 -0
  116. package/extensions/local-tracker/dist/register.js +19 -0
  117. package/extensions/local-tracker/dist/register.js.map +1 -0
  118. package/extensions/local-tracker/dist/resolveBoardDir.d.ts +24 -0
  119. package/extensions/local-tracker/dist/resolveBoardDir.d.ts.map +1 -0
  120. package/extensions/local-tracker/dist/resolveBoardDir.js +39 -0
  121. package/extensions/local-tracker/dist/resolveBoardDir.js.map +1 -0
  122. package/extensions/local-tracker/dist/toolOps.d.ts +9 -0
  123. package/extensions/local-tracker/dist/toolOps.d.ts.map +1 -0
  124. package/extensions/local-tracker/dist/toolOps.js +86 -0
  125. package/extensions/local-tracker/dist/toolOps.js.map +1 -0
  126. package/extensions/local-tracker/dist/tools.d.ts +7 -0
  127. package/extensions/local-tracker/dist/tools.d.ts.map +1 -0
  128. package/extensions/local-tracker/dist/tools.js +170 -0
  129. package/extensions/local-tracker/dist/tools.js.map +1 -0
  130. package/extensions/local-tracker/package.json +18 -0
  131. package/extensions/memory-tracker/dist/index.d.ts +24 -0
  132. package/extensions/memory-tracker/dist/index.d.ts.map +1 -0
  133. package/extensions/memory-tracker/dist/index.js +110 -0
  134. package/extensions/memory-tracker/dist/index.js.map +1 -0
  135. package/extensions/memory-tracker/package.json +16 -0
  136. package/extensions/slack-tracker/dist/client.d.ts +88 -0
  137. package/extensions/slack-tracker/dist/client.d.ts.map +1 -0
  138. package/extensions/slack-tracker/dist/client.js +246 -0
  139. package/extensions/slack-tracker/dist/client.js.map +1 -0
  140. package/extensions/slack-tracker/dist/inMemoryTransport.d.ts +42 -0
  141. package/extensions/slack-tracker/dist/inMemoryTransport.d.ts.map +1 -0
  142. package/extensions/slack-tracker/dist/inMemoryTransport.js +104 -0
  143. package/extensions/slack-tracker/dist/inMemoryTransport.js.map +1 -0
  144. package/extensions/slack-tracker/dist/index.d.ts +15 -0
  145. package/extensions/slack-tracker/dist/index.d.ts.map +1 -0
  146. package/extensions/slack-tracker/dist/index.js +11 -0
  147. package/extensions/slack-tracker/dist/index.js.map +1 -0
  148. package/extensions/slack-tracker/dist/mapping.d.ts +27 -0
  149. package/extensions/slack-tracker/dist/mapping.d.ts.map +1 -0
  150. package/extensions/slack-tracker/dist/mapping.js +109 -0
  151. package/extensions/slack-tracker/dist/mapping.js.map +1 -0
  152. package/extensions/slack-tracker/dist/operations.d.ts +41 -0
  153. package/extensions/slack-tracker/dist/operations.d.ts.map +1 -0
  154. package/extensions/slack-tracker/dist/operations.js +97 -0
  155. package/extensions/slack-tracker/dist/operations.js.map +1 -0
  156. package/extensions/slack-tracker/dist/options.d.ts +30 -0
  157. package/extensions/slack-tracker/dist/options.d.ts.map +1 -0
  158. package/extensions/slack-tracker/dist/options.js +49 -0
  159. package/extensions/slack-tracker/dist/options.js.map +1 -0
  160. package/extensions/slack-tracker/dist/provider.d.ts +9 -0
  161. package/extensions/slack-tracker/dist/provider.d.ts.map +1 -0
  162. package/extensions/slack-tracker/dist/provider.js +74 -0
  163. package/extensions/slack-tracker/dist/provider.js.map +1 -0
  164. package/extensions/slack-tracker/dist/register.d.ts +11 -0
  165. package/extensions/slack-tracker/dist/register.d.ts.map +1 -0
  166. package/extensions/slack-tracker/dist/register.js +19 -0
  167. package/extensions/slack-tracker/dist/register.js.map +1 -0
  168. package/extensions/slack-tracker/dist/threadState.d.ts +52 -0
  169. package/extensions/slack-tracker/dist/threadState.d.ts.map +1 -0
  170. package/extensions/slack-tracker/dist/threadState.js +192 -0
  171. package/extensions/slack-tracker/dist/threadState.js.map +1 -0
  172. package/extensions/slack-tracker/dist/toolOps.d.ts +13 -0
  173. package/extensions/slack-tracker/dist/toolOps.d.ts.map +1 -0
  174. package/extensions/slack-tracker/dist/toolOps.js +76 -0
  175. package/extensions/slack-tracker/dist/toolOps.js.map +1 -0
  176. package/extensions/slack-tracker/dist/tools.d.ts +8 -0
  177. package/extensions/slack-tracker/dist/tools.d.ts.map +1 -0
  178. package/extensions/slack-tracker/dist/tools.js +266 -0
  179. package/extensions/slack-tracker/dist/tools.js.map +1 -0
  180. package/extensions/slack-tracker/dist/transport.d.ts +63 -0
  181. package/extensions/slack-tracker/dist/transport.d.ts.map +1 -0
  182. package/extensions/slack-tracker/dist/transport.js +2 -0
  183. package/extensions/slack-tracker/dist/transport.js.map +1 -0
  184. package/extensions/slack-tracker/dist/webTransport.d.ts +44 -0
  185. package/extensions/slack-tracker/dist/webTransport.d.ts.map +1 -0
  186. package/extensions/slack-tracker/dist/webTransport.js +402 -0
  187. package/extensions/slack-tracker/dist/webTransport.js.map +1 -0
  188. package/extensions/slack-tracker/package.json +17 -0
  189. package/package.json +89 -0
  190. package/packages/acp/dist/childProcess.d.ts +4 -0
  191. package/packages/acp/dist/childProcess.d.ts.map +1 -0
  192. package/packages/acp/dist/childProcess.js +33 -0
  193. package/packages/acp/dist/childProcess.js.map +1 -0
  194. package/packages/acp/dist/index.d.ts +70 -0
  195. package/packages/acp/dist/index.d.ts.map +1 -0
  196. package/packages/acp/dist/index.js +701 -0
  197. package/packages/acp/dist/index.js.map +1 -0
  198. package/packages/acp/dist/options.d.ts +24 -0
  199. package/packages/acp/dist/options.d.ts.map +1 -0
  200. package/packages/acp/dist/options.js +92 -0
  201. package/packages/acp/dist/options.js.map +1 -0
  202. package/packages/acp/dist/toml.d.ts +2 -0
  203. package/packages/acp/dist/toml.d.ts.map +1 -0
  204. package/packages/acp/dist/toml.js +51 -0
  205. package/packages/acp/dist/toml.js.map +1 -0
  206. package/packages/acp/package.json +24 -0
  207. package/packages/agent-runner/dist/index.d.ts +58 -0
  208. package/packages/agent-runner/dist/index.d.ts.map +1 -0
  209. package/packages/agent-runner/dist/index.js +288 -0
  210. package/packages/agent-runner/dist/index.js.map +1 -0
  211. package/packages/agent-runner/package.json +19 -0
  212. package/packages/agent-sdk/dist/index.d.ts +2 -0
  213. package/packages/agent-sdk/dist/index.d.ts.map +1 -0
  214. package/packages/agent-sdk/dist/index.js +2 -0
  215. package/packages/agent-sdk/dist/index.js.map +1 -0
  216. package/packages/agent-sdk/dist/provider.d.ts +66 -0
  217. package/packages/agent-sdk/dist/provider.d.ts.map +1 -0
  218. package/packages/agent-sdk/dist/provider.js +38 -0
  219. package/packages/agent-sdk/dist/provider.js.map +1 -0
  220. package/packages/agent-sdk/package.json +14 -0
  221. package/packages/cli-kit/dist/index.d.ts +20 -0
  222. package/packages/cli-kit/dist/index.d.ts.map +1 -0
  223. package/packages/cli-kit/dist/index.js +72 -0
  224. package/packages/cli-kit/dist/index.js.map +1 -0
  225. package/packages/cli-kit/package.json +14 -0
  226. package/packages/config/dist/aliases.d.ts +10 -0
  227. package/packages/config/dist/aliases.d.ts.map +1 -0
  228. package/packages/config/dist/aliases.js +153 -0
  229. package/packages/config/dist/aliases.js.map +1 -0
  230. package/packages/config/dist/defaults.d.ts +12 -0
  231. package/packages/config/dist/defaults.d.ts.map +1 -0
  232. package/packages/config/dist/defaults.js +78 -0
  233. package/packages/config/dist/defaults.js.map +1 -0
  234. package/packages/config/dist/errors.d.ts +3 -0
  235. package/packages/config/dist/errors.d.ts.map +1 -0
  236. package/packages/config/dist/errors.js +56 -0
  237. package/packages/config/dist/errors.js.map +1 -0
  238. package/packages/config/dist/index.d.ts +5 -0
  239. package/packages/config/dist/index.d.ts.map +1 -0
  240. package/packages/config/dist/index.js +4 -0
  241. package/packages/config/dist/index.js.map +1 -0
  242. package/packages/config/dist/leaf-utils.d.ts +3 -0
  243. package/packages/config/dist/leaf-utils.d.ts.map +1 -0
  244. package/packages/config/dist/leaf-utils.js +9 -0
  245. package/packages/config/dist/leaf-utils.js.map +1 -0
  246. package/packages/config/dist/parse.d.ts +11 -0
  247. package/packages/config/dist/parse.d.ts.map +1 -0
  248. package/packages/config/dist/parse.js +821 -0
  249. package/packages/config/dist/parse.js.map +1 -0
  250. package/packages/config/dist/schemas.d.ts +214 -0
  251. package/packages/config/dist/schemas.d.ts.map +1 -0
  252. package/packages/config/dist/schemas.js +248 -0
  253. package/packages/config/dist/schemas.js.map +1 -0
  254. package/packages/config/package.json +19 -0
  255. package/packages/dispatch/dist/index.d.ts +22 -0
  256. package/packages/dispatch/dist/index.d.ts.map +1 -0
  257. package/packages/dispatch/dist/index.js +117 -0
  258. package/packages/dispatch/dist/index.js.map +1 -0
  259. package/packages/dispatch/package.json +16 -0
  260. package/packages/dispatch-coordinator/dist/coordinator.d.ts +158 -0
  261. package/packages/dispatch-coordinator/dist/coordinator.d.ts.map +1 -0
  262. package/packages/dispatch-coordinator/dist/coordinator.js +529 -0
  263. package/packages/dispatch-coordinator/dist/coordinator.js.map +1 -0
  264. package/packages/dispatch-coordinator/dist/gate.d.ts +24 -0
  265. package/packages/dispatch-coordinator/dist/gate.d.ts.map +1 -0
  266. package/packages/dispatch-coordinator/dist/gate.js +47 -0
  267. package/packages/dispatch-coordinator/dist/gate.js.map +1 -0
  268. package/packages/dispatch-coordinator/dist/index.d.ts +6 -0
  269. package/packages/dispatch-coordinator/dist/index.d.ts.map +1 -0
  270. package/packages/dispatch-coordinator/dist/index.js +16 -0
  271. package/packages/dispatch-coordinator/dist/index.js.map +1 -0
  272. package/packages/dispatch-coordinator/dist/mcpEndpointManager.d.ts +28 -0
  273. package/packages/dispatch-coordinator/dist/mcpEndpointManager.d.ts.map +1 -0
  274. package/packages/dispatch-coordinator/dist/mcpEndpointManager.js +54 -0
  275. package/packages/dispatch-coordinator/dist/mcpEndpointManager.js.map +1 -0
  276. package/packages/dispatch-coordinator/dist/nullEndpointManager.d.ts +18 -0
  277. package/packages/dispatch-coordinator/dist/nullEndpointManager.d.ts.map +1 -0
  278. package/packages/dispatch-coordinator/dist/nullEndpointManager.js +40 -0
  279. package/packages/dispatch-coordinator/dist/nullEndpointManager.js.map +1 -0
  280. package/packages/dispatch-coordinator/dist/types.d.ts +119 -0
  281. package/packages/dispatch-coordinator/dist/types.d.ts.map +1 -0
  282. package/packages/dispatch-coordinator/dist/types.js +17 -0
  283. package/packages/dispatch-coordinator/dist/types.js.map +1 -0
  284. package/packages/dispatch-coordinator/package.json +16 -0
  285. package/packages/domain/dist/index.d.ts +775 -0
  286. package/packages/domain/dist/index.d.ts.map +1 -0
  287. package/packages/domain/dist/index.js +124 -0
  288. package/packages/domain/dist/index.js.map +1 -0
  289. package/packages/domain/package.json +14 -0
  290. package/packages/humanize/dist/index.d.ts +4 -0
  291. package/packages/humanize/dist/index.d.ts.map +1 -0
  292. package/packages/humanize/dist/index.js +347 -0
  293. package/packages/humanize/dist/index.js.map +1 -0
  294. package/packages/humanize/package.json +11 -0
  295. package/packages/issue/dist/index.d.ts +7 -0
  296. package/packages/issue/dist/index.d.ts.map +1 -0
  297. package/packages/issue/dist/index.js +147 -0
  298. package/packages/issue/dist/index.js.map +1 -0
  299. package/packages/issue/package.json +14 -0
  300. package/packages/log-file/dist/index.d.ts +10 -0
  301. package/packages/log-file/dist/index.d.ts.map +1 -0
  302. package/packages/log-file/dist/index.js +200 -0
  303. package/packages/log-file/dist/index.js.map +1 -0
  304. package/packages/log-file/package.json +15 -0
  305. package/packages/mcp/dist/agentEndpoint.d.ts +31 -0
  306. package/packages/mcp/dist/agentEndpoint.d.ts.map +1 -0
  307. package/packages/mcp/dist/agentEndpoint.js +270 -0
  308. package/packages/mcp/dist/agentEndpoint.js.map +1 -0
  309. package/packages/mcp/dist/auth.d.ts +7 -0
  310. package/packages/mcp/dist/auth.d.ts.map +1 -0
  311. package/packages/mcp/dist/auth.js +48 -0
  312. package/packages/mcp/dist/auth.js.map +1 -0
  313. package/packages/mcp/dist/filter.d.ts +70 -0
  314. package/packages/mcp/dist/filter.d.ts.map +1 -0
  315. package/packages/mcp/dist/filter.js +231 -0
  316. package/packages/mcp/dist/filter.js.map +1 -0
  317. package/packages/mcp/dist/index.d.ts +7 -0
  318. package/packages/mcp/dist/index.d.ts.map +1 -0
  319. package/packages/mcp/dist/index.js +5 -0
  320. package/packages/mcp/dist/index.js.map +1 -0
  321. package/packages/mcp/dist/server.d.ts +31 -0
  322. package/packages/mcp/dist/server.d.ts.map +1 -0
  323. package/packages/mcp/dist/server.js +176 -0
  324. package/packages/mcp/dist/server.js.map +1 -0
  325. package/packages/mcp/dist/tools/linear.d.ts +5 -0
  326. package/packages/mcp/dist/tools/linear.d.ts.map +1 -0
  327. package/packages/mcp/dist/tools/linear.js +192 -0
  328. package/packages/mcp/dist/tools/linear.js.map +1 -0
  329. package/packages/mcp/dist/tools/local.d.ts +5 -0
  330. package/packages/mcp/dist/tools/local.d.ts.map +1 -0
  331. package/packages/mcp/dist/tools/local.js +161 -0
  332. package/packages/mcp/dist/tools/local.js.map +1 -0
  333. package/packages/mcp/dist/tools/result.d.ts +5 -0
  334. package/packages/mcp/dist/tools/result.d.ts.map +1 -0
  335. package/packages/mcp/dist/tools/result.js +15 -0
  336. package/packages/mcp/dist/tools/result.js.map +1 -0
  337. package/packages/mcp/dist/tools.d.ts +14 -0
  338. package/packages/mcp/dist/tools.d.ts.map +1 -0
  339. package/packages/mcp/dist/tools.js +58 -0
  340. package/packages/mcp/dist/tools.js.map +1 -0
  341. package/packages/mcp/package.json +20 -0
  342. package/packages/orchestrator/dist/index.d.ts +171 -0
  343. package/packages/orchestrator/dist/index.d.ts.map +1 -0
  344. package/packages/orchestrator/dist/index.js +524 -0
  345. package/packages/orchestrator/dist/index.js.map +1 -0
  346. package/packages/orchestrator/package.json +18 -0
  347. package/packages/policies/dist/index.d.ts +11 -0
  348. package/packages/policies/dist/index.d.ts.map +1 -0
  349. package/packages/policies/dist/index.js +6 -0
  350. package/packages/policies/dist/index.js.map +1 -0
  351. package/packages/policies/dist/reconciliation.d.ts +5 -0
  352. package/packages/policies/dist/reconciliation.d.ts.map +1 -0
  353. package/packages/policies/dist/reconciliation.js +17 -0
  354. package/packages/policies/dist/reconciliation.js.map +1 -0
  355. package/packages/policies/dist/resume.d.ts +14 -0
  356. package/packages/policies/dist/resume.d.ts.map +1 -0
  357. package/packages/policies/dist/resume.js +7 -0
  358. package/packages/policies/dist/resume.js.map +1 -0
  359. package/packages/policies/dist/retry.d.ts +4 -0
  360. package/packages/policies/dist/retry.d.ts.map +1 -0
  361. package/packages/policies/dist/retry.js +7 -0
  362. package/packages/policies/dist/retry.js.map +1 -0
  363. package/packages/policies/dist/stopReason.d.ts +4 -0
  364. package/packages/policies/dist/stopReason.d.ts.map +1 -0
  365. package/packages/policies/dist/stopReason.js +11 -0
  366. package/packages/policies/dist/stopReason.js.map +1 -0
  367. package/packages/policies/dist/usage.d.ts +14 -0
  368. package/packages/policies/dist/usage.d.ts.map +1 -0
  369. package/packages/policies/dist/usage.js +38 -0
  370. package/packages/policies/dist/usage.js.map +1 -0
  371. package/packages/policies/dist/workerHost.d.ts +8 -0
  372. package/packages/policies/dist/workerHost.d.ts.map +1 -0
  373. package/packages/policies/dist/workerHost.js +20 -0
  374. package/packages/policies/dist/workerHost.js.map +1 -0
  375. package/packages/policies/package.json +21 -0
  376. package/packages/presenter/dist/index.d.ts +81 -0
  377. package/packages/presenter/dist/index.d.ts.map +1 -0
  378. package/packages/presenter/dist/index.js +421 -0
  379. package/packages/presenter/dist/index.js.map +1 -0
  380. package/packages/presenter/package.json +16 -0
  381. package/packages/projections/dist/index.d.ts +10 -0
  382. package/packages/projections/dist/index.d.ts.map +1 -0
  383. package/packages/projections/dist/index.js +30 -0
  384. package/packages/projections/dist/index.js.map +1 -0
  385. package/packages/projections/package.json +15 -0
  386. package/packages/prompt/dist/index.d.ts +9 -0
  387. package/packages/prompt/dist/index.d.ts.map +1 -0
  388. package/packages/prompt/dist/index.js +71 -0
  389. package/packages/prompt/dist/index.js.map +1 -0
  390. package/packages/prompt/package.json +16 -0
  391. package/packages/retry-scheduler/dist/index.d.ts +12 -0
  392. package/packages/retry-scheduler/dist/index.d.ts.map +1 -0
  393. package/packages/retry-scheduler/dist/index.js +39 -0
  394. package/packages/retry-scheduler/dist/index.js.map +1 -0
  395. package/packages/retry-scheduler/package.json +15 -0
  396. package/packages/runtime/dist/index.d.ts +157 -0
  397. package/packages/runtime/dist/index.d.ts.map +1 -0
  398. package/packages/runtime/dist/index.js +1074 -0
  399. package/packages/runtime/dist/index.js.map +1 -0
  400. package/packages/runtime/package.json +26 -0
  401. package/packages/runtime-events/dist/index.d.ts +110 -0
  402. package/packages/runtime-events/dist/index.d.ts.map +1 -0
  403. package/packages/runtime-events/dist/index.js +25 -0
  404. package/packages/runtime-events/dist/index.js.map +1 -0
  405. package/packages/runtime-events/package.json +14 -0
  406. package/packages/server/dist/index.d.ts +25 -0
  407. package/packages/server/dist/index.d.ts.map +1 -0
  408. package/packages/server/dist/index.js +213 -0
  409. package/packages/server/dist/index.js.map +1 -0
  410. package/packages/server/dist/issue-store.d.ts +26 -0
  411. package/packages/server/dist/issue-store.d.ts.map +1 -0
  412. package/packages/server/dist/issue-store.js +88 -0
  413. package/packages/server/dist/issue-store.js.map +1 -0
  414. package/packages/server/dist/path-params.d.ts +6 -0
  415. package/packages/server/dist/path-params.d.ts.map +1 -0
  416. package/packages/server/dist/path-params.js +15 -0
  417. package/packages/server/dist/path-params.js.map +1 -0
  418. package/packages/server/dist/source.d.ts +12 -0
  419. package/packages/server/dist/source.d.ts.map +1 -0
  420. package/packages/server/dist/source.js +2 -0
  421. package/packages/server/dist/source.js.map +1 -0
  422. package/packages/server/dist/trace-routes.d.ts +21 -0
  423. package/packages/server/dist/trace-routes.d.ts.map +1 -0
  424. package/packages/server/dist/trace-routes.js +66 -0
  425. package/packages/server/dist/trace-routes.js.map +1 -0
  426. package/packages/server/dist/ws.d.ts +18 -0
  427. package/packages/server/dist/ws.d.ts.map +1 -0
  428. package/packages/server/dist/ws.js +168 -0
  429. package/packages/server/dist/ws.js.map +1 -0
  430. package/packages/server/package.json +22 -0
  431. package/packages/ssh/dist/index.d.ts +33 -0
  432. package/packages/ssh/dist/index.d.ts.map +1 -0
  433. package/packages/ssh/dist/index.js +281 -0
  434. package/packages/ssh/dist/index.js.map +1 -0
  435. package/packages/ssh/package.json +15 -0
  436. package/packages/static-worker/dist/index.d.ts +73 -0
  437. package/packages/static-worker/dist/index.d.ts.map +1 -0
  438. package/packages/static-worker/dist/index.js +150 -0
  439. package/packages/static-worker/dist/index.js.map +1 -0
  440. package/packages/static-worker/package.json +14 -0
  441. package/packages/tool-sdk/dist/filter.d.ts +70 -0
  442. package/packages/tool-sdk/dist/filter.d.ts.map +1 -0
  443. package/packages/tool-sdk/dist/filter.js +231 -0
  444. package/packages/tool-sdk/dist/filter.js.map +1 -0
  445. package/packages/tool-sdk/dist/index.d.ts +6 -0
  446. package/packages/tool-sdk/dist/index.d.ts.map +1 -0
  447. package/packages/tool-sdk/dist/index.js +4 -0
  448. package/packages/tool-sdk/dist/index.js.map +1 -0
  449. package/packages/tool-sdk/dist/provider.d.ts +51 -0
  450. package/packages/tool-sdk/dist/provider.d.ts.map +1 -0
  451. package/packages/tool-sdk/dist/provider.js +2 -0
  452. package/packages/tool-sdk/dist/provider.js.map +1 -0
  453. package/packages/tool-sdk/dist/registry.d.ts +35 -0
  454. package/packages/tool-sdk/dist/registry.d.ts.map +1 -0
  455. package/packages/tool-sdk/dist/registry.js +85 -0
  456. package/packages/tool-sdk/dist/registry.js.map +1 -0
  457. package/packages/tool-sdk/dist/result.d.ts +5 -0
  458. package/packages/tool-sdk/dist/result.d.ts.map +1 -0
  459. package/packages/tool-sdk/dist/result.js +15 -0
  460. package/packages/tool-sdk/dist/result.js.map +1 -0
  461. package/packages/tool-sdk/package.json +14 -0
  462. package/packages/traceviz-emitter/dist/index.d.ts +19 -0
  463. package/packages/traceviz-emitter/dist/index.d.ts.map +1 -0
  464. package/packages/traceviz-emitter/dist/index.js +97 -0
  465. package/packages/traceviz-emitter/dist/index.js.map +1 -0
  466. package/packages/traceviz-emitter/package.json +17 -0
  467. package/packages/traceviz-server/dist/index.d.ts +14 -0
  468. package/packages/traceviz-server/dist/index.d.ts.map +1 -0
  469. package/packages/traceviz-server/dist/index.js +10 -0
  470. package/packages/traceviz-server/dist/index.js.map +1 -0
  471. package/packages/traceviz-server/dist/models/api.d.ts +51 -0
  472. package/packages/traceviz-server/dist/models/api.d.ts.map +1 -0
  473. package/packages/traceviz-server/dist/models/api.js +5 -0
  474. package/packages/traceviz-server/dist/models/api.js.map +1 -0
  475. package/packages/traceviz-server/dist/models/display-events.d.ts +58 -0
  476. package/packages/traceviz-server/dist/models/display-events.d.ts.map +1 -0
  477. package/packages/traceviz-server/dist/models/display-events.js +6 -0
  478. package/packages/traceviz-server/dist/models/display-events.js.map +1 -0
  479. package/packages/traceviz-server/dist/parser.d.ts +14 -0
  480. package/packages/traceviz-server/dist/parser.d.ts.map +1 -0
  481. package/packages/traceviz-server/dist/parser.js +363 -0
  482. package/packages/traceviz-server/dist/parser.js.map +1 -0
  483. package/packages/traceviz-server/dist/stats.d.ts +7 -0
  484. package/packages/traceviz-server/dist/stats.d.ts.map +1 -0
  485. package/packages/traceviz-server/dist/stats.js +81 -0
  486. package/packages/traceviz-server/dist/stats.js.map +1 -0
  487. package/packages/traceviz-server/dist/watcher.d.ts +54 -0
  488. package/packages/traceviz-server/dist/watcher.d.ts.map +1 -0
  489. package/packages/traceviz-server/dist/watcher.js +368 -0
  490. package/packages/traceviz-server/dist/watcher.js.map +1 -0
  491. package/packages/traceviz-server/package.json +16 -0
  492. package/packages/tracker-sdk/dist/index.d.ts +5 -0
  493. package/packages/tracker-sdk/dist/index.d.ts.map +1 -0
  494. package/packages/tracker-sdk/dist/index.js +4 -0
  495. package/packages/tracker-sdk/dist/index.js.map +1 -0
  496. package/packages/tracker-sdk/dist/options.d.ts +20 -0
  497. package/packages/tracker-sdk/dist/options.d.ts.map +1 -0
  498. package/packages/tracker-sdk/dist/options.js +46 -0
  499. package/packages/tracker-sdk/dist/options.js.map +1 -0
  500. package/packages/tracker-sdk/dist/provider.d.ts +104 -0
  501. package/packages/tracker-sdk/dist/provider.d.ts.map +1 -0
  502. package/packages/tracker-sdk/dist/provider.js +2 -0
  503. package/packages/tracker-sdk/dist/provider.js.map +1 -0
  504. package/packages/tracker-sdk/dist/registry.d.ts +26 -0
  505. package/packages/tracker-sdk/dist/registry.d.ts.map +1 -0
  506. package/packages/tracker-sdk/dist/registry.js +52 -0
  507. package/packages/tracker-sdk/dist/registry.js.map +1 -0
  508. package/packages/tracker-sdk/dist/toolPack.d.ts +10 -0
  509. package/packages/tracker-sdk/dist/toolPack.d.ts.map +1 -0
  510. package/packages/tracker-sdk/dist/toolPack.js +185 -0
  511. package/packages/tracker-sdk/dist/toolPack.js.map +1 -0
  512. package/packages/tracker-sdk/package.json +15 -0
  513. package/packages/tui/dist/index.d.ts +35 -0
  514. package/packages/tui/dist/index.d.ts.map +1 -0
  515. package/packages/tui/dist/index.js +354 -0
  516. package/packages/tui/dist/index.js.map +1 -0
  517. package/packages/tui/package.json +18 -0
  518. package/packages/worker-host-pool/dist/index.d.ts +33 -0
  519. package/packages/worker-host-pool/dist/index.d.ts.map +1 -0
  520. package/packages/worker-host-pool/dist/index.js +311 -0
  521. package/packages/worker-host-pool/dist/index.js.map +1 -0
  522. package/packages/worker-host-pool/package.json +14 -0
  523. package/packages/worker-pool/dist/index.d.ts +6 -0
  524. package/packages/worker-pool/dist/index.d.ts.map +1 -0
  525. package/packages/worker-pool/dist/index.js +15 -0
  526. package/packages/worker-pool/dist/index.js.map +1 -0
  527. package/packages/worker-pool/dist/lease.d.ts +36 -0
  528. package/packages/worker-pool/dist/lease.d.ts.map +1 -0
  529. package/packages/worker-pool/dist/lease.js +53 -0
  530. package/packages/worker-pool/dist/lease.js.map +1 -0
  531. package/packages/worker-pool/dist/ledger.d.ts +51 -0
  532. package/packages/worker-pool/dist/ledger.d.ts.map +1 -0
  533. package/packages/worker-pool/dist/ledger.js +165 -0
  534. package/packages/worker-pool/dist/ledger.js.map +1 -0
  535. package/packages/worker-pool/dist/mutex.d.ts +10 -0
  536. package/packages/worker-pool/dist/mutex.d.ts.map +1 -0
  537. package/packages/worker-pool/dist/mutex.js +22 -0
  538. package/packages/worker-pool/dist/mutex.js.map +1 -0
  539. package/packages/worker-pool/dist/pool.d.ts +33 -0
  540. package/packages/worker-pool/dist/pool.d.ts.map +1 -0
  541. package/packages/worker-pool/dist/pool.js +1727 -0
  542. package/packages/worker-pool/dist/pool.js.map +1 -0
  543. package/packages/worker-pool/dist/reaper.d.ts +94 -0
  544. package/packages/worker-pool/dist/reaper.d.ts.map +1 -0
  545. package/packages/worker-pool/dist/reaper.js +295 -0
  546. package/packages/worker-pool/dist/reaper.js.map +1 -0
  547. package/packages/worker-pool/dist/types.d.ts +249 -0
  548. package/packages/worker-pool/dist/types.d.ts.map +1 -0
  549. package/packages/worker-pool/dist/types.js +2 -0
  550. package/packages/worker-pool/dist/types.js.map +1 -0
  551. package/packages/worker-pool/package.json +16 -0
  552. package/packages/worker-sdk/dist/conformance.d.ts +64 -0
  553. package/packages/worker-sdk/dist/conformance.d.ts.map +1 -0
  554. package/packages/worker-sdk/dist/conformance.js +109 -0
  555. package/packages/worker-sdk/dist/conformance.js.map +1 -0
  556. package/packages/worker-sdk/dist/fake.d.ts +76 -0
  557. package/packages/worker-sdk/dist/fake.d.ts.map +1 -0
  558. package/packages/worker-sdk/dist/fake.js +142 -0
  559. package/packages/worker-sdk/dist/fake.js.map +1 -0
  560. package/packages/worker-sdk/dist/index.d.ts +5 -0
  561. package/packages/worker-sdk/dist/index.d.ts.map +1 -0
  562. package/packages/worker-sdk/dist/index.js +10 -0
  563. package/packages/worker-sdk/dist/index.js.map +1 -0
  564. package/packages/worker-sdk/dist/module.d.ts +46 -0
  565. package/packages/worker-sdk/dist/module.d.ts.map +1 -0
  566. package/packages/worker-sdk/dist/module.js +59 -0
  567. package/packages/worker-sdk/dist/module.js.map +1 -0
  568. package/packages/worker-sdk/dist/registry.d.ts +24 -0
  569. package/packages/worker-sdk/dist/registry.d.ts.map +1 -0
  570. package/packages/worker-sdk/dist/registry.js +49 -0
  571. package/packages/worker-sdk/dist/registry.js.map +1 -0
  572. package/packages/worker-sdk/dist/types.d.ts +138 -0
  573. package/packages/worker-sdk/dist/types.d.ts.map +1 -0
  574. package/packages/worker-sdk/dist/types.js +21 -0
  575. package/packages/worker-sdk/dist/types.js.map +1 -0
  576. package/packages/worker-sdk/package.json +15 -0
  577. package/packages/workflow/dist/index.d.ts +33 -0
  578. package/packages/workflow/dist/index.d.ts.map +1 -0
  579. package/packages/workflow/dist/index.js +125 -0
  580. package/packages/workflow/dist/index.js.map +1 -0
  581. package/packages/workflow/package.json +19 -0
  582. package/packages/workspace/dist/index.d.ts +70 -0
  583. package/packages/workspace/dist/index.d.ts.map +1 -0
  584. package/packages/workspace/dist/index.js +1016 -0
  585. package/packages/workspace/dist/index.js.map +1 -0
  586. package/packages/workspace/package.json +17 -0
  587. package/runtime-deps/anthropic-claude-agent-sdk/LICENSE.md +1 -0
  588. package/runtime-deps/anthropic-claude-agent-sdk/README.md +65 -0
  589. package/runtime-deps/anthropic-claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  590. package/runtime-deps/anthropic-claude-agent-sdk/assistant.d.ts +135 -0
  591. package/runtime-deps/anthropic-claude-agent-sdk/assistant.mjs +190 -0
  592. package/runtime-deps/anthropic-claude-agent-sdk/bridge.d.ts +231 -0
  593. package/runtime-deps/anthropic-claude-agent-sdk/bridge.mjs +168 -0
  594. package/runtime-deps/anthropic-claude-agent-sdk/browser-sdk.d.ts +53 -0
  595. package/runtime-deps/anthropic-claude-agent-sdk/browser-sdk.js +93 -0
  596. package/runtime-deps/anthropic-claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  597. package/runtime-deps/anthropic-claude-agent-sdk/extractFromBunfs.js +156 -0
  598. package/runtime-deps/anthropic-claude-agent-sdk/manifest.json +47 -0
  599. package/runtime-deps/anthropic-claude-agent-sdk/manifest.zst.json +55 -0
  600. package/runtime-deps/anthropic-claude-agent-sdk/node_modules/.bin/anthropic-ai-sdk +21 -0
  601. package/runtime-deps/anthropic-claude-agent-sdk/package.json +81 -0
  602. package/runtime-deps/anthropic-claude-agent-sdk/sdk-tools.d.ts +3170 -0
  603. package/runtime-deps/anthropic-claude-agent-sdk/sdk.d.ts +6000 -0
  604. package/runtime-deps/anthropic-claude-agent-sdk/sdk.mjs +119 -0
  605. package/runtime-deps/openai-codex/README.md +60 -0
  606. package/runtime-deps/openai-codex/bin/codex.js +229 -0
  607. package/runtime-deps/openai-codex/bin/rg +79 -0
  608. package/runtime-deps/openai-codex/package.json +22 -0
  609. package/vendor/claude-agent-acp/dist/acp-agent.d.ts +239 -0
  610. package/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
  611. package/vendor/claude-agent-acp/dist/acp-agent.js +2693 -0
  612. package/vendor/claude-agent-acp/dist/bundle.js +41230 -0
  613. package/vendor/claude-agent-acp/dist/index.d.ts +3 -0
  614. package/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
  615. package/vendor/claude-agent-acp/dist/index.js +67 -0
  616. package/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
  617. package/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
  618. package/vendor/claude-agent-acp/dist/lib.js +5 -0
  619. package/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
  620. package/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
  621. package/vendor/claude-agent-acp/dist/settings.js +182 -0
  622. package/vendor/claude-agent-acp/dist/tools.d.ts +103 -0
  623. package/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
  624. package/vendor/claude-agent-acp/dist/tools.js +713 -0
  625. package/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
  626. package/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
  627. package/vendor/claude-agent-acp/dist/utils.js +83 -0
  628. package/vendor/claude-agent-acp/package.json +23 -0
  629. package/vendor/codex-acp/dist/index.js +21280 -0
  630. package/vendor/codex-acp/package.json +17 -0
@@ -0,0 +1,2693 @@
1
+ import { AgentSideConnection, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
2
+ import { deleteSession, getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
3
+ import { randomUUID } from "node:crypto";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import packageJson from "../package.json" with { type: "json" };
7
+ import { SettingsManager } from "./settings.js";
8
+ import { applyTaskCreate, applyTaskUpdate, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, } from "./tools.js";
9
+ import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
10
+ export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
11
+ const MAX_TITLE_LENGTH = 256;
12
+ function sanitizeTitle(text) {
13
+ // Replace newlines and collapse whitespace
14
+ const sanitized = text
15
+ .replace(/[\r\n]+/g, " ")
16
+ .replace(/\s+/g, " ")
17
+ .trim();
18
+ if (sanitized.length <= MAX_TITLE_LENGTH) {
19
+ return sanitized;
20
+ }
21
+ return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
22
+ }
23
+ const ZERO_USAGE = Object.freeze({
24
+ input_tokens: 0,
25
+ output_tokens: 0,
26
+ cache_read_input_tokens: 0,
27
+ cache_creation_input_tokens: 0,
28
+ });
29
+ const DEFAULT_CONTEXT_WINDOW = 200000;
30
+ /** Compute a stable fingerprint of the session-defining params so we can
31
+ * detect when a loadSession/resumeSession call requires tearing down and
32
+ * recreating the underlying Query process. MCP servers are sorted by name
33
+ * so that ordering differences don't trigger unnecessary recreations. */
34
+ function computeSessionFingerprint(params) {
35
+ const servers = [...(params.mcpServers ?? [])].sort((a, b) => a.name.localeCompare(b.name));
36
+ return JSON.stringify({ cwd: params.cwd, mcpServers: servers });
37
+ }
38
+ export async function claudeCliPath() {
39
+ if (process.env.CLAUDE_CODE_EXECUTABLE) {
40
+ return process.env.CLAUDE_CODE_EXECUTABLE;
41
+ }
42
+ // The SDK's CLI is a native binary shipped as a platform-specific optional
43
+ // dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
44
+ // to the SDK so nested installs are found even when npm doesn't hoist.
45
+ const { createRequire } = await import("node:module");
46
+ const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
47
+ const ext = process.platform === "win32" ? ".exe" : "";
48
+ // On linux, both glibc and musl variants may be installed side-by-side
49
+ // (e.g. bunx hydrates every optional dep), so picking one by trial is
50
+ // unreliable: the wrong binary segfaults at runtime instead of failing to
51
+ // spawn. Detect the runtime libc and prefer the matching variant, falling
52
+ // back to the other only if the preferred one isn't installed.
53
+ const candidates = process.platform === "linux"
54
+ ? isMuslLibc()
55
+ ? [
56
+ `@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
57
+ `@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
58
+ ]
59
+ : [
60
+ `@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
61
+ `@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
62
+ ]
63
+ : [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
64
+ for (const candidate of candidates) {
65
+ try {
66
+ return req.resolve(candidate);
67
+ }
68
+ catch {
69
+ // try next candidate
70
+ }
71
+ }
72
+ throw new Error(`Claude native binary not found for ${process.platform}-${process.arch}. ` +
73
+ `Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`);
74
+ }
75
+ function isMuslLibc() {
76
+ // process.report.getReport().header.glibcVersionRuntime is populated when
77
+ // Node is dynamically linked against glibc, and absent on musl.
78
+ const report = process.report?.getReport();
79
+ return !report?.header?.glibcVersionRuntime;
80
+ }
81
+ function shouldHideClaudeAuth() {
82
+ return process.argv.includes("--hide-claude-auth");
83
+ }
84
+ // Bypass Permissions doesn't work if we are a root/sudo user
85
+ const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
86
+ const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
87
+ // Slash commands that the SDK handles locally without replaying the user
88
+ // message and without invoking the model.
89
+ const LOCAL_ONLY_COMMANDS = new Set(["/context", "/heapdump", "/extra-usage"]);
90
+ // The Claude SDK persists local slash command invocations (e.g. `/model`) and
91
+ // their output as user messages in the session transcript, wrapping the
92
+ // payload in these XML-like markers that the CLI uses for its own display.
93
+ // The live prompt loop drops them; replay must strip them too or they leak
94
+ // into the UI on session/load.
95
+ const LOCAL_COMMAND_MARKERS = [
96
+ "command-name",
97
+ "command-message",
98
+ "command-args",
99
+ "local-command-stdout",
100
+ "local-command-stderr",
101
+ ].map((tag) => ({ open: `<${tag}>`, close: `</${tag}>` }));
102
+ // Single-pass scanner that removes each `<tag>…</tag>` marker (matching the
103
+ // nearest closing tag of the same name, like a lazy regex would).
104
+ function stripMarkerTags(text) {
105
+ const dead = new Set();
106
+ let result = "";
107
+ let copiedUpTo = 0;
108
+ let i = 0;
109
+ while (i < text.length) {
110
+ if (text[i] === "<") {
111
+ const marker = LOCAL_COMMAND_MARKERS.find((m) => !dead.has(m.open) && text.startsWith(m.open, i));
112
+ if (marker) {
113
+ const end = text.indexOf(marker.close, i + marker.open.length);
114
+ if (end !== -1) {
115
+ result += text.slice(copiedUpTo, i);
116
+ i = copiedUpTo = end + marker.close.length;
117
+ continue;
118
+ }
119
+ // No closing marker remains anywhere ahead, and `indexOf` only ever
120
+ // searches forward from here on, so stop treating this tag as an
121
+ // opener — that avoids rescanning the tail for it on every match.
122
+ dead.add(marker.open);
123
+ }
124
+ }
125
+ i++;
126
+ }
127
+ return result + text.slice(copiedUpTo);
128
+ }
129
+ /**
130
+ * Return user-message content with local-command marker tags removed, or
131
+ * `null` if nothing meaningful remains (caller should skip the message).
132
+ * Preserves real prose that's mixed in alongside the markers — e.g. a
133
+ * message like `<command-name>…</command-name>hi` becomes `hi`.
134
+ */
135
+ export function stripLocalCommandMetadata(content) {
136
+ if (typeof content === "string") {
137
+ const stripped = stripMarkerTags(content);
138
+ return stripped.trim() === "" ? null : stripped;
139
+ }
140
+ if (!Array.isArray(content))
141
+ return content;
142
+ const kept = [];
143
+ for (const block of content) {
144
+ if (block &&
145
+ typeof block === "object" &&
146
+ "type" in block &&
147
+ block.type === "text" &&
148
+ "text" in block &&
149
+ typeof block.text === "string") {
150
+ const stripped = stripMarkerTags(block.text);
151
+ if (stripped.trim() === "")
152
+ continue;
153
+ kept.push({ ...block, text: stripped });
154
+ }
155
+ else {
156
+ kept.push(block);
157
+ }
158
+ }
159
+ if (kept.length === 0)
160
+ return null;
161
+ return kept;
162
+ }
163
+ export function isLocalCommandMetadata(content) {
164
+ return stripLocalCommandMetadata(content) === null;
165
+ }
166
+ const PERMISSION_MODE_ALIASES = {
167
+ auto: "auto",
168
+ default: "default",
169
+ acceptedits: "acceptEdits",
170
+ dontask: "dontAsk",
171
+ plan: "plan",
172
+ bypasspermissions: "bypassPermissions",
173
+ bypass: "bypassPermissions",
174
+ };
175
+ export function resolvePermissionMode(defaultMode, logger = console) {
176
+ if (defaultMode === undefined) {
177
+ return "default";
178
+ }
179
+ if (typeof defaultMode !== "string") {
180
+ logger.error("Ignoring permissions.defaultMode from settings: expected a string.");
181
+ return "default";
182
+ }
183
+ const normalized = defaultMode.trim().toLowerCase();
184
+ if (normalized === "") {
185
+ logger.error("Ignoring permissions.defaultMode from settings: expected a non-empty string.");
186
+ return "default";
187
+ }
188
+ const mapped = PERMISSION_MODE_ALIASES[normalized];
189
+ if (!mapped) {
190
+ logger.error(`Ignoring permissions.defaultMode from settings: unknown value '${defaultMode}'.`);
191
+ return "default";
192
+ }
193
+ if (mapped === "bypassPermissions" && !ALLOW_BYPASS) {
194
+ logger.error("Ignoring permissions.defaultMode from settings: bypassPermissions is not available when running as root.");
195
+ return "default";
196
+ }
197
+ return mapped;
198
+ }
199
+ /**
200
+ * Builds the label for the "Always Allow" permission option so the user can see
201
+ * the exact scope they are committing to. Uses the SDK-provided suggestions
202
+ * when available (e.g. `Bash(npm test:*)`) and falls back to naming the whole
203
+ * tool so "Always Allow" is never a blank check without disclosure.
204
+ */
205
+ export function describeAlwaysAllow(suggestions, toolName) {
206
+ if (!suggestions || suggestions.length === 0) {
207
+ return `Always Allow all ${toolName}`;
208
+ }
209
+ const ruleLabels = [];
210
+ const directories = [];
211
+ for (const update of suggestions) {
212
+ if (update.type === "addRules" && update.behavior === "allow") {
213
+ for (const rule of update.rules) {
214
+ ruleLabels.push(rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : `all ${rule.toolName}`);
215
+ }
216
+ }
217
+ else if (update.type === "addDirectories") {
218
+ directories.push(...update.directories);
219
+ }
220
+ }
221
+ const parts = [];
222
+ if (ruleLabels.length > 0) {
223
+ parts.push(ruleLabels.join(", "));
224
+ }
225
+ if (directories.length > 0) {
226
+ parts.push(`access to ${directories.join(", ")}`);
227
+ }
228
+ if (parts.length === 0) {
229
+ return `Always Allow all ${toolName}`;
230
+ }
231
+ return `Always Allow ${parts.join(" and ")}`;
232
+ }
233
+ // Implement the ACP Agent interface
234
+ export class ClaudeAcpAgent {
235
+ constructor(client, logger) {
236
+ this.backgroundTerminals = {};
237
+ this.sessions = {};
238
+ this.client = client;
239
+ this.toolUseCache = {};
240
+ this.logger = logger ?? console;
241
+ }
242
+ async initialize(request) {
243
+ this.clientCapabilities = request.clientCapabilities;
244
+ // Bypasses standard auth by routing requests through a custom Anthropic-protocol gateway.
245
+ // Only offered when the client advertises `auth._meta.gateway` capability.
246
+ const supportsGatewayAuth = request.clientCapabilities?.auth?._meta?.gateway === true;
247
+ const gatewayAuthMethod = {
248
+ id: "gateway",
249
+ name: "Custom model gateway",
250
+ description: "Use a custom gateway to authenticate and access models",
251
+ _meta: {
252
+ gateway: {
253
+ protocol: "anthropic",
254
+ },
255
+ },
256
+ };
257
+ const gatewayBedrockAuthMethod = {
258
+ id: "gateway-bedrock",
259
+ name: "Custom model gateway",
260
+ description: "Use a custom gateway to authenticate and access models",
261
+ _meta: {
262
+ gateway: {
263
+ protocol: "bedrock",
264
+ },
265
+ },
266
+ };
267
+ const supportsTerminalAuth = request.clientCapabilities?.auth?.terminal === true;
268
+ const supportsMetaTerminalAuth = request.clientCapabilities?._meta?.["terminal-auth"] === true;
269
+ // Detect remote environments where the OAuth browser redirect to localhost
270
+ // won't work. This matches the SDK's internal isRemote check. In these cases,
271
+ // the `auth login` subcommand would fall back to a device-code-like manual
272
+ // flow, which doesn't work well over ACP, so we offer the TUI login instead.
273
+ const isRemote = !!(process.env.NO_BROWSER ||
274
+ process.env.SSH_CONNECTION ||
275
+ process.env.SSH_CLIENT ||
276
+ process.env.SSH_TTY ||
277
+ process.env.CLAUDE_CODE_REMOTE);
278
+ const terminalAuthMethods = [];
279
+ if (isRemote) {
280
+ const remoteLoginMethod = {
281
+ description: "Run `claude /login` in the terminal",
282
+ name: "Log in with Claude",
283
+ id: "claude-login",
284
+ type: "terminal",
285
+ args: ["--cli"],
286
+ };
287
+ if (supportsMetaTerminalAuth) {
288
+ remoteLoginMethod._meta = {
289
+ "terminal-auth": {
290
+ command: process.execPath,
291
+ args: [...process.argv.slice(1), "--cli"],
292
+ label: "Claude Login",
293
+ },
294
+ };
295
+ }
296
+ if (!shouldHideClaudeAuth() && (supportsTerminalAuth || supportsMetaTerminalAuth)) {
297
+ terminalAuthMethods.push(remoteLoginMethod);
298
+ }
299
+ }
300
+ else {
301
+ const claudeLoginMethod = {
302
+ description: "Use Claude subscription ",
303
+ name: "Claude Subscription",
304
+ id: "claude-ai-login",
305
+ type: "terminal",
306
+ args: ["--cli", "auth", "login", "--claudeai"],
307
+ };
308
+ const consoleLoginMethod = {
309
+ description: "Use Anthropic Console (API usage billing)",
310
+ name: "Anthropic Console",
311
+ id: "console-login",
312
+ type: "terminal",
313
+ args: ["--cli", "auth", "login", "--console"],
314
+ };
315
+ if (supportsMetaTerminalAuth) {
316
+ const baseArgs = process.argv.slice(1);
317
+ claudeLoginMethod._meta = {
318
+ "terminal-auth": {
319
+ command: process.execPath,
320
+ args: [...baseArgs, "--cli", "auth", "login", "--claudeai"],
321
+ label: "Claude Login",
322
+ },
323
+ };
324
+ consoleLoginMethod._meta = {
325
+ "terminal-auth": {
326
+ command: process.execPath,
327
+ args: [...baseArgs, "--cli", "auth", "login", "--console"],
328
+ label: "Anthropic Console Login",
329
+ },
330
+ };
331
+ }
332
+ if (!shouldHideClaudeAuth() && (supportsTerminalAuth || supportsMetaTerminalAuth)) {
333
+ terminalAuthMethods.push(claudeLoginMethod);
334
+ }
335
+ if (supportsTerminalAuth || supportsMetaTerminalAuth) {
336
+ terminalAuthMethods.push(consoleLoginMethod);
337
+ }
338
+ }
339
+ return {
340
+ protocolVersion: 1,
341
+ agentCapabilities: {
342
+ _meta: {
343
+ claudeCode: {
344
+ promptQueueing: true,
345
+ },
346
+ },
347
+ promptCapabilities: {
348
+ image: true,
349
+ embeddedContext: true,
350
+ },
351
+ mcpCapabilities: {
352
+ http: true,
353
+ sse: true,
354
+ },
355
+ loadSession: true,
356
+ sessionCapabilities: {
357
+ additionalDirectories: {},
358
+ close: {},
359
+ delete: {},
360
+ fork: {},
361
+ list: {},
362
+ resume: {},
363
+ },
364
+ },
365
+ agentInfo: {
366
+ name: packageJson.name,
367
+ title: "Claude Agent",
368
+ version: packageJson.version,
369
+ },
370
+ authMethods: [
371
+ ...terminalAuthMethods,
372
+ ...(supportsGatewayAuth ? [gatewayAuthMethod, gatewayBedrockAuthMethod] : []),
373
+ ],
374
+ };
375
+ }
376
+ async newSession(params) {
377
+ const response = await this.createSession(params, {
378
+ // Revisit these meta values once we support resume
379
+ resume: params._meta?.claudeCode?.options?.resume,
380
+ });
381
+ // Needs to happen after we return the session
382
+ setTimeout(() => {
383
+ this.sendAvailableCommandsUpdate(response.sessionId);
384
+ }, 0);
385
+ return response;
386
+ }
387
+ async unstable_forkSession(params) {
388
+ const response = await this.createSession({
389
+ cwd: params.cwd,
390
+ mcpServers: params.mcpServers ?? [],
391
+ additionalDirectories: params.additionalDirectories,
392
+ _meta: params._meta,
393
+ }, {
394
+ resume: params.sessionId,
395
+ forkSession: true,
396
+ });
397
+ // Needs to happen after we return the session
398
+ setTimeout(() => {
399
+ this.sendAvailableCommandsUpdate(response.sessionId);
400
+ }, 0);
401
+ return response;
402
+ }
403
+ async resumeSession(params) {
404
+ const result = await this.getOrCreateSession(params);
405
+ // Needs to happen after we return the session
406
+ setTimeout(() => {
407
+ this.sendAvailableCommandsUpdate(params.sessionId);
408
+ }, 0);
409
+ return result;
410
+ }
411
+ async loadSession(params) {
412
+ const result = await this.getOrCreateSession(params);
413
+ await this.replaySessionHistory(params.sessionId);
414
+ // Send available commands after replay so it doesn't interleave with history
415
+ setTimeout(() => {
416
+ this.sendAvailableCommandsUpdate(params.sessionId);
417
+ }, 0);
418
+ return result;
419
+ }
420
+ async listSessions(params) {
421
+ const sdk_sessions = await listSessions({ dir: params.cwd ?? undefined });
422
+ const sessions = [];
423
+ for (const session of sdk_sessions) {
424
+ if (!session.cwd)
425
+ continue;
426
+ sessions.push({
427
+ sessionId: session.sessionId,
428
+ cwd: session.cwd,
429
+ title: sanitizeTitle(session.summary),
430
+ updatedAt: new Date(session.lastModified).toISOString(),
431
+ });
432
+ }
433
+ return {
434
+ sessions,
435
+ };
436
+ }
437
+ async authenticate(_params) {
438
+ if (_params.methodId === "gateway" || _params.methodId === "gateway-bedrock") {
439
+ this.gatewayAuthRequest = _params;
440
+ return;
441
+ }
442
+ throw new Error("Method not implemented.");
443
+ }
444
+ async prompt(params) {
445
+ const session = this.sessions[params.sessionId];
446
+ if (!session) {
447
+ throw new Error("Session not found");
448
+ }
449
+ session.cancelled = false;
450
+ session.accumulatedUsage = {
451
+ inputTokens: 0,
452
+ outputTokens: 0,
453
+ cachedReadTokens: 0,
454
+ cachedWriteTokens: 0,
455
+ };
456
+ let lastAssistantTotalUsage = null;
457
+ let lastAssistantUsage = null;
458
+ let lastAssistantModel = null;
459
+ // When the Claude SDK classifies a turn as failed (e.g. rate limit, auth
460
+ // problem, billing), it sets a categorical `error` field on the
461
+ // `SDKAssistantMessage` that precedes the final `result` message. We
462
+ // capture it here so the subsequent `RequestError.internalError` can
463
+ // forward it to clients as structured `data`, sparing them from
464
+ // pattern-matching on the human-readable message text.
465
+ let lastAssistantError;
466
+ // Tracks whether we're inside a compaction. The SDK emits the terminal
467
+ // `status` (compact_result success/failed) twice for a single failed
468
+ // compaction, and the two messages are indistinguishable — so we report the
469
+ // outcome only while a compaction is in progress, then clear this. A fresh
470
+ // `compacting` status sets it again, so every distinct compaction (e.g.
471
+ // repeated auto-compactions in a long turn) is still shown.
472
+ let compactionInProgress = false;
473
+ const userMessage = promptToClaude(params);
474
+ const promptUuid = randomUUID();
475
+ userMessage.uuid = promptUuid;
476
+ // These local-only commands return a result without replaying the user
477
+ // message. Mark promptReplayed=true so their result isn't consumed as a
478
+ // background task result.
479
+ const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
480
+ const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
481
+ if (session.promptRunning) {
482
+ session.input.push(userMessage);
483
+ const order = session.nextPendingOrder++;
484
+ const cancelled = await new Promise((resolve) => {
485
+ session.pendingMessages.set(promptUuid, { resolve, order });
486
+ });
487
+ if (cancelled) {
488
+ return { stopReason: "cancelled" };
489
+ }
490
+ }
491
+ else {
492
+ session.input.push(userMessage);
493
+ }
494
+ session.promptRunning = true;
495
+ let handedOff = false;
496
+ let errored = false;
497
+ let stopReason = "end_turn";
498
+ try {
499
+ while (true) {
500
+ const { value: message, done } = await session.query.next();
501
+ if (done || !message) {
502
+ if (session.cancelled) {
503
+ return { stopReason: "cancelled" };
504
+ }
505
+ break;
506
+ }
507
+ if (session.emitRawSDKMessages &&
508
+ shouldEmitRawMessage(session.emitRawSDKMessages, message)) {
509
+ await this.client.extNotification("_claude/sdkMessage", {
510
+ sessionId: params.sessionId,
511
+ message: message,
512
+ });
513
+ }
514
+ switch (message.type) {
515
+ case "system":
516
+ switch (message.subtype) {
517
+ case "init":
518
+ break;
519
+ case "status": {
520
+ if (message.status === "compacting") {
521
+ compactionInProgress = true;
522
+ await this.client.sessionUpdate({
523
+ sessionId: message.session_id,
524
+ update: {
525
+ sessionUpdate: "agent_message_chunk",
526
+ content: { type: "text", text: "Compacting..." },
527
+ },
528
+ });
529
+ }
530
+ else if (message.compact_result === "success" && compactionInProgress) {
531
+ // The SDK signals manual `/compact` completion with a status
532
+ // message carrying `compact_result`, not the `compact_boundary`
533
+ // message (which only fires when there's content to compact).
534
+ compactionInProgress = false;
535
+ await this.client.sessionUpdate({
536
+ sessionId: message.session_id,
537
+ update: {
538
+ sessionUpdate: "agent_message_chunk",
539
+ content: { type: "text", text: "\n\nCompacting completed." },
540
+ },
541
+ });
542
+ }
543
+ else if (message.compact_result === "failed" && compactionInProgress) {
544
+ compactionInProgress = false;
545
+ const reason = message.compact_error ? `: ${message.compact_error}` : ".";
546
+ await this.client.sessionUpdate({
547
+ sessionId: message.session_id,
548
+ update: {
549
+ sessionUpdate: "agent_message_chunk",
550
+ content: { type: "text", text: `\n\nCompacting failed${reason}` },
551
+ },
552
+ });
553
+ }
554
+ break;
555
+ }
556
+ case "compact_boundary": {
557
+ // Send used:0 immediately so the client doesn't keep showing
558
+ // the stale pre-compaction context size until the next turn.
559
+ //
560
+ // This is a deliberate approximation: we don't know the exact
561
+ // post-compaction token count (only the SDK's next API call
562
+ // reveals that). But used:0 is directionally correct — context
563
+ // just dropped dramatically — and the real value replaces it
564
+ // within seconds when the next result message arrives.
565
+ // The alternative (no update) leaves the client showing e.g.
566
+ // "944k/1m" right after the user sees "Compacting completed",
567
+ // which is confusing and wrong.
568
+ //
569
+ // The "Compacting completed." text is emitted from the `status`
570
+ // handler (keyed on `compact_result`), not here, so the failure
571
+ // path gets a message too.
572
+ lastAssistantTotalUsage = 0;
573
+ lastAssistantUsage = null;
574
+ await this.client.sessionUpdate({
575
+ sessionId: message.session_id,
576
+ update: {
577
+ sessionUpdate: "usage_update",
578
+ used: 0,
579
+ size: session.contextWindowSize,
580
+ },
581
+ });
582
+ break;
583
+ }
584
+ case "local_command_output": {
585
+ await this.client.sessionUpdate({
586
+ sessionId: message.session_id,
587
+ update: {
588
+ sessionUpdate: "agent_message_chunk",
589
+ content: { type: "text", text: message.content },
590
+ },
591
+ });
592
+ break;
593
+ }
594
+ case "session_state_changed": {
595
+ if (message.state === "idle") {
596
+ if (session.cancelled) {
597
+ stopReason = "cancelled";
598
+ }
599
+ return { stopReason, usage: sessionUsage(session) };
600
+ }
601
+ break;
602
+ }
603
+ case "memory_recall": {
604
+ const isSynthesis = message.mode === "synthesize";
605
+ const locations = isSynthesis
606
+ ? []
607
+ : message.memories.map((m) => ({ path: m.path }));
608
+ const content = isSynthesis
609
+ ? message.memories
610
+ .filter((m) => typeof m.content === "string")
611
+ .map((m) => ({
612
+ type: "content",
613
+ content: { type: "text", text: m.content },
614
+ }))
615
+ : [];
616
+ const count = message.memories.length;
617
+ const title = isSynthesis
618
+ ? "Recalled synthesized memory"
619
+ : `Recalled ${count} ${count === 1 ? "memory" : "memories"}`;
620
+ await this.client.sessionUpdate({
621
+ sessionId: message.session_id,
622
+ update: {
623
+ sessionUpdate: "tool_call",
624
+ toolCallId: message.uuid,
625
+ title,
626
+ kind: "read",
627
+ status: "completed",
628
+ ...(locations.length > 0 && { locations }),
629
+ ...(content.length > 0 && { content }),
630
+ _meta: {
631
+ claudeCode: {
632
+ toolName: "memory_recall",
633
+ toolResponse: { mode: message.mode },
634
+ },
635
+ },
636
+ },
637
+ });
638
+ break;
639
+ }
640
+ case "hook_started":
641
+ case "hook_progress":
642
+ case "hook_response":
643
+ case "files_persisted":
644
+ case "task_started":
645
+ case "task_notification":
646
+ case "task_progress":
647
+ case "task_updated":
648
+ case "elicitation_complete":
649
+ case "plugin_install":
650
+ case "notification":
651
+ case "api_retry":
652
+ case "mirror_error":
653
+ case "permission_denied":
654
+ case "thinking_tokens":
655
+ // Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
656
+ break;
657
+ default:
658
+ unreachable(message, this.logger);
659
+ break;
660
+ }
661
+ break;
662
+ case "result": {
663
+ // Accumulate usage from this result
664
+ session.accumulatedUsage.inputTokens += message.usage.input_tokens;
665
+ session.accumulatedUsage.outputTokens += message.usage.output_tokens;
666
+ session.accumulatedUsage.cachedReadTokens += message.usage.cache_read_input_tokens;
667
+ session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens;
668
+ const matchingModelUsage = lastAssistantModel
669
+ ? getMatchingModelUsage(message.modelUsage, lastAssistantModel)
670
+ : null;
671
+ // Only overwrite when we have an authoritative value — a miss
672
+ // (e.g. a turn with no top-level assistant message) would
673
+ // otherwise discard the window learned on a prior turn and
674
+ // leave the next prompt's mid-stream updates reporting 200k.
675
+ if (matchingModelUsage) {
676
+ session.contextWindowSize = matchingModelUsage.contextWindow;
677
+ }
678
+ // Task-notification followups are autonomous work triggered by a
679
+ // task-notification system message, not by the user's prompt.
680
+ // They should not influence the user-turn lifecycle (stop reason,
681
+ // slash-command output forwarding) but their cost is real.
682
+ const isTaskNotification = message.origin?.kind === "task-notification";
683
+ // Send usage_update notification
684
+ if (lastAssistantTotalUsage !== null) {
685
+ await this.client.sessionUpdate({
686
+ sessionId: params.sessionId,
687
+ update: {
688
+ sessionUpdate: "usage_update",
689
+ used: lastAssistantTotalUsage,
690
+ size: session.contextWindowSize,
691
+ cost: {
692
+ amount: message.total_cost_usd,
693
+ currency: "USD",
694
+ },
695
+ ...(message.origin && {
696
+ _meta: { "_claude/origin": message.origin },
697
+ }),
698
+ },
699
+ });
700
+ }
701
+ if (session.cancelled) {
702
+ if (!isTaskNotification) {
703
+ stopReason = "cancelled";
704
+ }
705
+ break;
706
+ }
707
+ switch (message.subtype) {
708
+ case "success": {
709
+ if (message.result.includes("Please run /login")) {
710
+ throw RequestError.authRequired();
711
+ }
712
+ if (message.stop_reason === "max_tokens") {
713
+ if (!isTaskNotification) {
714
+ stopReason = "max_tokens";
715
+ }
716
+ break;
717
+ }
718
+ if (message.is_error) {
719
+ throw RequestError.internalError(errorKindData(lastAssistantError), message.result);
720
+ }
721
+ // For local-only commands (no model invocation), the result
722
+ // text is the command output — forward it to the client.
723
+ // Task-notification followups never originate from a user
724
+ // slash command, so skip the forwarding for them.
725
+ if (isLocalOnlyCommand && !isTaskNotification) {
726
+ for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId, this.toolUseCache, this.client, this.logger)) {
727
+ await this.client.sessionUpdate(notification);
728
+ }
729
+ }
730
+ break;
731
+ }
732
+ case "error_during_execution": {
733
+ if (message.stop_reason === "max_tokens") {
734
+ if (!isTaskNotification) {
735
+ stopReason = "max_tokens";
736
+ }
737
+ break;
738
+ }
739
+ if (message.is_error) {
740
+ throw RequestError.internalError(errorKindData(lastAssistantError), message.errors.join(", ") || message.subtype);
741
+ }
742
+ if (!isTaskNotification) {
743
+ stopReason = "end_turn";
744
+ }
745
+ break;
746
+ }
747
+ case "error_max_budget_usd":
748
+ case "error_max_turns":
749
+ case "error_max_structured_output_retries":
750
+ if (message.is_error) {
751
+ throw RequestError.internalError(errorKindData(lastAssistantError), message.errors.join(", ") || message.subtype);
752
+ }
753
+ if (!isTaskNotification) {
754
+ stopReason = "max_turn_requests";
755
+ }
756
+ break;
757
+ default:
758
+ unreachable(message, this.logger);
759
+ break;
760
+ }
761
+ break;
762
+ }
763
+ case "stream_event": {
764
+ if (message.parent_tool_use_id === null &&
765
+ (message.event.type === "message_start" || message.event.type === "message_delta")) {
766
+ if (message.event.type === "message_start") {
767
+ lastAssistantUsage = snapshotFromUsage(message.event.message.usage);
768
+ const model = message.event.message.model;
769
+ if (model && model !== "<synthetic>") {
770
+ lastAssistantModel = model;
771
+ // Only upgrade from the default — once a `result` has given
772
+ // us an authoritative window, trust it over the heuristic.
773
+ // Model switches invalidate the cached window via
774
+ // `syncSessionConfigState`, which resets us back to the
775
+ // default so this branch runs again for the new model.
776
+ if (session.contextWindowSize === DEFAULT_CONTEXT_WINDOW) {
777
+ const inferred = inferContextWindowFromModel(model);
778
+ if (inferred !== null) {
779
+ session.contextWindowSize = inferred;
780
+ }
781
+ }
782
+ }
783
+ }
784
+ else {
785
+ const usage = message.event.usage;
786
+ const prev = lastAssistantUsage ?? ZERO_USAGE;
787
+ // Per Anthropic API, message_delta usage fields are *cumulative*;
788
+ // nullable fields (input_tokens and the cache fields) fall back
789
+ // to the prior snapshot when the server omits them from this
790
+ // delta. Only output_tokens is guaranteed non-null.
791
+ lastAssistantUsage = {
792
+ input_tokens: usage.input_tokens ?? prev.input_tokens,
793
+ output_tokens: usage.output_tokens,
794
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? prev.cache_read_input_tokens,
795
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? prev.cache_creation_input_tokens,
796
+ };
797
+ }
798
+ const nextUsage = totalTokens(lastAssistantUsage);
799
+ if (nextUsage !== lastAssistantTotalUsage) {
800
+ lastAssistantTotalUsage = nextUsage;
801
+ await this.client.sessionUpdate({
802
+ sessionId: params.sessionId,
803
+ update: {
804
+ sessionUpdate: "usage_update",
805
+ used: nextUsage,
806
+ size: session.contextWindowSize,
807
+ },
808
+ });
809
+ }
810
+ }
811
+ for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger, {
812
+ clientCapabilities: this.clientCapabilities,
813
+ cwd: session.cwd,
814
+ taskState: session.taskState,
815
+ })) {
816
+ await this.client.sessionUpdate(notification);
817
+ }
818
+ break;
819
+ }
820
+ case "user":
821
+ case "assistant": {
822
+ if (session.cancelled) {
823
+ break;
824
+ }
825
+ // Check for prompt replay
826
+ if (message.type === "user" && "uuid" in message && message.uuid) {
827
+ if (message.uuid === promptUuid) {
828
+ break;
829
+ }
830
+ const pending = session.pendingMessages.get(message.uuid);
831
+ if (pending) {
832
+ pending.resolve(false);
833
+ session.pendingMessages.delete(message.uuid);
834
+ handedOff = true;
835
+ // the current loop stops with end_turn,
836
+ // the loop of the next prompt continues running
837
+ return { stopReason: "end_turn", usage: sessionUsage(session) };
838
+ }
839
+ if ("isReplay" in message && message.isReplay) {
840
+ // not pending or unrelated replay message
841
+ break;
842
+ }
843
+ }
844
+ // Snapshot the latest top-level assistant usage and model so the
845
+ // next `result` can emit a usage_update tied to the right context
846
+ // window. Subagent messages are excluded to keep the snapshot
847
+ // aligned with what the user's current selection is producing.
848
+ if (message.type === "assistant" && message.parent_tool_use_id === null) {
849
+ lastAssistantUsage = snapshotFromUsage(message.message.usage);
850
+ lastAssistantTotalUsage = totalTokens(lastAssistantUsage);
851
+ if (message.message.model && message.message.model !== "<synthetic>") {
852
+ lastAssistantModel = message.message.model;
853
+ }
854
+ if (message.error) {
855
+ lastAssistantError = message.error;
856
+ }
857
+ }
858
+ // symphony-patch: every assistant message carries the final token
859
+ // usage of exactly one model call (subagent calls included via
860
+ // parent_tool_use_id), so surface it as a per-call bucket on
861
+ // usage_update._meta for call-by-call accounting downstream.
862
+ if (message.type === "assistant" &&
863
+ message.message.usage &&
864
+ message.message.model !== "<synthetic>") {
865
+ const callUsage = message.message.usage;
866
+ session.symphonyCallSeq = (session.symphonyCallSeq ?? 0) + 1;
867
+ await this.client.sessionUpdate({
868
+ sessionId: params.sessionId,
869
+ update: {
870
+ sessionUpdate: "usage_update",
871
+ used: lastAssistantTotalUsage ?? 0,
872
+ size: session.contextWindowSize,
873
+ _meta: {
874
+ "symphony/callUsage": {
875
+ seq: session.symphonyCallSeq,
876
+ inputTokens: callUsage.input_tokens ?? 0,
877
+ outputTokens: callUsage.output_tokens ?? 0,
878
+ cachedReadTokens: callUsage.cache_read_input_tokens ?? 0,
879
+ cachedWriteTokens: callUsage.cache_creation_input_tokens ?? 0,
880
+ totalTokens: (callUsage.input_tokens ?? 0) +
881
+ (callUsage.output_tokens ?? 0) +
882
+ (callUsage.cache_read_input_tokens ?? 0) +
883
+ (callUsage.cache_creation_input_tokens ?? 0),
884
+ },
885
+ },
886
+ },
887
+ });
888
+ }
889
+ // Strip <command-*>/<local-command-stdout> markers and render any
890
+ // remaining prose. Skill bodies and built-in slash commands (e.g.
891
+ // /usage, /status, /model) arrive wrapped in these tags; pure-marker
892
+ // payloads (e.g. /compact's malformed output) strip to null and are
893
+ // skipped. Mirrors the replay path at replaySessionHistory.
894
+ if (message.message.role !== "system" &&
895
+ typeof message.message.content === "string" &&
896
+ message.message.content.includes("<local-command-stdout>")) {
897
+ const stripped = stripLocalCommandMetadata(message.message.content);
898
+ if (typeof stripped === "string") {
899
+ for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
900
+ clientCapabilities: this.clientCapabilities,
901
+ parentToolUseId: message.parent_tool_use_id,
902
+ cwd: session.cwd,
903
+ taskState: session.taskState,
904
+ })) {
905
+ await this.client.sessionUpdate(notification);
906
+ }
907
+ }
908
+ else {
909
+ this.logger.log(message.message.content);
910
+ }
911
+ break;
912
+ }
913
+ if (typeof message.message.content === "string" &&
914
+ message.message.content.includes("<local-command-stderr>")) {
915
+ this.logger.error(message.message.content);
916
+ break;
917
+ }
918
+ // Skip these user messages for now, since they seem to just be messages we don't want in the feed
919
+ if (message.type === "user" &&
920
+ (typeof message.message.content === "string" ||
921
+ (Array.isArray(message.message.content) &&
922
+ message.message.content.length === 1 &&
923
+ message.message.content[0].type === "text"))) {
924
+ break;
925
+ }
926
+ if (message.message.role === "system") {
927
+ break;
928
+ }
929
+ if (message.type === "assistant" &&
930
+ message.message.model === "<synthetic>" &&
931
+ Array.isArray(message.message.content) &&
932
+ message.message.content.length === 1 &&
933
+ message.message.content[0].type === "text" &&
934
+ message.message.content[0].text.includes("Please run /login")) {
935
+ throw RequestError.authRequired();
936
+ }
937
+ const content = message.type === "assistant"
938
+ ? // Handled by stream events above
939
+ message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
940
+ : message.message.content;
941
+ for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
942
+ clientCapabilities: this.clientCapabilities,
943
+ parentToolUseId: message.parent_tool_use_id,
944
+ cwd: session.cwd,
945
+ taskState: session.taskState,
946
+ })) {
947
+ await this.client.sessionUpdate(notification);
948
+ }
949
+ break;
950
+ }
951
+ case "tool_progress":
952
+ case "tool_use_summary":
953
+ case "auth_status":
954
+ case "prompt_suggestion":
955
+ case "rate_limit_event":
956
+ break;
957
+ default:
958
+ unreachable(message);
959
+ break;
960
+ }
961
+ }
962
+ throw new Error("Session did not end in result");
963
+ }
964
+ catch (error) {
965
+ errored = true;
966
+ // A failed turn typically leaves a trailing `session_state_changed: idle`
967
+ // (and possibly more) in the query iterator. If we don't drain it here,
968
+ // the next prompt's first `query.next()` consumes that stale idle and
969
+ // short-circuits to end_turn with zero usage
970
+ // Bounded so a misbehaving SDK can't hang the next prompt indefinitely.
971
+ try {
972
+ await session.query.interrupt();
973
+ const MAX_DRAIN = 100;
974
+ for (let i = 0; i < MAX_DRAIN; i++) {
975
+ const { value: m, done } = await session.query.next();
976
+ if (done || !m)
977
+ break;
978
+ if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
979
+ break;
980
+ }
981
+ if (i === MAX_DRAIN - 1) {
982
+ this.logger.error(`Session ${params.sessionId}: drained ${MAX_DRAIN} messages after error without observing idle`);
983
+ }
984
+ }
985
+ }
986
+ catch (drainErr) {
987
+ this.logger.error(`Session ${params.sessionId}: failed to drain query after prompt error:`, drainErr);
988
+ }
989
+ if (error instanceof RequestError || !(error instanceof Error)) {
990
+ throw error;
991
+ }
992
+ const message = error.message;
993
+ if (message.includes("ProcessTransport") ||
994
+ message.includes("terminated process") ||
995
+ message.includes("process exited with") ||
996
+ message.includes("process terminated by signal") ||
997
+ message.includes("Failed to write to process stdin")) {
998
+ this.logger.error(`Session ${params.sessionId}: Claude Agent process died: ${message}`);
999
+ session.settingsManager.dispose();
1000
+ session.input.end();
1001
+ delete this.sessions[params.sessionId];
1002
+ throw RequestError.internalError(undefined, "The Claude Agent process exited unexpectedly. Please start a new session.");
1003
+ }
1004
+ throw error;
1005
+ }
1006
+ finally {
1007
+ if (!handedOff) {
1008
+ session.promptRunning = false;
1009
+ if (errored) {
1010
+ // The query stream was just drained — handing pending prompts off
1011
+ // onto it would let them race with the recovery. Cancel them so
1012
+ // each waiting prompt() returns stopReason: "cancelled" and the
1013
+ // client can decide whether to retry.
1014
+ for (const pending of session.pendingMessages.values()) {
1015
+ pending.resolve(true);
1016
+ }
1017
+ session.pendingMessages.clear();
1018
+ }
1019
+ else if (session.pendingMessages.size > 0) {
1020
+ // This usually should not happen, but in case the loop finishes
1021
+ // without claude sending all message replays, we resolve the
1022
+ // next pending prompt call to ensure no prompts get stuck.
1023
+ const next = [...session.pendingMessages.entries()].sort((a, b) => a[1].order - b[1].order)[0];
1024
+ if (next) {
1025
+ next[1].resolve(false);
1026
+ session.pendingMessages.delete(next[0]);
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ async cancel(params) {
1033
+ const session = this.sessions[params.sessionId];
1034
+ if (!session) {
1035
+ return;
1036
+ }
1037
+ session.cancelled = true;
1038
+ for (const [, pending] of session.pendingMessages) {
1039
+ pending.resolve(true);
1040
+ }
1041
+ session.pendingMessages.clear();
1042
+ await session.query.interrupt();
1043
+ }
1044
+ /** Cleanly tear down a session: cancel in-flight work, dispose resources,
1045
+ * and remove it from the session map. */
1046
+ async teardownSession(sessionId) {
1047
+ const session = this.sessions[sessionId];
1048
+ if (!session) {
1049
+ return;
1050
+ }
1051
+ await this.cancel({ sessionId });
1052
+ session.settingsManager.dispose();
1053
+ session.abortController.abort();
1054
+ session.query.close();
1055
+ delete this.sessions[sessionId];
1056
+ }
1057
+ /** Tear down all active sessions. Called when the ACP connection closes. */
1058
+ async dispose() {
1059
+ await Promise.all(Object.keys(this.sessions).map((id) => this.teardownSession(id)));
1060
+ }
1061
+ async closeSession(params) {
1062
+ if (!this.sessions[params.sessionId]) {
1063
+ throw new Error("Session not found");
1064
+ }
1065
+ await this.teardownSession(params.sessionId);
1066
+ return {};
1067
+ }
1068
+ async unstable_deleteSession(params) {
1069
+ // Tear down any active in-memory state first so the on-disk file isn't
1070
+ // recreated by an outstanding query writing to it.
1071
+ if (this.sessions[params.sessionId]) {
1072
+ await this.teardownSession(params.sessionId);
1073
+ }
1074
+ await deleteSession(params.sessionId);
1075
+ return {};
1076
+ }
1077
+ async setSessionMode(params) {
1078
+ if (!this.sessions[params.sessionId]) {
1079
+ throw new Error("Session not found");
1080
+ }
1081
+ await this.applySessionMode(params.sessionId, params.modeId);
1082
+ await this.updateConfigOption(params.sessionId, "mode", params.modeId);
1083
+ return {};
1084
+ }
1085
+ async setSessionConfigOption(params) {
1086
+ const session = this.sessions[params.sessionId];
1087
+ if (!session) {
1088
+ throw new Error("Session not found");
1089
+ }
1090
+ if (typeof params.value !== "string") {
1091
+ throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
1092
+ }
1093
+ const option = session.configOptions.find((o) => o.id === params.configId);
1094
+ if (!option) {
1095
+ throw new Error(`Unknown config option: ${params.configId}`);
1096
+ }
1097
+ const allValues = "options" in option && Array.isArray(option.options)
1098
+ ? option.options.flatMap((o) => ("options" in o ? o.options : [o]))
1099
+ : [];
1100
+ let validValue = allValues.find((o) => o.value === params.value);
1101
+ // For model options, fall back to resolveModelPreference when the exact
1102
+ // value doesn't match. This lets callers use human-friendly aliases like
1103
+ // "opus" or "sonnet" instead of full model IDs like "claude-opus-4-6".
1104
+ if (!validValue && params.configId === "model") {
1105
+ const modelInfos = allValues.map((o) => ({
1106
+ value: o.value,
1107
+ displayName: o.name,
1108
+ description: o.description ?? "",
1109
+ }));
1110
+ const resolved = resolveModelPreference(modelInfos, params.value);
1111
+ if (resolved) {
1112
+ validValue = allValues.find((o) => o.value === resolved.value);
1113
+ }
1114
+ }
1115
+ if (!validValue) {
1116
+ throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
1117
+ }
1118
+ // Use the canonical option value so downstream code always receives the
1119
+ // model ID rather than the caller-supplied alias.
1120
+ const resolvedValue = validValue.value;
1121
+ if (params.configId === "mode") {
1122
+ await this.applySessionMode(params.sessionId, resolvedValue);
1123
+ await this.client.sessionUpdate({
1124
+ sessionId: params.sessionId,
1125
+ update: {
1126
+ sessionUpdate: "current_mode_update",
1127
+ currentModeId: resolvedValue,
1128
+ },
1129
+ });
1130
+ }
1131
+ else if (params.configId === "model") {
1132
+ await this.sessions[params.sessionId].query.setModel(resolvedValue);
1133
+ }
1134
+ // Effort SDK sync is handled inside applyConfigOptionValue so that direct
1135
+ // effort changes and effort changes induced by a model switch go through
1136
+ // the same path.
1137
+ await this.applyConfigOptionValue(params.sessionId, session, params.configId, resolvedValue);
1138
+ return { configOptions: session.configOptions };
1139
+ }
1140
+ async applySessionMode(sessionId, modeId) {
1141
+ switch (modeId) {
1142
+ case "auto":
1143
+ case "default":
1144
+ case "acceptEdits":
1145
+ case "bypassPermissions":
1146
+ case "dontAsk":
1147
+ case "plan":
1148
+ break;
1149
+ default:
1150
+ throw new Error("Invalid Mode");
1151
+ }
1152
+ const session = this.sessions[sessionId];
1153
+ if (!session) {
1154
+ throw new Error("Session not found");
1155
+ }
1156
+ if (!session.modes.availableModes.some((mode) => mode.id === modeId)) {
1157
+ throw new Error(`Mode ${modeId} is not available in this session`);
1158
+ }
1159
+ try {
1160
+ await session.query.setPermissionMode(modeId);
1161
+ }
1162
+ catch (error) {
1163
+ if (error instanceof Error) {
1164
+ if (!error.message) {
1165
+ error.message = "Invalid Mode";
1166
+ }
1167
+ throw error;
1168
+ }
1169
+ else {
1170
+ // eslint-disable-next-line preserve-caught-error
1171
+ throw new Error("Invalid Mode");
1172
+ }
1173
+ }
1174
+ }
1175
+ async replaySessionHistory(sessionId) {
1176
+ const toolUseCache = {};
1177
+ const messages = await getSessionMessages(sessionId);
1178
+ for (const message of messages) {
1179
+ // @ts-expect-error - untyped in SDK but we handle all of these
1180
+ let content = message.message.content;
1181
+ // @ts-expect-error - untyped in SDK but we handle all of these
1182
+ if (message.message.role === "user") {
1183
+ content = stripLocalCommandMetadata(content);
1184
+ if (content === null)
1185
+ continue;
1186
+ }
1187
+ for (const notification of toAcpNotifications(
1188
+ // @ts-expect-error - untyped in SDK but we handle all of these
1189
+ content,
1190
+ // @ts-expect-error - untyped in SDK but we handle all of these
1191
+ message.message.role, sessionId, toolUseCache, this.client, this.logger, {
1192
+ registerHooks: false,
1193
+ clientCapabilities: this.clientCapabilities,
1194
+ cwd: this.sessions[sessionId]?.cwd,
1195
+ taskState: this.sessions[sessionId]?.taskState,
1196
+ })) {
1197
+ await this.client.sessionUpdate(notification);
1198
+ }
1199
+ }
1200
+ }
1201
+ async readTextFile(params) {
1202
+ const response = await this.client.readTextFile(params);
1203
+ return response;
1204
+ }
1205
+ async writeTextFile(params) {
1206
+ const response = await this.client.writeTextFile(params);
1207
+ return response;
1208
+ }
1209
+ canUseTool(sessionId) {
1210
+ return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
1211
+ const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
1212
+ const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
1213
+ const session = this.sessions[sessionId];
1214
+ if (!session) {
1215
+ return {
1216
+ behavior: "deny",
1217
+ message: "Session not found",
1218
+ };
1219
+ }
1220
+ if (toolName === "ExitPlanMode") {
1221
+ const optionsAll = [
1222
+ { kind: "allow_always", name: 'Yes, and use "auto" mode', optionId: "auto" },
1223
+ {
1224
+ kind: "allow_always",
1225
+ name: "Yes, and auto-accept edits",
1226
+ optionId: "acceptEdits",
1227
+ },
1228
+ { kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
1229
+ { kind: "reject_once", name: "No, keep planning", optionId: "plan" },
1230
+ ];
1231
+ if (ALLOW_BYPASS) {
1232
+ optionsAll.unshift({
1233
+ kind: "allow_always",
1234
+ name: "Yes, and bypass permissions",
1235
+ optionId: "bypassPermissions",
1236
+ });
1237
+ }
1238
+ // Filter against the session's currently-advertised modes so we never
1239
+ // present options the active model can't honor (e.g. `auto` on Haiku).
1240
+ // `bypassPermissions` is already covered by `availableModes` via
1241
+ // `buildAvailableModes`/`ALLOW_BYPASS`. The `plan` option is a
1242
+ // "keep planning" reject path; it's always present in `availableModes`.
1243
+ const options = optionsAll.filter((o) => session.modes.availableModes.some((m) => m.id === o.optionId));
1244
+ const response = await this.client.requestPermission({
1245
+ options,
1246
+ sessionId,
1247
+ toolCall: {
1248
+ toolCallId: toolUseID,
1249
+ rawInput: toolInput,
1250
+ ...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
1251
+ },
1252
+ });
1253
+ if (signal.aborted || response.outcome?.outcome === "cancelled") {
1254
+ throw new Error("Tool use aborted");
1255
+ }
1256
+ const selectedMode = response.outcome?.outcome === "selected" ? response.outcome.optionId : undefined;
1257
+ const selectedModeWasOffered = options.some((option) => option.optionId === selectedMode);
1258
+ if (selectedModeWasOffered &&
1259
+ (selectedMode === "default" ||
1260
+ selectedMode === "acceptEdits" ||
1261
+ selectedMode === "auto" ||
1262
+ selectedMode === "bypassPermissions")) {
1263
+ await this.client.sessionUpdate({
1264
+ sessionId,
1265
+ update: {
1266
+ sessionUpdate: "current_mode_update",
1267
+ currentModeId: selectedMode,
1268
+ },
1269
+ });
1270
+ await this.updateConfigOption(sessionId, "mode", selectedMode);
1271
+ return {
1272
+ behavior: "allow",
1273
+ updatedInput: toolInput,
1274
+ updatedPermissions: suggestions ?? [
1275
+ { type: "setMode", mode: selectedMode, destination: "session" },
1276
+ ],
1277
+ };
1278
+ }
1279
+ else {
1280
+ return {
1281
+ behavior: "deny",
1282
+ message: "User rejected request to exit plan mode.",
1283
+ };
1284
+ }
1285
+ }
1286
+ if (session.modes.currentModeId === "bypassPermissions") {
1287
+ return {
1288
+ behavior: "allow",
1289
+ updatedInput: toolInput,
1290
+ updatedPermissions: suggestions ?? [
1291
+ { type: "addRules", rules: [{ toolName }], behavior: "allow", destination: "session" },
1292
+ ],
1293
+ };
1294
+ }
1295
+ const response = await this.client.requestPermission({
1296
+ options: [
1297
+ {
1298
+ kind: "allow_always",
1299
+ name: alwaysAllowLabel,
1300
+ optionId: "allow_always",
1301
+ },
1302
+ { kind: "allow_once", name: "Allow", optionId: "allow" },
1303
+ { kind: "reject_once", name: "Reject", optionId: "reject" },
1304
+ ],
1305
+ sessionId,
1306
+ toolCall: {
1307
+ toolCallId: toolUseID,
1308
+ rawInput: toolInput,
1309
+ ...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
1310
+ },
1311
+ });
1312
+ if (signal.aborted || response.outcome?.outcome === "cancelled") {
1313
+ throw new Error("Tool use aborted");
1314
+ }
1315
+ if (response.outcome?.outcome === "selected" &&
1316
+ (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
1317
+ // If Claude Code has suggestions, it will update their settings already
1318
+ if (response.outcome.optionId === "allow_always") {
1319
+ return {
1320
+ behavior: "allow",
1321
+ updatedInput: toolInput,
1322
+ updatedPermissions: suggestions ?? [
1323
+ {
1324
+ type: "addRules",
1325
+ rules: [{ toolName }],
1326
+ behavior: "allow",
1327
+ destination: "session",
1328
+ },
1329
+ ],
1330
+ };
1331
+ }
1332
+ return {
1333
+ behavior: "allow",
1334
+ updatedInput: toolInput,
1335
+ };
1336
+ }
1337
+ else {
1338
+ return {
1339
+ behavior: "deny",
1340
+ message: "User refused permission to run tool",
1341
+ };
1342
+ }
1343
+ };
1344
+ }
1345
+ async sendAvailableCommandsUpdate(sessionId) {
1346
+ const session = this.sessions[sessionId];
1347
+ if (!session)
1348
+ return;
1349
+ const commands = await session.query.supportedCommands();
1350
+ await this.client.sessionUpdate({
1351
+ sessionId,
1352
+ update: {
1353
+ sessionUpdate: "available_commands_update",
1354
+ availableCommands: getAvailableSlashCommands(commands),
1355
+ },
1356
+ });
1357
+ }
1358
+ async updateConfigOption(sessionId, configId, value) {
1359
+ const session = this.sessions[sessionId];
1360
+ if (!session)
1361
+ return;
1362
+ await this.applyConfigOptionValue(sessionId, session, configId, value);
1363
+ await this.client.sessionUpdate({
1364
+ sessionId,
1365
+ update: {
1366
+ sessionUpdate: "config_option_update",
1367
+ configOptions: session.configOptions,
1368
+ },
1369
+ });
1370
+ }
1371
+ async applyConfigOptionValue(sessionId, session, configId, value) {
1372
+ if (configId === "mode") {
1373
+ session.modes = { ...session.modes, currentModeId: value };
1374
+ session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
1375
+ }
1376
+ else if (configId === "model") {
1377
+ if (session.models.currentModelId !== value) {
1378
+ // The cached context window was learned for the previous model; reset
1379
+ // to the new model's heuristic so mid-stream updates between now and
1380
+ // the next `result` reflect the user's selection instead of the old
1381
+ // model's window.
1382
+ session.contextWindowSize = inferContextWindowFromModel(value) ?? DEFAULT_CONTEXT_WINDOW;
1383
+ }
1384
+ session.models = { ...session.models, currentModelId: value };
1385
+ // Recompute availableModes for the new model and clamp the current
1386
+ // mode if the SDK no longer offers it (today: "auto" on Haiku).
1387
+ // `ModelInfo.supportsAutoMode` is the canonical SDK signal.
1388
+ const newModelInfo = session.modelInfos.find((m) => m.value === value);
1389
+ const newAvailableModes = buildAvailableModes(newModelInfo);
1390
+ // Capture BEFORE mutating session.modes so the log message reflects
1391
+ // the invalidated mode rather than "default".
1392
+ const previousModeId = session.modes.currentModeId;
1393
+ let modeDowngraded = false;
1394
+ if (!newAvailableModes.some((m) => m.id === previousModeId)) {
1395
+ session.modes = {
1396
+ availableModes: newAvailableModes,
1397
+ currentModeId: "default",
1398
+ };
1399
+ try {
1400
+ await session.query.setPermissionMode("default");
1401
+ }
1402
+ catch (err) {
1403
+ // Failing the entire model switch over a bookkeeping sync error is
1404
+ // worse UX than logging and continuing; the user explicitly asked
1405
+ // to change models. The next setPermissionMode from the user will
1406
+ // either succeed or surface a fresh error.
1407
+ this.logger.error(`Failed to sync permissionMode to "default" after model switch invalidated "${previousModeId}":`, err);
1408
+ }
1409
+ modeDowngraded = true;
1410
+ }
1411
+ else {
1412
+ session.modes = { ...session.modes, availableModes: newAvailableModes };
1413
+ }
1414
+ // Rebuild config options since effort levels depend on the selected model
1415
+ const effortOpt = session.configOptions.find((o) => o.id === "effort");
1416
+ const currentEffort = typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
1417
+ session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, currentEffort);
1418
+ // Sync effort with the SDK if it changed after the model switch
1419
+ const newEffortOpt = session.configOptions.find((o) => o.id === "effort");
1420
+ const newEffort = typeof newEffortOpt?.currentValue === "string" ? newEffortOpt.currentValue : undefined;
1421
+ if (newEffort !== currentEffort) {
1422
+ await session.query.applyFlagSettings({
1423
+ effortLevel: toSdkEffortLevel(newEffort),
1424
+ });
1425
+ }
1426
+ // Emit current_mode_update only after session.modes AND
1427
+ // session.configOptions have been fully reconciled. This way, a failure
1428
+ // in the configOptions/effort rebuild above can't leave the client with
1429
+ // a clamped currentModeId but stale configOptions, and the notification
1430
+ // still precedes the caller's config_option_update so order-sensitive
1431
+ // clients update currentModeId before re-rendering the option list.
1432
+ if (modeDowngraded) {
1433
+ await this.client.sessionUpdate({
1434
+ sessionId,
1435
+ update: {
1436
+ sessionUpdate: "current_mode_update",
1437
+ currentModeId: "default",
1438
+ },
1439
+ });
1440
+ }
1441
+ }
1442
+ else {
1443
+ session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
1444
+ if (configId === "effort") {
1445
+ await session.query.applyFlagSettings({
1446
+ effortLevel: toSdkEffortLevel(value),
1447
+ });
1448
+ }
1449
+ }
1450
+ }
1451
+ async getOrCreateSession(params) {
1452
+ const existingSession = this.sessions[params.sessionId];
1453
+ if (existingSession) {
1454
+ const fingerprint = computeSessionFingerprint(params);
1455
+ if (fingerprint === existingSession.sessionFingerprint) {
1456
+ return {
1457
+ sessionId: params.sessionId,
1458
+ modes: existingSession.modes,
1459
+ configOptions: existingSession.configOptions,
1460
+ };
1461
+ }
1462
+ // Session-defining params changed (e.g. cwd pointed at a git worktree,
1463
+ // or MCP servers reconfigured). Tear down the existing session and
1464
+ // recreate it so the underlying Query process picks up the new values.
1465
+ await this.teardownSession(params.sessionId);
1466
+ }
1467
+ const response = await this.createSession({
1468
+ cwd: params.cwd,
1469
+ mcpServers: params.mcpServers ?? [],
1470
+ additionalDirectories: params.additionalDirectories,
1471
+ _meta: params._meta,
1472
+ }, {
1473
+ resume: params.sessionId,
1474
+ });
1475
+ return {
1476
+ sessionId: response.sessionId,
1477
+ modes: response.modes,
1478
+ configOptions: response.configOptions,
1479
+ };
1480
+ }
1481
+ async createSession(params, creationOpts = {}) {
1482
+ // We want to create a new session id unless it is resume,
1483
+ // but not resume + forkSession.
1484
+ let sessionId;
1485
+ if (creationOpts.forkSession) {
1486
+ sessionId = randomUUID();
1487
+ }
1488
+ else if (creationOpts.resume) {
1489
+ sessionId = creationOpts.resume;
1490
+ }
1491
+ else {
1492
+ sessionId = randomUUID();
1493
+ }
1494
+ const input = new Pushable();
1495
+ const settingsManager = new SettingsManager(params.cwd, {
1496
+ logger: this.logger,
1497
+ });
1498
+ await settingsManager.initialize();
1499
+ // symphony-patch: overlay per-session settings supplied via session
1500
+ // _meta (settings.json shape) on top of the resolved file settings.
1501
+ // Everything the bridge derives from settings (permissions.defaultMode,
1502
+ // model, availableModels, effortLevel) then works per session without
1503
+ // writing settings files into the workspace.
1504
+ const symphonySettings = params._meta?.["symphony/settings"];
1505
+ if (symphonySettings &&
1506
+ typeof symphonySettings === "object" &&
1507
+ !Array.isArray(symphonySettings)) {
1508
+ const baseGetSettings = settingsManager.getSettings.bind(settingsManager);
1509
+ settingsManager.getSettings = () => mergeSymphonySettings(baseGetSettings(), symphonySettings);
1510
+ }
1511
+ const mcpServers = {};
1512
+ if (Array.isArray(params.mcpServers)) {
1513
+ for (const server of params.mcpServers) {
1514
+ if ("type" in server && (server.type === "http" || server.type === "sse")) {
1515
+ // HTTP or SSE type MCP server
1516
+ mcpServers[server.name] = {
1517
+ type: server.type,
1518
+ url: server.url,
1519
+ headers: server.headers
1520
+ ? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
1521
+ : undefined,
1522
+ };
1523
+ }
1524
+ else if (!("type" in server)) {
1525
+ // Stdio type MCP server (with or without explicit type field)
1526
+ mcpServers[server.name] = {
1527
+ type: "stdio",
1528
+ command: server.command,
1529
+ args: server.args,
1530
+ env: server.env
1531
+ ? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
1532
+ : undefined,
1533
+ };
1534
+ }
1535
+ }
1536
+ }
1537
+ let systemPrompt = { type: "preset", preset: "claude_code" };
1538
+ if (params._meta?.systemPrompt) {
1539
+ const customPrompt = params._meta.systemPrompt;
1540
+ if (typeof customPrompt === "string") {
1541
+ systemPrompt = customPrompt;
1542
+ }
1543
+ else if (typeof customPrompt === "object" &&
1544
+ customPrompt !== null &&
1545
+ !Array.isArray(customPrompt)) {
1546
+ // Forward all preset options (append, excludeDynamicSections, and
1547
+ // anything the SDK adds later) while locking type/preset.
1548
+ systemPrompt = {
1549
+ ...customPrompt,
1550
+ type: "preset",
1551
+ preset: "claude_code",
1552
+ };
1553
+ }
1554
+ }
1555
+ const permissionMode = resolvePermissionMode(settingsManager.getSettings().permissions?.defaultMode, this.logger);
1556
+ // Extract options from _meta if provided
1557
+ const sessionMeta = params._meta;
1558
+ const userProvidedOptions = sessionMeta?.claudeCode?.options;
1559
+ // Configure thinking tokens from environment variable
1560
+ const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
1561
+ ? parseInt(process.env.MAX_THINKING_TOKENS, 10)
1562
+ : undefined;
1563
+ // Parse model configuration from environment (e.g. Bedrock model overrides)
1564
+ const modelConfig = parseModelConfig(process.env.CLAUDE_MODEL_CONFIG);
1565
+ // Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
1566
+ const disallowedTools = ["AskUserQuestion"];
1567
+ // Resolve which built-in tools to expose.
1568
+ // Explicit tools array from _meta.claudeCode.options takes precedence.
1569
+ // disableBuiltInTools is a legacy shorthand for tools: [] — kept for
1570
+ // backward compatibility but callers should prefer the tools array.
1571
+ const tools = userProvidedOptions?.tools ??
1572
+ (params._meta?.disableBuiltInTools === true ? [] : { type: "preset", preset: "claude_code" });
1573
+ const abortController = userProvidedOptions?.abortController || new AbortController();
1574
+ // Per-session task state. Created here (rather than in the session record
1575
+ // below) so the TaskCreated/TaskCompleted hook callbacks can close over
1576
+ // the same Map that the streaming message handler will read from.
1577
+ const taskState = new Map();
1578
+ const options = {
1579
+ systemPrompt,
1580
+ settingSources: ["user", "project", "local"],
1581
+ ...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
1582
+ ...userProvidedOptions,
1583
+ // CLAUDE_MODEL_CONFIG env var is a fallback for model
1584
+ // configuration (e.g. Bedrock model ID overrides). When the caller
1585
+ // provides settings via _meta, we intentionally ignore the env var —
1586
+ // the caller is assumed to have full control over model configuration.
1587
+ ...(!userProvidedOptions?.settings &&
1588
+ modelConfig && {
1589
+ settings: {
1590
+ ...(modelConfig.modelOverrides && { modelOverrides: modelConfig.modelOverrides }),
1591
+ ...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
1592
+ },
1593
+ }),
1594
+ env: {
1595
+ ...process.env,
1596
+ ...userProvidedOptions?.env,
1597
+ ...createEnvForGateway(this.gatewayAuthRequest),
1598
+ // Opt-in to session state events like when the agent is idle
1599
+ CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
1600
+ },
1601
+ // Override certain fields that must be controlled by ACP
1602
+ cwd: params.cwd,
1603
+ includePartialMessages: true,
1604
+ mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
1605
+ // If we want bypassPermissions to be an option, we have to allow it here.
1606
+ // But it doesn't work in root mode, so we only activate it if it will work.
1607
+ allowDangerouslySkipPermissions: ALLOW_BYPASS,
1608
+ permissionMode,
1609
+ canUseTool: this.canUseTool(sessionId),
1610
+ pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),
1611
+ extraArgs: {
1612
+ ...userProvidedOptions?.extraArgs,
1613
+ "replay-user-messages": "",
1614
+ },
1615
+ disallowedTools: [...(userProvidedOptions?.disallowedTools || []), ...disallowedTools],
1616
+ tools,
1617
+ hooks: {
1618
+ ...userProvidedOptions?.hooks,
1619
+ PostToolUse: [
1620
+ ...(userProvidedOptions?.hooks?.PostToolUse || []),
1621
+ {
1622
+ hooks: [
1623
+ createPostToolUseHook(this.logger, {
1624
+ onEnterPlanMode: async () => {
1625
+ await this.client.sessionUpdate({
1626
+ sessionId,
1627
+ update: {
1628
+ sessionUpdate: "current_mode_update",
1629
+ currentModeId: "plan",
1630
+ },
1631
+ });
1632
+ await this.updateConfigOption(sessionId, "mode", "plan");
1633
+ },
1634
+ }),
1635
+ ],
1636
+ },
1637
+ ],
1638
+ TaskCreated: [
1639
+ ...(userProvidedOptions?.hooks?.TaskCreated || []),
1640
+ {
1641
+ hooks: [
1642
+ createTaskHook({
1643
+ taskState,
1644
+ onChange: async () => {
1645
+ await this.client.sessionUpdate({
1646
+ sessionId,
1647
+ update: {
1648
+ sessionUpdate: "plan",
1649
+ entries: taskStateToPlanEntries(taskState),
1650
+ },
1651
+ });
1652
+ },
1653
+ }),
1654
+ ],
1655
+ },
1656
+ ],
1657
+ TaskCompleted: [
1658
+ ...(userProvidedOptions?.hooks?.TaskCompleted || []),
1659
+ {
1660
+ hooks: [
1661
+ createTaskHook({
1662
+ taskState,
1663
+ onChange: async () => {
1664
+ await this.client.sessionUpdate({
1665
+ sessionId,
1666
+ update: {
1667
+ sessionUpdate: "plan",
1668
+ entries: taskStateToPlanEntries(taskState),
1669
+ },
1670
+ });
1671
+ },
1672
+ }),
1673
+ ],
1674
+ },
1675
+ ],
1676
+ },
1677
+ ...creationOpts,
1678
+ abortController,
1679
+ };
1680
+ // Prefer the official ACP `additionalDirectories` field. Fall back to the
1681
+ // legacy `_meta.additionalRoots` extension for clients that haven't been
1682
+ // updated yet. Either source is merged with directories supplied via
1683
+ // `_meta.claudeCode.options.additionalDirectories` (SDK pass-through).
1684
+ const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
1685
+ options.additionalDirectories = [
1686
+ ...(userProvidedOptions?.additionalDirectories ?? []),
1687
+ ...acpAdditionalDirectories,
1688
+ ];
1689
+ if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
1690
+ // Set our own session id if not resuming an existing session.
1691
+ options.sessionId = sessionId;
1692
+ }
1693
+ // Handle abort controller from meta options
1694
+ if (abortController?.signal.aborted) {
1695
+ throw new Error("Cancelled");
1696
+ }
1697
+ const q = query({
1698
+ prompt: input,
1699
+ options,
1700
+ });
1701
+ let initializationResult;
1702
+ try {
1703
+ initializationResult = await q.initializationResult();
1704
+ }
1705
+ catch (error) {
1706
+ if (creationOpts.resume &&
1707
+ error instanceof Error &&
1708
+ (error.message === "Query closed before response received" ||
1709
+ error.message.includes("No conversation found with session ID"))) {
1710
+ throw RequestError.resourceNotFound(sessionId);
1711
+ }
1712
+ throw error;
1713
+ }
1714
+ if (shouldHideClaudeAuth() &&
1715
+ initializationResult.account.subscriptionType &&
1716
+ !this.gatewayAuthRequest) {
1717
+ throw RequestError.authRequired(undefined, "This integration does not support using claude.ai subscriptions.");
1718
+ }
1719
+ // Apply user's `availableModels` allowlist from settings.json before any
1720
+ // downstream model handling. The SDK only enforces this allowlist in its
1721
+ // own UI, not in `initializationResult.models`, so we filter here to keep
1722
+ // configOptions, the current-model resolver, and the stored modelInfos
1723
+ // consistent with what the user configured.
1724
+ const settingsAvailableModels = settingsManager.getSettings().availableModels;
1725
+ const allowedModels = Array.isArray(settingsAvailableModels)
1726
+ ? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels)
1727
+ : initializationResult.models;
1728
+ const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger);
1729
+ // Gate `auto` (and future model-specific modes) on the resolved model's
1730
+ // `ModelInfo`. See `buildAvailableModes` for the canonical SDK signal.
1731
+ const currentModelInfo = allowedModels.find((m) => m.value === models.currentModelId);
1732
+ const availableModes = buildAvailableModes(currentModelInfo);
1733
+ // Clamp `permissionMode` if the resolved session does not offer it. The
1734
+ // common case is `permissions.defaultMode: "auto"` resolving to a model
1735
+ // that does not support auto mode (e.g. Haiku); without this clamp the
1736
+ // SDK would later throw `"auto mode unavailable for this model"` from
1737
+ // `setPermissionMode`. Keep `permissionMode` as the resolved user intent
1738
+ // (matches what was passed into `options.permissionMode` above) and use
1739
+ // `effectiveMode` for the post-clamp value the session actually runs in.
1740
+ let effectiveMode = permissionMode;
1741
+ if (!availableModes.some((m) => m.id === effectiveMode)) {
1742
+ if (effectiveMode === "auto") {
1743
+ this.logger.error(`permissions.defaultMode "auto" is not available for model ` +
1744
+ `"${models.currentModelId}"; falling back to "default".`);
1745
+ }
1746
+ else {
1747
+ this.logger.error(`permissions.defaultMode "${effectiveMode}" is not available in ` +
1748
+ `this session; falling back to "default".`);
1749
+ }
1750
+ effectiveMode = "default";
1751
+ // Sync the SDK so it doesn't keep "auto" cached internally. Wrapped in
1752
+ // try/catch since failing here would abort session creation entirely.
1753
+ try {
1754
+ await q.setPermissionMode("default");
1755
+ }
1756
+ catch (err) {
1757
+ this.logger.error("Failed to sync clamped permissionMode to SDK:", err);
1758
+ }
1759
+ }
1760
+ const modes = {
1761
+ currentModeId: effectiveMode,
1762
+ availableModes,
1763
+ };
1764
+ const configOptions = buildConfigOptions(modes, models, allowedModels, settingsManager.getSettings().effortLevel);
1765
+ // Apply the initial effort level to the SDK so it matches the UI default
1766
+ const initialEffort = configOptions.find((o) => o.id === "effort");
1767
+ if (initialEffort &&
1768
+ typeof initialEffort.currentValue === "string" &&
1769
+ initialEffort.currentValue !== "default") {
1770
+ await q.applyFlagSettings({
1771
+ effortLevel: initialEffort.currentValue,
1772
+ });
1773
+ }
1774
+ this.sessions[sessionId] = {
1775
+ query: q,
1776
+ input: input,
1777
+ cancelled: false,
1778
+ cwd: params.cwd,
1779
+ sessionFingerprint: computeSessionFingerprint(params),
1780
+ settingsManager,
1781
+ accumulatedUsage: {
1782
+ inputTokens: 0,
1783
+ outputTokens: 0,
1784
+ cachedReadTokens: 0,
1785
+ cachedWriteTokens: 0,
1786
+ },
1787
+ modes,
1788
+ models,
1789
+ modelInfos: allowedModels,
1790
+ configOptions,
1791
+ promptRunning: false,
1792
+ pendingMessages: new Map(),
1793
+ nextPendingOrder: 0,
1794
+ abortController,
1795
+ emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
1796
+ contextWindowSize: inferContextWindowFromModel(models.currentModelId) ?? DEFAULT_CONTEXT_WINDOW,
1797
+ taskState,
1798
+ };
1799
+ return {
1800
+ sessionId,
1801
+ modes,
1802
+ configOptions,
1803
+ };
1804
+ }
1805
+ }
1806
+ function shouldEmitRawMessage(config, message) {
1807
+ if (config === true)
1808
+ return true;
1809
+ if (config === false)
1810
+ return false;
1811
+ return config.some((f) => f.type === message.type &&
1812
+ (f.subtype === undefined || f.subtype === message.subtype) &&
1813
+ (f.origin === undefined || f.origin === message.origin?.kind));
1814
+ }
1815
+ function sessionUsage(session) {
1816
+ return {
1817
+ inputTokens: session.accumulatedUsage.inputTokens,
1818
+ outputTokens: session.accumulatedUsage.outputTokens,
1819
+ cachedReadTokens: session.accumulatedUsage.cachedReadTokens,
1820
+ cachedWriteTokens: session.accumulatedUsage.cachedWriteTokens,
1821
+ totalTokens: session.accumulatedUsage.inputTokens +
1822
+ session.accumulatedUsage.outputTokens +
1823
+ session.accumulatedUsage.cachedReadTokens +
1824
+ session.accumulatedUsage.cachedWriteTokens,
1825
+ };
1826
+ }
1827
+ /** Sum all four fields as a proxy for post-turn context occupancy: the current
1828
+ * turn's output becomes next turn's input. Per the Anthropic API, input_tokens
1829
+ * excludes cache tokens — cache_read and cache_creation are reported
1830
+ * separately — so summing all four is not double-counting. */
1831
+ function totalTokens(usage) {
1832
+ return (usage.input_tokens +
1833
+ usage.output_tokens +
1834
+ usage.cache_read_input_tokens +
1835
+ usage.cache_creation_input_tokens);
1836
+ }
1837
+ /**
1838
+ * Build the `data` payload attached to a `RequestError.internalError` when we
1839
+ * have a categorical error from the Claude SDK. Returns `undefined` when no
1840
+ * categorical error is available, matching the previous behavior of passing
1841
+ * `undefined` to `RequestError.internalError`.
1842
+ *
1843
+ * The `errorKind` field is a convention for ACP clients to dispatch on
1844
+ * without having to pattern-match the human-readable message text. Clients
1845
+ * that don't understand it fall back to the existing message-based rendering.
1846
+ */
1847
+ function errorKindData(errorKind) {
1848
+ return errorKind ? { errorKind } : undefined;
1849
+ }
1850
+ // symphony-patch: two-level merge for per-session settings overlays. Nested
1851
+ // plain objects (e.g. permissions) merge key-wise so an overlay that only
1852
+ // sets permissions.defaultMode keeps the rest of the resolved permissions;
1853
+ // every other value replaces the base wholesale.
1854
+ function mergeSymphonySettings(base, overlay) {
1855
+ const merged = { ...base };
1856
+ for (const [key, value] of Object.entries(overlay)) {
1857
+ const baseValue = merged[key];
1858
+ const bothPlainObjects = value !== null &&
1859
+ typeof value === "object" &&
1860
+ !Array.isArray(value) &&
1861
+ baseValue !== null &&
1862
+ typeof baseValue === "object" &&
1863
+ !Array.isArray(baseValue);
1864
+ merged[key] = bothPlainObjects ? { ...baseValue, ...value } : value;
1865
+ }
1866
+ return merged;
1867
+ }
1868
+ /** Project a nullable API usage object into our non-null snapshot shape.
1869
+ * Both SDK message_start and assistant message `usage` have `number | null`
1870
+ * cache fields; we coerce absent values to 0 so `totalTokens` never hits
1871
+ * NaN. `input_tokens`/`output_tokens` are typed `number` by the SDK but
1872
+ * synthetic or third-party-backend stream events have been observed emitting
1873
+ * them as null/undefined — coerce those too so a malformed upstream event
1874
+ * can't leak NaN into the wire `used` field. Delta events have different
1875
+ * semantics (cumulative + prev fallback) and are handled inline. */
1876
+ function snapshotFromUsage(usage) {
1877
+ return {
1878
+ input_tokens: usage.input_tokens ?? 0,
1879
+ output_tokens: usage.output_tokens ?? 0,
1880
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
1881
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
1882
+ };
1883
+ }
1884
+ function createEnvForGateway(request) {
1885
+ if (!request?._meta) {
1886
+ return {};
1887
+ }
1888
+ const customHeaders = Object.entries(request._meta.gateway.headers)
1889
+ .map(([key, value]) => `${key}: ${value}`)
1890
+ .join("\n");
1891
+ if (request.methodId === "gateway-bedrock") {
1892
+ return {
1893
+ CLAUDE_CODE_USE_BEDROCK: "1",
1894
+ AWS_BEARER_TOKEN_BEDROCK: " ", // Must be non-empty to bypass pass configuration check
1895
+ ANTHROPIC_BEDROCK_BASE_URL: request._meta.gateway.baseUrl,
1896
+ ANTHROPIC_CUSTOM_HEADERS: customHeaders,
1897
+ };
1898
+ }
1899
+ return {
1900
+ ANTHROPIC_BASE_URL: request._meta.gateway.baseUrl,
1901
+ ANTHROPIC_CUSTOM_HEADERS: customHeaders,
1902
+ ANTHROPIC_AUTH_TOKEN: " ", // Must be specified to bypass claude login requirement
1903
+ };
1904
+ }
1905
+ /**
1906
+ * Build the list of permission modes the agent will advertise for the given
1907
+ * model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
1908
+ * the SDK's model-level availability signal. `undefined`/`false` both exclude
1909
+ * `auto`. `bypassPermissions` is still gated by `ALLOW_BYPASS`.
1910
+ */
1911
+ function buildAvailableModes(modelInfo) {
1912
+ const modes = [];
1913
+ // Only advertise "auto" when the SDK reports the model supports it.
1914
+ if (modelInfo?.supportsAutoMode === true) {
1915
+ modes.push({
1916
+ id: "auto",
1917
+ name: "Auto",
1918
+ description: "Use a model classifier to approve/deny permission prompts",
1919
+ });
1920
+ }
1921
+ modes.push({
1922
+ id: "default",
1923
+ name: "Default",
1924
+ description: "Standard behavior, prompts for dangerous operations",
1925
+ }, {
1926
+ id: "acceptEdits",
1927
+ name: "Accept Edits",
1928
+ description: "Auto-accept file edit operations",
1929
+ }, {
1930
+ id: "plan",
1931
+ name: "Plan Mode",
1932
+ description: "Planning mode, no actual tool execution",
1933
+ }, {
1934
+ id: "dontAsk",
1935
+ name: "Don't Ask",
1936
+ description: "Don't prompt for permissions, deny if not pre-approved",
1937
+ });
1938
+ if (ALLOW_BYPASS) {
1939
+ modes.push({
1940
+ id: "bypassPermissions",
1941
+ name: "Bypass Permissions",
1942
+ description: "Bypass all permission checks",
1943
+ });
1944
+ }
1945
+ return modes;
1946
+ }
1947
+ // Translate a UI effort value into the flag-layer payload. The SDK
1948
+ // shallow-merges `applyFlagSettings`, drops `undefined` during JSON transport,
1949
+ // and only clears a key when an explicit `null` is sent — see
1950
+ // `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
1951
+ // `"default"` sentinel and `undefined` (effort option absent for the model) to
1952
+ // `null` ensures any previously-applied flag is actually cleared.
1953
+ function toSdkEffortLevel(value) {
1954
+ return value === undefined || value === "default" ? null : value;
1955
+ }
1956
+ function buildConfigOptions(modes, models, modelInfos, currentEffortLevel) {
1957
+ const options = [
1958
+ {
1959
+ id: "mode",
1960
+ name: "Mode",
1961
+ description: "Session permission mode",
1962
+ category: "mode",
1963
+ type: "select",
1964
+ currentValue: modes.currentModeId,
1965
+ options: modes.availableModes.map((m) => ({
1966
+ value: m.id,
1967
+ name: m.name,
1968
+ description: m.description,
1969
+ })),
1970
+ },
1971
+ {
1972
+ id: "model",
1973
+ name: "Model",
1974
+ description: "AI model to use",
1975
+ category: "model",
1976
+ type: "select",
1977
+ currentValue: models.currentModelId,
1978
+ options: models.availableModels.map((m) => ({
1979
+ value: m.modelId,
1980
+ name: m.name,
1981
+ description: m.description ?? undefined,
1982
+ })),
1983
+ },
1984
+ ];
1985
+ // Add effort level option based on the currently selected model
1986
+ const currentModelInfo = modelInfos.find((m) => m.value === models.currentModelId);
1987
+ const supportedLevels = currentModelInfo?.supportsEffort
1988
+ ? (currentModelInfo.supportedEffortLevels ?? [])
1989
+ : [];
1990
+ if (supportedLevels.length > 0) {
1991
+ const effortOptions = [
1992
+ { value: "default", name: "Default" },
1993
+ ...supportedLevels.map((level) => ({
1994
+ value: level,
1995
+ name: level
1996
+ .split(/[_-]/)
1997
+ .map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
1998
+ .join(" "),
1999
+ })),
2000
+ ];
2001
+ const includes = (l) => l === "default" || supportedLevels.includes(l);
2002
+ const validEffort = currentEffortLevel && includes(currentEffortLevel) ? currentEffortLevel : "default";
2003
+ options.push({
2004
+ id: "effort",
2005
+ name: "Effort",
2006
+ description: "Available effort levels for this model",
2007
+ category: "thought_level",
2008
+ type: "select",
2009
+ currentValue: validEffort,
2010
+ options: effortOptions,
2011
+ });
2012
+ }
2013
+ return options;
2014
+ }
2015
+ // Claude Code CLI persists display strings like "opus[1m]" in settings,
2016
+ // but the SDK model list uses IDs like "claude-opus-4-6-1m".
2017
+ const MODEL_CONTEXT_HINT_PATTERN = /\[(\d+m)\]$/i;
2018
+ // Captures a model family version such as `4-6` or `4.7` so we can keep
2019
+ // `claude-opus-4-6` from being copied onto the SDK's `opus` alias when that
2020
+ // alias currently resolves to a different family version (e.g. Opus 4.7).
2021
+ const MODEL_FAMILY_VERSION_PATTERN = /\b(\d+)[-.](\d+)\b/;
2022
+ function extractModelFamilyVersion(s) {
2023
+ const match = s.match(MODEL_FAMILY_VERSION_PATTERN);
2024
+ return match ? `${match[1]}.${match[2]}` : null;
2025
+ }
2026
+ function modelVersionsCompatible(preference, candidate) {
2027
+ const preferred = extractModelFamilyVersion(preference);
2028
+ if (!preferred)
2029
+ return true;
2030
+ const candidateVersion = extractModelFamilyVersion(candidate.value) ??
2031
+ extractModelFamilyVersion(candidate.displayName) ??
2032
+ extractModelFamilyVersion(candidate.description);
2033
+ if (!candidateVersion)
2034
+ return true;
2035
+ return preferred === candidateVersion;
2036
+ }
2037
+ function tokenizeModelPreference(model) {
2038
+ const lower = model.trim().toLowerCase();
2039
+ const contextHint = lower.match(MODEL_CONTEXT_HINT_PATTERN)?.[1]?.toLowerCase();
2040
+ const normalized = lower.replace(MODEL_CONTEXT_HINT_PATTERN, " $1 ");
2041
+ const rawTokens = normalized.split(/[^a-z0-9]+/).filter(Boolean);
2042
+ const tokens = rawTokens
2043
+ .map((token) => {
2044
+ if (token === "opusplan")
2045
+ return "opus";
2046
+ if (token === "best" || token === "default")
2047
+ return "";
2048
+ return token;
2049
+ })
2050
+ .filter((token) => token && token !== "claude")
2051
+ .filter((token) => /[a-z]/.test(token) || token.endsWith("m"));
2052
+ return { tokens, contextHint };
2053
+ }
2054
+ function scoreModelMatch(model, tokens, contextHint) {
2055
+ const haystack = `${model.value} ${model.displayName}`.toLowerCase();
2056
+ let score = 0;
2057
+ for (const token of tokens) {
2058
+ if (haystack.includes(token)) {
2059
+ score += token === contextHint ? 3 : 1;
2060
+ }
2061
+ }
2062
+ return score;
2063
+ }
2064
+ function resolveModelPreference(models, preference) {
2065
+ const trimmed = preference.trim();
2066
+ if (!trimmed)
2067
+ return null;
2068
+ const lower = trimmed.toLowerCase();
2069
+ // Exact match on value or display name
2070
+ const directMatch = models.find((model) => model.value === trimmed ||
2071
+ model.value.toLowerCase() === lower ||
2072
+ model.displayName.toLowerCase() === lower);
2073
+ if (directMatch)
2074
+ return directMatch;
2075
+ // Substring match
2076
+ const includesMatch = models.find((model) => {
2077
+ if (!modelVersionsCompatible(trimmed, model))
2078
+ return false;
2079
+ const value = model.value.toLowerCase();
2080
+ const display = model.displayName.toLowerCase();
2081
+ return value.includes(lower) || display.includes(lower) || lower.includes(value);
2082
+ });
2083
+ if (includesMatch)
2084
+ return includesMatch;
2085
+ // Tokenized matching for aliases like "opus[1m]"
2086
+ const { tokens, contextHint } = tokenizeModelPreference(trimmed);
2087
+ if (tokens.length === 0)
2088
+ return null;
2089
+ let bestMatch = null;
2090
+ let bestScore = 0;
2091
+ for (const model of models) {
2092
+ if (!modelVersionsCompatible(trimmed, model))
2093
+ continue;
2094
+ const score = scoreModelMatch(model, tokens, contextHint);
2095
+ if (0 < score && (!bestMatch || bestScore < score)) {
2096
+ bestMatch = model;
2097
+ bestScore = score;
2098
+ }
2099
+ }
2100
+ return bestMatch;
2101
+ }
2102
+ function resolveSettingsModel(models, settingsModel, logger) {
2103
+ if (settingsModel === undefined) {
2104
+ return null;
2105
+ }
2106
+ if (typeof settingsModel !== "string") {
2107
+ const typeLabel = settingsModel === null ? "null" : typeof settingsModel;
2108
+ logger.error(`Ignoring model from settings: expected a string, got ${typeLabel}.`);
2109
+ return null;
2110
+ }
2111
+ return resolveModelPreference(models, settingsModel);
2112
+ }
2113
+ /**
2114
+ * Restrict the SDK's model list to the user's `availableModels` allowlist
2115
+ * (already merged-and-deduped across settings sources by `SettingsManager`).
2116
+ * The user's exact entries become the model IDs surfaced via configOptions
2117
+ * and passed to `setModel`, which prevents Claude Code from silently
2118
+ * substituting a date-pinned variant (e.g. `haiku` →
2119
+ * `claude-haiku-4-5-20251001`) that the user may not have access to.
2120
+ *
2121
+ * Display info and capability flags are copied from the closest SDK match so
2122
+ * the UI still renders sensible names and effort levels.
2123
+ *
2124
+ * Semantics from https://code.claude.com/docs/en/model-config#restrict-model-selection:
2125
+ * - `undefined` is handled by the caller (no allowlist applied).
2126
+ * - The Default option is unaffected by `availableModels` — it always remains
2127
+ * available, even when the allowlist is `[]`.
2128
+ */
2129
+ function applyAvailableModelsAllowlist(sdkModels, allowlist) {
2130
+ // Default is always preserved per the docs. Synthesize one if the SDK
2131
+ // didn't surface it so downstream code (e.g. `getAvailableModels` picking
2132
+ // `models[0]` as a fallback) still has something to work with.
2133
+ const defaultModel = sdkModels.find((m) => m.value === "default") ?? {
2134
+ value: "default",
2135
+ displayName: "Default",
2136
+ description: "",
2137
+ };
2138
+ const result = [defaultModel];
2139
+ const seen = new Set([defaultModel.value]);
2140
+ const sdkModelsWithoutDefault = sdkModels.filter((m) => m.value !== "default");
2141
+ for (const entry of allowlist) {
2142
+ const trimmed = entry.trim();
2143
+ if (!trimmed || seen.has(trimmed))
2144
+ continue;
2145
+ const sdkMatch = resolveModelPreference(sdkModelsWithoutDefault, trimmed);
2146
+ if (sdkMatch) {
2147
+ result.push({ ...sdkMatch, value: trimmed });
2148
+ }
2149
+ else {
2150
+ result.push({ value: trimmed, displayName: trimmed, description: "" });
2151
+ }
2152
+ seen.add(trimmed);
2153
+ }
2154
+ return result;
2155
+ }
2156
+ async function getAvailableModels(query, models, sdkModels, settingsManager, logger) {
2157
+ const settings = settingsManager.getSettings();
2158
+ let currentModel = models[0];
2159
+ let resolvedFromInput;
2160
+ // Model priority (highest to lowest):
2161
+ // 1. ANTHROPIC_MODEL environment variable
2162
+ // 2. settings.model (user configuration)
2163
+ // 3. models[0] (default first model)
2164
+ if (process.env.ANTHROPIC_MODEL) {
2165
+ const match = resolveModelPreference(models, process.env.ANTHROPIC_MODEL);
2166
+ if (match) {
2167
+ currentModel = match;
2168
+ resolvedFromInput = process.env.ANTHROPIC_MODEL;
2169
+ }
2170
+ }
2171
+ else if (typeof settings.model === "string") {
2172
+ const match = resolveSettingsModel(models, settings.model, logger);
2173
+ if (match) {
2174
+ currentModel = match;
2175
+ resolvedFromInput = settings.model;
2176
+ }
2177
+ }
2178
+ // Skip the setModel round-trip when we can prove the SDK has already landed
2179
+ // on the same model. Two cases qualify:
2180
+ // (a) No override applied — currentModel stayed at models[0]; the SDK is on
2181
+ // its own default and we have nothing to sync.
2182
+ // (b) The resolver returned the user's input verbatim AND that value exists
2183
+ // in the SDK's original model list — meaning no fuzzy match or
2184
+ // allowlist rewrite was involved, and the SDK (which reads the same
2185
+ // ANTHROPIC_MODEL / settings.json) will have arrived at the same entry.
2186
+ // Anything else (fuzzy match, allowlist-synthesized value, alias) gets a
2187
+ // setModel call so we don't drift from the user's intended pin.
2188
+ const sdkSawSameValue = sdkModels.some((m) => m.value === currentModel.value);
2189
+ const skipSetModel = resolvedFromInput === undefined ||
2190
+ (currentModel.value === resolvedFromInput && sdkSawSameValue);
2191
+ if (!skipSetModel) {
2192
+ await query.setModel(currentModel.value);
2193
+ }
2194
+ return {
2195
+ availableModels: models.map((model) => ({
2196
+ modelId: model.value,
2197
+ name: model.displayName,
2198
+ description: model.description,
2199
+ })),
2200
+ currentModelId: currentModel.value,
2201
+ };
2202
+ }
2203
+ function getAvailableSlashCommands(commands) {
2204
+ const UNSUPPORTED_COMMANDS = [
2205
+ "clear",
2206
+ "cost",
2207
+ "keybindings-help",
2208
+ "login",
2209
+ "logout",
2210
+ "output-style:new",
2211
+ "release-notes",
2212
+ "todos",
2213
+ ];
2214
+ return commands
2215
+ .map((command) => {
2216
+ const input = command.argumentHint
2217
+ ? {
2218
+ hint: Array.isArray(command.argumentHint)
2219
+ ? command.argumentHint.join(" ")
2220
+ : command.argumentHint,
2221
+ }
2222
+ : null;
2223
+ let name = command.name;
2224
+ if (command.name.endsWith(" (MCP)")) {
2225
+ name = `mcp:${name.replace(" (MCP)", "")}`;
2226
+ }
2227
+ return {
2228
+ name,
2229
+ description: command.description || "",
2230
+ input,
2231
+ };
2232
+ })
2233
+ .filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
2234
+ }
2235
+ function formatUriAsLink(uri) {
2236
+ try {
2237
+ if (uri.startsWith("file://")) {
2238
+ const path = uri.slice(7); // Remove "file://"
2239
+ const name = path.split("/").pop() || path;
2240
+ return `[@${name}](${uri})`;
2241
+ }
2242
+ else if (uri.startsWith("zed://")) {
2243
+ const parts = uri.split("/");
2244
+ const name = parts[parts.length - 1] || uri;
2245
+ return `[@${name}](${uri})`;
2246
+ }
2247
+ return uri;
2248
+ }
2249
+ catch {
2250
+ return uri;
2251
+ }
2252
+ }
2253
+ export function promptToClaude(prompt) {
2254
+ const content = [];
2255
+ const context = [];
2256
+ for (const chunk of prompt.prompt) {
2257
+ switch (chunk.type) {
2258
+ case "text": {
2259
+ let text = chunk.text;
2260
+ // change /mcp:server:command args -> /server:command (MCP) args
2261
+ const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s(.*))?$/);
2262
+ if (mcpMatch) {
2263
+ const [, server, command, args] = mcpMatch;
2264
+ text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
2265
+ }
2266
+ content.push({ type: "text", text });
2267
+ break;
2268
+ }
2269
+ case "resource_link": {
2270
+ const formattedUri = formatUriAsLink(chunk.uri);
2271
+ content.push({
2272
+ type: "text",
2273
+ text: formattedUri,
2274
+ });
2275
+ break;
2276
+ }
2277
+ case "resource": {
2278
+ if ("text" in chunk.resource) {
2279
+ const formattedUri = formatUriAsLink(chunk.resource.uri);
2280
+ content.push({
2281
+ type: "text",
2282
+ text: formattedUri,
2283
+ });
2284
+ context.push({
2285
+ type: "text",
2286
+ text: `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`,
2287
+ });
2288
+ }
2289
+ // Ignore blob resources (unsupported)
2290
+ break;
2291
+ }
2292
+ case "image":
2293
+ if (chunk.data) {
2294
+ content.push({
2295
+ type: "image",
2296
+ source: {
2297
+ type: "base64",
2298
+ data: chunk.data,
2299
+ media_type: chunk.mimeType,
2300
+ },
2301
+ });
2302
+ }
2303
+ else if (chunk.uri && chunk.uri.startsWith("http")) {
2304
+ content.push({
2305
+ type: "image",
2306
+ source: {
2307
+ type: "url",
2308
+ url: chunk.uri,
2309
+ },
2310
+ });
2311
+ }
2312
+ break;
2313
+ // Ignore audio and other unsupported types
2314
+ default:
2315
+ break;
2316
+ }
2317
+ }
2318
+ content.push(...context);
2319
+ return {
2320
+ type: "user",
2321
+ message: {
2322
+ role: "user",
2323
+ content: content,
2324
+ },
2325
+ session_id: prompt.sessionId,
2326
+ parent_tool_use_id: null,
2327
+ };
2328
+ }
2329
+ /**
2330
+ * Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
2331
+ * Only handles text, image, and thinking chunks for now.
2332
+ */
2333
+ export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
2334
+ const taskState = options?.taskState ?? new Map();
2335
+ const registerHooks = options?.registerHooks !== false;
2336
+ const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
2337
+ if (typeof content === "string") {
2338
+ const update = {
2339
+ sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
2340
+ content: {
2341
+ type: "text",
2342
+ text: content,
2343
+ },
2344
+ };
2345
+ if (options?.parentToolUseId) {
2346
+ update._meta = {
2347
+ ...update._meta,
2348
+ claudeCode: {
2349
+ ...(update._meta?.claudeCode || {}),
2350
+ parentToolUseId: options.parentToolUseId,
2351
+ },
2352
+ };
2353
+ }
2354
+ return [{ sessionId, update }];
2355
+ }
2356
+ const output = [];
2357
+ // Only handle the first chunk for streaming; extend as needed for batching
2358
+ for (const chunk of content) {
2359
+ let update = null;
2360
+ switch (chunk.type) {
2361
+ case "text":
2362
+ case "text_delta":
2363
+ update = {
2364
+ sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
2365
+ content: {
2366
+ type: "text",
2367
+ text: chunk.text,
2368
+ },
2369
+ };
2370
+ break;
2371
+ case "image":
2372
+ update = {
2373
+ sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
2374
+ content: {
2375
+ type: "image",
2376
+ data: chunk.source.type === "base64" ? chunk.source.data : "",
2377
+ mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
2378
+ uri: chunk.source.type === "url" ? chunk.source.url : undefined,
2379
+ },
2380
+ };
2381
+ break;
2382
+ case "thinking":
2383
+ case "thinking_delta":
2384
+ update = {
2385
+ sessionUpdate: "agent_thought_chunk",
2386
+ content: {
2387
+ type: "text",
2388
+ text: chunk.thinking,
2389
+ },
2390
+ };
2391
+ break;
2392
+ case "tool_use":
2393
+ case "server_tool_use":
2394
+ case "mcp_tool_use": {
2395
+ const alreadyCached = chunk.id in toolUseCache;
2396
+ toolUseCache[chunk.id] = chunk;
2397
+ if (chunk.name === "TodoWrite") {
2398
+ // @ts-expect-error - sometimes input is empty object or undefined
2399
+ if (Array.isArray(chunk.input?.todos)) {
2400
+ update = {
2401
+ sessionUpdate: "plan",
2402
+ entries: planEntries(chunk.input),
2403
+ };
2404
+ }
2405
+ }
2406
+ else if (chunk.name === "TaskCreate" ||
2407
+ chunk.name === "TaskUpdate" ||
2408
+ chunk.name === "TaskList" ||
2409
+ chunk.name === "TaskGet") {
2410
+ // Task* tool_use is suppressed; the plan update is emitted at
2411
+ // tool_result time once we have the task ID (for TaskCreate) and
2412
+ // confirmation that the change took effect.
2413
+ }
2414
+ else {
2415
+ // Only register hooks on first encounter to avoid double-firing
2416
+ if (registerHooks && !alreadyCached) {
2417
+ registerHookCallback(chunk.id, {
2418
+ onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
2419
+ const toolUse = toolUseCache[toolUseId];
2420
+ if (toolUse) {
2421
+ // Both `Edit` and `Write` produce a structuredPatch in their
2422
+ // PostToolUse tool_response. For Edit the diff replaces the
2423
+ // optimistic content built at tool_use time. For Write the
2424
+ // optimistic content (built from `input.content` alone with
2425
+ // `oldText: null`) shows "creation" semantics regardless of
2426
+ // whether the file existed; the structuredPatch from the
2427
+ // hook lets us emit the real diff for `type: "update"`. The
2428
+ // helper returns `{}` if the response shape isn't usable.
2429
+ const editDiff = toolUse.name === "Edit" || toolUse.name === "Write"
2430
+ ? toolUpdateFromDiffToolResponse(toolResponse)
2431
+ : {};
2432
+ const update = {
2433
+ _meta: {
2434
+ claudeCode: {
2435
+ toolResponse,
2436
+ toolName: toolUse.name,
2437
+ },
2438
+ },
2439
+ toolCallId: toolUseId,
2440
+ sessionUpdate: "tool_call_update",
2441
+ ...editDiff,
2442
+ };
2443
+ await client.sessionUpdate({
2444
+ sessionId,
2445
+ update,
2446
+ });
2447
+ }
2448
+ else {
2449
+ logger.error(`[claude-agent-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
2450
+ }
2451
+ },
2452
+ });
2453
+ }
2454
+ let rawInput;
2455
+ try {
2456
+ rawInput = JSON.parse(JSON.stringify(chunk.input));
2457
+ }
2458
+ catch {
2459
+ // ignore if we can't turn it to JSON
2460
+ }
2461
+ if (alreadyCached) {
2462
+ // Second encounter (full assistant message after streaming) —
2463
+ // send as tool_call_update to refine the existing tool_call
2464
+ // rather than emitting a duplicate tool_call.
2465
+ update = {
2466
+ _meta: {
2467
+ claudeCode: {
2468
+ toolName: chunk.name,
2469
+ },
2470
+ },
2471
+ toolCallId: chunk.id,
2472
+ sessionUpdate: "tool_call_update",
2473
+ rawInput,
2474
+ ...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
2475
+ };
2476
+ }
2477
+ else {
2478
+ // First encounter (streaming content_block_start or replay) —
2479
+ // send as tool_call with terminal_info for Bash tools.
2480
+ update = {
2481
+ _meta: {
2482
+ claudeCode: {
2483
+ toolName: chunk.name,
2484
+ },
2485
+ ...(chunk.name === "Bash" && supportsTerminalOutput
2486
+ ? { terminal_info: { terminal_id: chunk.id } }
2487
+ : {}),
2488
+ },
2489
+ toolCallId: chunk.id,
2490
+ sessionUpdate: "tool_call",
2491
+ rawInput,
2492
+ status: "pending",
2493
+ ...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
2494
+ };
2495
+ }
2496
+ }
2497
+ break;
2498
+ }
2499
+ case "tool_result":
2500
+ case "tool_search_tool_result":
2501
+ case "web_fetch_tool_result":
2502
+ case "web_search_tool_result":
2503
+ case "code_execution_tool_result":
2504
+ case "bash_code_execution_tool_result":
2505
+ case "text_editor_code_execution_tool_result":
2506
+ case "mcp_tool_result": {
2507
+ const toolUse = toolUseCache[chunk.tool_use_id];
2508
+ if (!toolUse) {
2509
+ logger.error(`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
2510
+ break;
2511
+ }
2512
+ if (toolUse.name === "TaskCreate" ||
2513
+ toolUse.name === "TaskUpdate" ||
2514
+ toolUse.name === "TaskList" ||
2515
+ toolUse.name === "TaskGet") {
2516
+ // Headless/SDK sessions emit Task* tools instead of TodoWrite.
2517
+ // TaskCreate / TaskUpdate mutate the accumulated task list; TaskList
2518
+ // and TaskGet are read-only so we just suppress their tool_call /
2519
+ // tool_result events. The plan update is emitted as a snapshot of
2520
+ // the accumulated state, mirroring the legacy TodoWrite behavior.
2521
+ const isError = "is_error" in chunk && chunk.is_error;
2522
+ if (!isError) {
2523
+ if (toolUse.name === "TaskCreate") {
2524
+ applyTaskCreate(taskState, toolUse.input, parseTaskCreateOutput(chunk.content));
2525
+ }
2526
+ else if (toolUse.name === "TaskUpdate") {
2527
+ applyTaskUpdate(taskState, toolUse.input);
2528
+ }
2529
+ }
2530
+ if (!isError && (toolUse.name === "TaskCreate" || toolUse.name === "TaskUpdate")) {
2531
+ update = {
2532
+ sessionUpdate: "plan",
2533
+ entries: taskStateToPlanEntries(taskState),
2534
+ };
2535
+ }
2536
+ }
2537
+ else if (toolUse.name !== "TodoWrite") {
2538
+ const { _meta: toolMeta, ...toolUpdate } = toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id], supportsTerminalOutput);
2539
+ // When terminal output is supported, send terminal_output as a
2540
+ // separate notification to match codex-acp's streaming lifecycle:
2541
+ // 1. tool_call → _meta.terminal_info (already sent above)
2542
+ // 2. tool_call_update → _meta.terminal_output (sent here)
2543
+ // 3. tool_call_update → _meta.terminal_exit (sent below with status)
2544
+ if (toolMeta?.terminal_output) {
2545
+ output.push({
2546
+ sessionId,
2547
+ update: {
2548
+ _meta: {
2549
+ terminal_output: toolMeta.terminal_output,
2550
+ ...(options?.parentToolUseId
2551
+ ? { claudeCode: { parentToolUseId: options.parentToolUseId } }
2552
+ : {}),
2553
+ },
2554
+ toolCallId: chunk.tool_use_id,
2555
+ sessionUpdate: "tool_call_update",
2556
+ },
2557
+ });
2558
+ }
2559
+ update = {
2560
+ _meta: {
2561
+ claudeCode: {
2562
+ toolName: toolUse.name,
2563
+ },
2564
+ ...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
2565
+ },
2566
+ toolCallId: chunk.tool_use_id,
2567
+ sessionUpdate: "tool_call_update",
2568
+ status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
2569
+ rawOutput: chunk.content,
2570
+ ...toolUpdate,
2571
+ };
2572
+ }
2573
+ break;
2574
+ }
2575
+ case "document":
2576
+ case "search_result":
2577
+ case "redacted_thinking":
2578
+ case "input_json_delta":
2579
+ case "citations_delta":
2580
+ case "signature_delta":
2581
+ case "container_upload":
2582
+ case "compaction":
2583
+ case "compaction_delta":
2584
+ case "advisor_tool_result":
2585
+ case "mid_conv_system":
2586
+ break;
2587
+ default:
2588
+ unreachable(chunk, logger);
2589
+ break;
2590
+ }
2591
+ if (update) {
2592
+ if (options?.parentToolUseId) {
2593
+ update._meta = {
2594
+ ...update._meta,
2595
+ claudeCode: {
2596
+ ...(update._meta?.claudeCode || {}),
2597
+ parentToolUseId: options.parentToolUseId,
2598
+ },
2599
+ };
2600
+ }
2601
+ output.push({ sessionId, update });
2602
+ }
2603
+ }
2604
+ return output;
2605
+ }
2606
+ export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger, options) {
2607
+ const event = message.event;
2608
+ switch (event.type) {
2609
+ case "content_block_start":
2610
+ return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger, {
2611
+ clientCapabilities: options?.clientCapabilities,
2612
+ parentToolUseId: message.parent_tool_use_id,
2613
+ cwd: options?.cwd,
2614
+ taskState: options?.taskState,
2615
+ });
2616
+ case "content_block_delta":
2617
+ return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, {
2618
+ clientCapabilities: options?.clientCapabilities,
2619
+ parentToolUseId: message.parent_tool_use_id,
2620
+ cwd: options?.cwd,
2621
+ taskState: options?.taskState,
2622
+ });
2623
+ // No content. `ping` is a Messages-API keep-alive event that the SDK's
2624
+ // `BetaRawMessageStreamEvent` union doesn't include even though the
2625
+ // wire format emits it; the `as never` cast lets us no-op it here
2626
+ // instead of letting it fall through to `unreachable`.
2627
+ case "ping":
2628
+ case "message_start":
2629
+ case "message_delta":
2630
+ case "message_stop":
2631
+ case "content_block_stop":
2632
+ return [];
2633
+ default:
2634
+ unreachable(event, logger);
2635
+ return [];
2636
+ }
2637
+ }
2638
+ export function runAcp() {
2639
+ const input = nodeToWebWritable(process.stdout);
2640
+ const output = nodeToWebReadable(process.stdin);
2641
+ const stream = ndJsonStream(input, output);
2642
+ let agent;
2643
+ const connection = new AgentSideConnection((client) => {
2644
+ agent = new ClaudeAcpAgent(client);
2645
+ return agent;
2646
+ }, stream);
2647
+ return { connection, agent };
2648
+ }
2649
+ function commonPrefixLength(a, b) {
2650
+ let i = 0;
2651
+ while (i < a.length && i < b.length && a[i] === b[i]) {
2652
+ i++;
2653
+ }
2654
+ return i;
2655
+ }
2656
+ /** Best-effort first guess of a model's context window from its ID, used only
2657
+ * until a `result` message arrives with the authoritative `modelUsage` value.
2658
+ * Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
2659
+ * model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
2660
+ * matching things like "10m" or embedded substrings. */
2661
+ function inferContextWindowFromModel(model) {
2662
+ if (/\b1m\b/i.test(model))
2663
+ return 1_000_000;
2664
+ return null;
2665
+ }
2666
+ function parseModelConfig(raw) {
2667
+ if (!raw)
2668
+ return undefined;
2669
+ const parsed = JSON.parse(raw);
2670
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2671
+ throw new Error("CLAUDE_MODEL_CONFIG must be a JSON object");
2672
+ }
2673
+ const result = {};
2674
+ if (parsed.modelOverrides !== undefined)
2675
+ result.modelOverrides = parsed.modelOverrides;
2676
+ if (parsed.availableModels !== undefined)
2677
+ result.availableModels = parsed.availableModels;
2678
+ return Object.keys(result).length > 0 ? result : undefined;
2679
+ }
2680
+ function getMatchingModelUsage(modelUsage, currentModel) {
2681
+ let bestKey = null;
2682
+ let bestLen = 0;
2683
+ for (const key of Object.keys(modelUsage)) {
2684
+ const len = commonPrefixLength(key, currentModel);
2685
+ if (len > bestLen) {
2686
+ bestLen = len;
2687
+ bestKey = key;
2688
+ }
2689
+ }
2690
+ if (bestKey) {
2691
+ return modelUsage[bestKey];
2692
+ }
2693
+ }