@loicngr/kobo 1.6.14 → 1.7.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 (200) hide show
  1. package/README.md +4 -3
  2. package/dist/mcp-server/kobo-tasks-server.js +51 -0
  3. package/dist/server/db/migrations.js +40 -0
  4. package/dist/server/db/schema.js +7 -5
  5. package/dist/server/index.js +12 -11
  6. package/dist/server/routes/workspaces.js +207 -26
  7. package/dist/server/services/agent/engines/claude-code/capabilities.js +1 -1
  8. package/dist/server/services/agent/engines/claude-code/engine.js +237 -132
  9. package/dist/server/services/agent/engines/claude-code/event-mapper.js +234 -0
  10. package/dist/server/services/agent/engines/claude-code/options-builder.js +68 -0
  11. package/dist/server/services/agent/engines/claude-code/precompact-hook.js +27 -0
  12. package/dist/server/services/agent/engines/types.js +1 -0
  13. package/dist/server/services/agent/orchestrator.js +536 -94
  14. package/dist/server/services/agent/session-controller.js +14 -43
  15. package/dist/server/services/auto-loop-service.js +17 -6
  16. package/dist/server/services/content-migration-service.js +24 -94
  17. package/dist/server/services/usage/poller.js +4 -1
  18. package/dist/server/services/wakeup-service.js +8 -9
  19. package/dist/server/services/workspace-service.js +40 -36
  20. package/dist/server/utils/git-ops.js +67 -5
  21. package/package.json +2 -1
  22. package/src/client/dist/spa/assets/ActivityFeed-CIJPN8TH.js +7 -0
  23. package/src/client/dist/spa/assets/ActivityFeed-LXnbg3ff.css +1 -0
  24. package/src/client/dist/spa/assets/ClosePopup-DzB3mDtj.js +1 -0
  25. package/src/client/dist/spa/assets/CreatePage-BE3xfQsC.css +1 -0
  26. package/src/client/dist/spa/assets/CreatePage-U6TtJzNe.js +2 -0
  27. package/src/client/dist/spa/assets/DiffViewer-D1Sdu307.css +1 -0
  28. package/src/client/dist/spa/assets/DiffViewer-Di85TBIi.js +7 -0
  29. package/src/client/dist/spa/assets/HealthPage-B7aWFxAZ.js +1 -0
  30. package/src/client/dist/spa/assets/{MainLayout-C3TUaYvQ.js → MainLayout-BHBrz4c9.js} +17 -17
  31. package/src/client/dist/spa/assets/MainLayout-Dba6SdpU.css +1 -0
  32. package/src/client/dist/spa/assets/QCheckbox-CcY7ZSk9.js +1 -0
  33. package/src/client/dist/spa/assets/QChip-BhT0W2Dg.js +1 -0
  34. package/src/client/dist/spa/assets/QExpansionItem-VS4b4eY6.js +1 -0
  35. package/src/client/dist/spa/assets/QInput-D4WJro4e.js +1 -0
  36. package/src/client/dist/spa/assets/QMenu-CchbRXbp.js +1 -0
  37. package/src/client/dist/spa/assets/{QPage-yqdKDG7-.js → QPage-Cu7zkfc6.js} +1 -1
  38. package/src/client/dist/spa/assets/QRadio-DaZhdLCg.js +1 -0
  39. package/src/client/dist/spa/assets/QResizeObserver-Cf79V-VZ.js +1 -0
  40. package/src/client/dist/spa/assets/QScrollArea-DrVTDLU0.js +1 -0
  41. package/src/client/dist/spa/assets/QTabPanels-HXz-evuj.js +1 -0
  42. package/src/client/dist/spa/assets/QToggle-CGpiJLDJ.js +1 -0
  43. package/src/client/dist/spa/assets/QTooltip-DjJYMTkN.js +1 -0
  44. package/src/client/dist/spa/assets/SearchPage-CVm-sqxH.js +1 -0
  45. package/src/client/dist/spa/assets/SettingsPage-ayDKGo9H.js +1 -0
  46. package/src/client/dist/spa/assets/SettingsPage-wTBCvK6t.css +1 -0
  47. package/src/client/dist/spa/assets/WorkspacePage-DQxGe62K.css +1 -0
  48. package/src/client/dist/spa/assets/WorkspacePage-xaVy8s5i.js +4 -0
  49. package/src/client/dist/spa/assets/build-path-tree-CdY1A6aP.js +1 -0
  50. package/src/client/dist/spa/assets/{cssMode-wNaxOrgG.js → cssMode-BVNBMOxh.js} +1 -1
  51. package/src/client/dist/spa/assets/documents-D6A3wRry.js +1 -0
  52. package/src/client/dist/spa/assets/{editor.api-CcDntllS.js → editor.api-D6Vfp5yv.js} +1 -1
  53. package/src/client/dist/spa/assets/{editor.main-Chu4hc0J.js → editor.main-CTCYF6V4.js} +3 -3
  54. package/src/client/dist/spa/assets/{expand-template-CcQus77v.js → expand-template-vHV2iwXf.js} +1 -1
  55. package/src/client/dist/spa/assets/{formatters-CX2gvLFv.js → formatters-ejxELb0M.js} +1 -1
  56. package/src/client/dist/spa/assets/{freemarker2-CO_b202E.js → freemarker2-nmzwPmzi.js} +1 -1
  57. package/src/client/dist/spa/assets/{handlebars-CJnTWNLs.js → handlebars-CI9lR7Ef.js} +1 -1
  58. package/src/client/dist/spa/assets/{html-DeArYseI.js → html-BQ21REnv.js} +1 -1
  59. package/src/client/dist/spa/assets/{htmlMode-BnNgEgdx.js → htmlMode-io5J5Qr1.js} +1 -1
  60. package/src/client/dist/spa/assets/i18n-Do8Kn8n0.js +1 -0
  61. package/src/client/dist/spa/assets/index-C_e7KOYh.js +2 -0
  62. package/src/client/dist/spa/assets/{javascript-C0pxfNu4.js → javascript--u9PDBCv.js} +1 -1
  63. package/src/client/dist/spa/assets/{jsonMode-ety87201.js → jsonMode-DBG5llk4.js} +1 -1
  64. package/src/client/dist/spa/assets/{kobo-commands-Cpl4IFon.js → kobo-commands-DiUm1Y34.js} +1 -1
  65. package/src/client/dist/spa/assets/{liquid-kanevKvC.js → liquid-DxAS4nYF.js} +1 -1
  66. package/src/client/dist/spa/assets/marked.esm-DuOsJx63.js +60 -0
  67. package/src/client/dist/spa/assets/{mdx-DkmtbRD7.js → mdx-BNXTiODW.js} +1 -1
  68. package/src/client/dist/spa/assets/models-DNYEhFF7.js +1 -0
  69. package/src/client/dist/spa/assets/{monaco.contribution-DsZsua59.js → monaco.contribution-CT3LAK0J.js} +2 -2
  70. package/src/client/dist/spa/assets/{python-DrxH1xl7.js → python-DztNww13.js} +1 -1
  71. package/src/client/dist/spa/assets/{razor-CU4khv8N.js → razor-Cyr82NZF.js} +1 -1
  72. package/src/client/dist/spa/assets/settings-Dbx1_ksA.js +1 -0
  73. package/src/client/dist/spa/assets/symbols-BVRrMH2r.js +1 -0
  74. package/src/client/dist/spa/assets/touch-Co9pfjUU.js +1 -0
  75. package/src/client/dist/spa/assets/{tsMode-CQ5yxoz_.js → tsMode-CbQVgsIP.js} +1 -1
  76. package/src/client/dist/spa/assets/{typescript-CSwKmP7l.js → typescript-UHOe4d1S.js} +1 -1
  77. package/src/client/dist/spa/assets/use-checkbox-DzHmcu7s.js +1 -0
  78. package/src/client/dist/spa/assets/use-id-CDuXkR0Z.js +1 -0
  79. package/src/client/dist/spa/assets/use-panel-Br8QNRMk.js +1 -0
  80. package/src/client/dist/spa/assets/{xml-9bnWANPJ.js → xml-DC88eFpV.js} +1 -1
  81. package/src/client/dist/spa/assets/{yaml-sUtDJGxo.js → yaml-DSTsIRJr.js} +1 -1
  82. package/src/client/dist/spa/index.html +11 -10
  83. package/src/mcp-server/kobo-tasks-server.ts +60 -1
  84. package/dist/server/services/agent/engines/claude-code/args-builder.js +0 -57
  85. package/dist/server/services/agent/engines/claude-code/mcp-config.js +0 -23
  86. package/dist/server/services/agent/engines/claude-code/stream-parser.js +0 -386
  87. package/src/client/dist/spa/assets/ActivityFeed-Be0QQryJ.css +0 -1
  88. package/src/client/dist/spa/assets/ActivityFeed-BtIOkIy6.js +0 -8
  89. package/src/client/dist/spa/assets/ClosePopup-DkLittac.js +0 -1
  90. package/src/client/dist/spa/assets/CreatePage-D6Q3nxkX.js +0 -2
  91. package/src/client/dist/spa/assets/CreatePage-DJbZH8wp.css +0 -1
  92. package/src/client/dist/spa/assets/DiffViewer-1s165rFm.css +0 -1
  93. package/src/client/dist/spa/assets/DiffViewer-D5u9p7il.js +0 -2
  94. package/src/client/dist/spa/assets/HealthPage-Cr7aAUy6.js +0 -1
  95. package/src/client/dist/spa/assets/MainLayout-CBnSwSfy.css +0 -1
  96. package/src/client/dist/spa/assets/QChip-bl3YRhax.js +0 -1
  97. package/src/client/dist/spa/assets/QExpansionItem-CWw6ZujM.js +0 -1
  98. package/src/client/dist/spa/assets/QScrollArea-DpCqRRE0.js +0 -1
  99. package/src/client/dist/spa/assets/QSeparator-DNSiXYrN.js +0 -1
  100. package/src/client/dist/spa/assets/QSlideTransition-BQxI8l5r.js +0 -1
  101. package/src/client/dist/spa/assets/QTabPanels-C4bZGqml.js +0 -1
  102. package/src/client/dist/spa/assets/QTooltip-BIDjo2hJ.js +0 -1
  103. package/src/client/dist/spa/assets/SearchPage-CavRaij6.js +0 -1
  104. package/src/client/dist/spa/assets/SettingsPage-B8DhSZw7.css +0 -1
  105. package/src/client/dist/spa/assets/SettingsPage-C13T1l_t.js +0 -1
  106. package/src/client/dist/spa/assets/TouchPan-vsl78kxF.js +0 -1
  107. package/src/client/dist/spa/assets/WorkspacePage-BEqEuPrb.js +0 -4
  108. package/src/client/dist/spa/assets/WorkspacePage-k2pgeRoy.css +0 -1
  109. package/src/client/dist/spa/assets/build-path-tree-BeAS10oa.js +0 -1
  110. package/src/client/dist/spa/assets/documents-Cw05r3zs.js +0 -60
  111. package/src/client/dist/spa/assets/i18n-CuT4b7ns.js +0 -1
  112. package/src/client/dist/spa/assets/index-CZA4BFN5.js +0 -2
  113. package/src/client/dist/spa/assets/models-CPFeBEQS.js +0 -1
  114. package/src/client/dist/spa/assets/private.use-form-Dlb0iQZh.js +0 -1
  115. package/src/client/dist/spa/assets/scroll-CYWyxBdv.js +0 -1
  116. package/src/client/dist/spa/assets/settings-CAILUJXO.js +0 -1
  117. package/src/client/dist/spa/assets/stats-C3n1k51k.js +0 -1
  118. package/src/client/dist/spa/assets/symbols-DCYodwb2.js +0 -1
  119. package/src/client/dist/spa/assets/touch-Bj_Fr4kC.js +0 -1
  120. package/src/client/dist/spa/assets/use-checkbox-B_o-iLG2.js +0 -1
  121. package/src/client/dist/spa/assets/use-id-C93QQwrt.js +0 -1
  122. /package/src/client/dist/spa/assets/{QBadge-DqtcDv8D.js → QBadge-fsQ2AokU.js} +0 -0
  123. /package/src/client/dist/spa/assets/{QItemLabel-Codqjisk.js → QItemLabel-DWwenW2S.js} +0 -0
  124. /package/src/client/dist/spa/assets/{QItemSection-CiY_LK5Y.js → QItemSection-KFAnxzMK.js} +0 -0
  125. /package/src/client/dist/spa/assets/{QList-Bl9824vi.js → QList-NmIE6Rd9.js} +0 -0
  126. /package/src/client/dist/spa/assets/{QSpace-BNr0AftG.js → QSpace-COlmM_4F.js} +0 -0
  127. /package/src/client/dist/spa/assets/{QSpinnerDots-DEiRooBD.js → QSpinnerDots-DwtnRN2r.js} +0 -0
  128. /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-r4mAJOHR.js → _plugin-vue_export-helper-B8bB5DBd.js} +0 -0
  129. /package/src/client/dist/spa/assets/{abap-Bgec7Keq.js → abap-DzK-OTGh.js} +0 -0
  130. /package/src/client/dist/spa/assets/{apex-VBlPwEoQ.js → apex-Bj60_dRt.js} +0 -0
  131. /package/src/client/dist/spa/assets/{azcli-DKqrEFBx.js → azcli-B6NwaBAZ.js} +0 -0
  132. /package/src/client/dist/spa/assets/{bat-DdgQWy_0.js → bat-bf7wXV68.js} +0 -0
  133. /package/src/client/dist/spa/assets/{bicep-CRMM43EB.js → bicep-C_bg8UgA.js} +0 -0
  134. /package/src/client/dist/spa/assets/{cameligo-UatALtML.js → cameligo-CTWw4D4B.js} +0 -0
  135. /package/src/client/dist/spa/assets/{clojure-D8JU08RA.js → clojure-CgdPoH0r.js} +0 -0
  136. /package/src/client/dist/spa/assets/{coffee-C56wu358.js → coffee-gHQfdA5M.js} +0 -0
  137. /package/src/client/dist/spa/assets/{cpp-CyZLvhJG.js → cpp-BM4Jj4aW.js} +0 -0
  138. /package/src/client/dist/spa/assets/{csharp-BJl3ixva.js → csharp-D8-bh4Cd.js} +0 -0
  139. /package/src/client/dist/spa/assets/{csp-CxEKxmO-.js → csp-CXBxRx0n.js} +0 -0
  140. /package/src/client/dist/spa/assets/{css-B0t_muXd.js → css-DKjIxrmY.js} +0 -0
  141. /package/src/client/dist/spa/assets/{cypher-D1hqiMFD.js → cypher-C5e5inIh.js} +0 -0
  142. /package/src/client/dist/spa/assets/{dart-Bz550Pyv.js → dart-BhRHHm4x.js} +0 -0
  143. /package/src/client/dist/spa/assets/{dockerfile-CIXgVAuA.js → dockerfile-DW5REF8E.js} +0 -0
  144. /package/src/client/dist/spa/assets/{ecl-D9qbvZoA.js → ecl-Bw4Hg3n_.js} +0 -0
  145. /package/src/client/dist/spa/assets/{elixir-b2M38fAy.js → elixir-DHmoBvpZ.js} +0 -0
  146. /package/src/client/dist/spa/assets/{flow9-Dq1UYMkt.js → flow9-BsFExz3v.js} +0 -0
  147. /package/src/client/dist/spa/assets/{fsharp-CFNadkg7.js → fsharp-BaeLhgfq.js} +0 -0
  148. /package/src/client/dist/spa/assets/{go-dSur1iB2.js → go-Bd-NFKIC.js} +0 -0
  149. /package/src/client/dist/spa/assets/{graphql-qyhAo11d.js → graphql-DZVerJfy.js} +0 -0
  150. /package/src/client/dist/spa/assets/{hcl-DFzjMyzm.js → hcl-CAVzrZfH.js} +0 -0
  151. /package/src/client/dist/spa/assets/{ini-TdzA8TIl.js → ini-CyXdX58t.js} +0 -0
  152. /package/src/client/dist/spa/assets/{is-DUKatk8N.js → is-BbsvEMaT.js} +0 -0
  153. /package/src/client/dist/spa/assets/{java-CSGA9pkE.js → java-B5pNgvhy.js} +0 -0
  154. /package/src/client/dist/spa/assets/{julia-9izz5OsY.js → julia-XRhmV3AN.js} +0 -0
  155. /package/src/client/dist/spa/assets/{kotlin-DuPK7AtF.js → kotlin-DOd3J5vr.js} +0 -0
  156. /package/src/client/dist/spa/assets/{less-B8d93iCg.js → less-veZSnyw6.js} +0 -0
  157. /package/src/client/dist/spa/assets/{lexon-DWtEIyu7.js → lexon-QWGkuK0H.js} +0 -0
  158. /package/src/client/dist/spa/assets/{lua-Ciq0OGgt.js → lua-CYGpjuO5.js} +0 -0
  159. /package/src/client/dist/spa/assets/{m3-Cki6JWj_.js → m3-yNnrZkdc.js} +0 -0
  160. /package/src/client/dist/spa/assets/{markdown-Cu47xwU0.js → markdown-BCSWEPSX.js} +0 -0
  161. /package/src/client/dist/spa/assets/{mips-BM8ui995.js → mips-OpYmcC30.js} +0 -0
  162. /package/src/client/dist/spa/assets/{msdax-DqLio0_c.js → msdax-2oxoTO9Z.js} +0 -0
  163. /package/src/client/dist/spa/assets/{mysql-v1wbjJOq.js → mysql-5KlC-K_9.js} +0 -0
  164. /package/src/client/dist/spa/assets/{objective-c-CQl3PGSB.js → objective-c-CcDCgtLx.js} +0 -0
  165. /package/src/client/dist/spa/assets/{pascal-D4iW0ZtD.js → pascal-BZGsbaEV.js} +0 -0
  166. /package/src/client/dist/spa/assets/{pascaligo-BdC9CZdj.js → pascaligo-DtD5qU3G.js} +0 -0
  167. /package/src/client/dist/spa/assets/{perl-BL10m4XD.js → perl-C1jNNS3E.js} +0 -0
  168. /package/src/client/dist/spa/assets/{pgsql-Be_oqVo3.js → pgsql-CT0fhiZa.js} +0 -0
  169. /package/src/client/dist/spa/assets/{php-BtvXSFRI.js → php-D6DrXoPM.js} +0 -0
  170. /package/src/client/dist/spa/assets/{pla-B2vUy15C.js → pla-b3-HN2pF.js} +0 -0
  171. /package/src/client/dist/spa/assets/{postiats-CbmTTfXr.js → postiats-Bin2ApVS.js} +0 -0
  172. /package/src/client/dist/spa/assets/{powerquery-DszLhJGx.js → powerquery-7ASnn-ZG.js} +0 -0
  173. /package/src/client/dist/spa/assets/{powershell-B0dYktF6.js → powershell-t4p7sU1H.js} +0 -0
  174. /package/src/client/dist/spa/assets/{protobuf-CZvaj1VX.js → protobuf-BUGeWa_j.js} +0 -0
  175. /package/src/client/dist/spa/assets/{pug-CPDx1B3S.js → pug-BuKcgC9s.js} +0 -0
  176. /package/src/client/dist/spa/assets/{qsharp-CDP9TFLl.js → qsharp-DSMtI_O7.js} +0 -0
  177. /package/src/client/dist/spa/assets/{r-8DbbFX2l.js → r-DMlFgn7A.js} +0 -0
  178. /package/src/client/dist/spa/assets/{redis-DRWj9MtJ.js → redis-cXItkC5u.js} +0 -0
  179. /package/src/client/dist/spa/assets/{redshift-C6cElE_5.js → redshift-BZVbW7HE.js} +0 -0
  180. /package/src/client/dist/spa/assets/{restructuredtext-W9pS9n3m.js → restructuredtext-BzjxwS8h.js} +0 -0
  181. /package/src/client/dist/spa/assets/{ruby-BKnzWnk-.js → ruby-C5nyLV4l.js} +0 -0
  182. /package/src/client/dist/spa/assets/{rust-YPCclWwe.js → rust-BcmMsHdf.js} +0 -0
  183. /package/src/client/dist/spa/assets/{sb-BgM4DTFb.js → sb-Dnb1iy6B.js} +0 -0
  184. /package/src/client/dist/spa/assets/{scala-fz1OPLMl.js → scala-anMIFYpA.js} +0 -0
  185. /package/src/client/dist/spa/assets/{scheme-8Uz1RIbu.js → scheme-BItQTe08.js} +0 -0
  186. /package/src/client/dist/spa/assets/{scss-Djo3IYXr.js → scss-BOv51BJ5.js} +0 -0
  187. /package/src/client/dist/spa/assets/{shell-CINF5Tx_.js → shell-BsRYRTNN.js} +0 -0
  188. /package/src/client/dist/spa/assets/{solidity-GgiNEuUm.js → solidity-BtuLgGDx.js} +0 -0
  189. /package/src/client/dist/spa/assets/{sophia-Culj97P9.js → sophia-B0Vkc5MF.js} +0 -0
  190. /package/src/client/dist/spa/assets/{sparql-C2ZlpxOY.js → sparql-B7lvkZQM.js} +0 -0
  191. /package/src/client/dist/spa/assets/{sql-BEf5Pg7Y.js → sql-DvP5MpA3.js} +0 -0
  192. /package/src/client/dist/spa/assets/{st-CT6UUoeH.js → st-GVUeyB3U.js} +0 -0
  193. /package/src/client/dist/spa/assets/{swift-B5g0xTG3.js → swift-DSPIoCjm.js} +0 -0
  194. /package/src/client/dist/spa/assets/{systemverilog-CEgQz9DR.js → systemverilog-Icj2-k23.js} +0 -0
  195. /package/src/client/dist/spa/assets/{tcl-D0qL2L0I.js → tcl-Cd8KQcm-.js} +0 -0
  196. /package/src/client/dist/spa/assets/{twig-BFUAVf1E.js → twig-CBHmt8z3.js} +0 -0
  197. /package/src/client/dist/spa/assets/{typespec-CjVVcNKm.js → typespec-Ckc037mq.js} +0 -0
  198. /package/src/client/dist/spa/assets/{vb-CZJr-DQz.js → vb-B97GW9Wb.js} +0 -0
  199. /package/src/client/dist/spa/assets/{vue-i18n-BJlZEYnA.js → vue-i18n-eUDnMrPl.js} +0 -0
  200. /package/src/client/dist/spa/assets/{wgsl-ivoXUo2e.js → wgsl-DIKmb3YH.js} +0 -0
@@ -1,166 +1,271 @@
1
- import { spawn } from 'node:child_process';
2
- import readline from 'node:readline';
3
- import { buildClaudeArgs } from './args-builder.js';
1
+ import { query, } from '@anthropic-ai/claude-agent-sdk';
2
+ import { nanoid } from 'nanoid';
4
3
  import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
5
- import { cleanupMcpConfig, writeMcpConfig } from './mcp-config.js';
6
- import { createParserState, parseClaudeLine } from './stream-parser.js';
4
+ import { createMapperState, mapSdkMessage } from './event-mapper.js';
5
+ import { buildClaudeOptions } from './options-builder.js';
6
+ import { buildPreCompactCustomInstructions } from './precompact-hook.js';
7
+ function toMcpServersMap(specs) {
8
+ if (!specs || specs.length === 0)
9
+ return undefined;
10
+ const map = {};
11
+ for (const s of specs) {
12
+ // `alwaysLoad: true` is required: without it, MCP tools sit behind the
13
+ // SDK's ToolSearch indirection that — even under bypassPermissions —
14
+ // surfaces a "haven't granted it yet" gate. With it, MCP tools behave
15
+ // like built-ins, matching pre-SDK CLI behaviour.
16
+ map[s.name] = { type: 'stdio', command: s.command, args: s.args, env: s.env, alwaysLoad: true };
17
+ }
18
+ return map;
19
+ }
7
20
  export function createClaudeCodeEngine() {
8
21
  return {
9
22
  id: 'claude-code',
10
23
  displayName: 'Claude Code',
11
24
  capabilities: CLAUDE_CODE_CAPABILITIES,
12
25
  async start(options, onEvent) {
13
- // Write MCP config if any servers requested + engine supports MCP
14
- let mcpConfigPath;
15
- if (options.mcpServers && options.mcpServers.length > 0) {
16
- mcpConfigPath = writeMcpConfig(options.workingDir, options.mcpServers);
17
- }
18
- const { args } = buildClaudeArgs({
26
+ const abortController = new AbortController();
27
+ const mapperState = createMapperState();
28
+ // Pending canUseTool callbacks, keyed by SDK ctx.toolUseID.
29
+ const pendingResolvers = new Map();
30
+ const isInteractive = options.agentPermissionMode === 'interactive';
31
+ const canUseTool = (toolName, input, ctx) => {
32
+ const toolCallId = typeof ctx.toolUseID === 'string' && ctx.toolUseID.length > 0 ? ctx.toolUseID : `tu_${nanoid()}`;
33
+ // Non-interactive modes: the SDK has already applied its permissionMode
34
+ // rules before reaching us, so allow through unchanged. AskUserQuestion
35
+ // is the exception — it always defers to the user.
36
+ if (toolName !== 'AskUserQuestion' && !isInteractive) {
37
+ return Promise.resolve({ behavior: 'allow', updatedInput: input });
38
+ }
39
+ const requestKind = toolName === 'AskUserQuestion' ? 'question' : 'permission';
40
+ return new Promise((resolve, reject) => {
41
+ const resolver = { resolve, input, requestKind };
42
+ pendingResolvers.set(toolCallId, resolver);
43
+ const onAbort = () => {
44
+ if (pendingResolvers.get(toolCallId) === resolver) {
45
+ pendingResolvers.delete(toolCallId);
46
+ const abortError = new Error('Pending user input aborted');
47
+ abortError.name = 'AbortError';
48
+ reject(abortError);
49
+ }
50
+ };
51
+ if (ctx.signal.aborted) {
52
+ onAbort();
53
+ return;
54
+ }
55
+ ctx.signal.addEventListener('abort', onAbort, { once: true });
56
+ onEvent({
57
+ kind: 'session:user-input-requested',
58
+ requestKind,
59
+ toolCallId,
60
+ toolName,
61
+ payload: input,
62
+ });
63
+ });
64
+ };
65
+ // PreCompact's hookSpecificOutput is missing from the SDK type union
66
+ // (SyncHookJSONOutput omits it), but the runtime accepts
67
+ // `additionalContext` as custom compaction instructions. Cast through
68
+ // `unknown` to satisfy the strict union.
69
+ const hooks = {
70
+ PreCompact: [
71
+ {
72
+ hooks: [
73
+ async () => {
74
+ const reminder = buildPreCompactCustomInstructions(options.workspaceId);
75
+ if (!reminder)
76
+ return {};
77
+ return {
78
+ hookSpecificOutput: {
79
+ hookEventName: 'PreCompact',
80
+ additionalContext: reminder,
81
+ },
82
+ };
83
+ },
84
+ ],
85
+ },
86
+ ],
87
+ };
88
+ const { options: sdkOptions, effectivePrompt } = buildClaudeOptions({
19
89
  prompt: options.prompt,
20
90
  model: options.model,
21
91
  effort: options.effort,
22
- permissionMode: options.permissionMode ?? 'auto-accept',
23
- skipPermissions: options.settings.dangerouslySkipPermissions ?? true,
24
- permissionProfile: options.permissionProfile,
92
+ agentPermissionMode: options.agentPermissionMode ?? 'bypass',
25
93
  resumeFromEngineSessionId: options.resumeFromEngineSessionId,
26
- mcpConfigPath,
27
- });
28
- const proc = spawn('claude', args, {
29
- cwd: options.workingDir,
30
- stdio: ['pipe', 'pipe', 'pipe'],
31
- });
32
- const parserState = createParserState();
33
- if (!proc.stdout)
34
- throw new Error('Claude process has no stdout');
35
- const rl = readline.createInterface({
36
- input: proc.stdout,
37
- crlfDelay: Number.POSITIVE_INFINITY,
94
+ workingDir: options.workingDir,
95
+ mcpServers: toMcpServersMap(options.mcpServers),
96
+ hooks,
97
+ canUseTool,
98
+ stderr: (data) => {
99
+ const lower = data.toLowerCase();
100
+ if (lower.includes('rate limit exceeded') ||
101
+ lower.includes('rate_limit_exceeded') ||
102
+ (lower.includes('429') && lower.includes('rate')) ||
103
+ lower.includes('quota exceeded')) {
104
+ onEvent({ kind: 'error', category: 'quota', message: data });
105
+ }
106
+ else if (lower.includes('no conversation found with session id')) {
107
+ onEvent({ kind: 'error', category: 'resume_failed', message: data });
108
+ }
109
+ else if (data.trim().length > 0) {
110
+ console.warn(`[claude-engine stderr] ${data}`);
111
+ }
112
+ },
38
113
  });
114
+ sdkOptions.abortController = abortController;
115
+ const q = query({ prompt: effectivePrompt, options: sdkOptions });
39
116
  let discoveredSessionId;
40
- rl.on('line', (line) => {
41
- const { events } = parseClaudeLine(line, parserState);
42
- for (const ev of events) {
43
- if (ev.kind === 'session:started')
44
- discoveredSessionId = ev.engineSessionId;
117
+ // A throwing onEvent handler (e.g. DB query against a closed connection
118
+ // during async test teardown) must not escape as an unhandled rejection.
119
+ const safeEmit = (ev) => {
120
+ try {
45
121
  onEvent(ev);
46
122
  }
47
- });
48
- // Line-buffer stderr so we see one event per log line instead of
49
- // arbitrary byte chunks, and restrict quota detection to clear rate-
50
- // limit signals (not every occurrence of the word "rate" or "quota").
51
- // Non-quota stderr lines are logged to the console but do NOT emit
52
- // an error event — this avoids false positives flooding the UI.
53
- const stderrRl = proc.stderr
54
- ? readline.createInterface({
55
- input: proc.stderr,
56
- crlfDelay: Number.POSITIVE_INFINITY,
57
- })
58
- : undefined;
59
- // Known benign stderr lines from the Claude CLI that should NOT be
60
- // logged — they flood the dev console and carry no actionable info.
61
- // Strip ANSI color codes before matching.
62
- // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes by design
63
- const stripAnsi = (s) => s.replace(/\u001b\[\d+m/g, '');
64
- function isBenignStderr(line) {
65
- const cleaned = stripAnsi(line).trim();
66
- return /^warning: no stdin data received in \d+s/i.test(cleaned);
67
- }
68
- stderrRl?.on('line', (line) => {
69
- const lower = line.toLowerCase();
70
- const isQuota = lower.includes('rate limit exceeded') ||
71
- lower.includes('rate_limit_exceeded') ||
72
- (lower.includes('429') && lower.includes('rate')) ||
73
- lower.includes('quota exceeded');
74
- const isResumeFailed = lower.includes('no conversation found with session id');
75
- if (isQuota) {
76
- onEvent({ kind: 'error', category: 'quota', message: line });
123
+ catch (err) {
124
+ console.error('[claude-engine] onEvent handler threw:', err);
77
125
  }
78
- else if (isResumeFailed) {
79
- onEvent({ kind: 'error', category: 'resume_failed', message: line });
80
- console.warn(`[claude-engine stderr] ${line}`);
126
+ };
127
+ let iteratorRunning = false;
128
+ let userInterrupted = false;
129
+ const iteratorPromise = (async () => {
130
+ iteratorRunning = true;
131
+ try {
132
+ for await (const msg of q) {
133
+ const events = mapSdkMessage(msg, mapperState);
134
+ for (const ev of events) {
135
+ if (ev.kind === 'session:started')
136
+ discoveredSessionId = ev.engineSessionId;
137
+ safeEmit(ev);
138
+ }
139
+ }
140
+ // If the SDK ended with a `result.subtype === 'error_*'`, the
141
+ // event-mapper already surfaced an `error` event but the iterator
142
+ // still terminated naturally. Reflect that in the session:ended
143
+ // reason so the orchestrator transitions the workspace to `error`.
144
+ safeEmit({
145
+ kind: 'session:ended',
146
+ reason: mapperState.sawErrorResult ? 'error' : 'completed',
147
+ exitCode: mapperState.sawErrorResult ? null : 0,
148
+ });
81
149
  }
82
- else if (line.trim().length > 0 && !isBenignStderr(line)) {
83
- console.warn(`[claude-engine stderr] ${line}`);
150
+ catch (err) {
151
+ // Treat any abort we triggered (stop() abortController.abort()) as
152
+ // a clean kill. The SDK sometimes throws a generic Error with message
153
+ // "Claude Code process aborted by user" instead of a typed AbortError.
154
+ const error = err;
155
+ const isAbort = userInterrupted ||
156
+ error.name === 'AbortError' ||
157
+ abortController.signal.aborted ||
158
+ /aborted by user|process aborted|abortError|ede_diagnostic/i.test(error.message ?? '');
159
+ if (isAbort) {
160
+ safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
161
+ }
162
+ else {
163
+ safeEmit({
164
+ kind: 'error',
165
+ category: 'spawn_failed',
166
+ message: error.message,
167
+ });
168
+ safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
169
+ }
84
170
  }
85
- });
86
- // 'error' fires when spawn itself fails (e.g. ENOENT if the `claude`
87
- // binary is missing from PATH). In that case 'exit' never fires, so we
88
- // emit the lifecycle pair here and clean the MCP config ourselves.
89
- proc.on('error', (err) => {
90
- onEvent({ kind: 'error', category: 'spawn_failed', message: err.message });
91
- onEvent({ kind: 'session:ended', reason: 'error', exitCode: null });
92
- cleanupMcpConfig(options.workingDir);
93
- rl.close();
94
- stderrRl?.close();
95
- });
96
- proc.on('exit', (code) => {
97
- cleanupMcpConfig(options.workingDir);
98
- rl.close();
99
- stderrRl?.close();
100
- onEvent({
101
- kind: 'session:ended',
102
- reason: code === 0 ? 'completed' : code === null ? 'killed' : 'error',
103
- exitCode: code,
104
- });
105
- });
171
+ finally {
172
+ // Drain any callback still pending (SDK terminated while awaiting an
173
+ // answer). canUseTool's abort path covers signalled stops; this
174
+ // covers natural iterator completion.
175
+ for (const resolver of pendingResolvers.values()) {
176
+ try {
177
+ resolver.resolve({ behavior: 'deny', message: 'session ended', interrupt: false });
178
+ }
179
+ catch {
180
+ // best-effort
181
+ }
182
+ }
183
+ pendingResolvers.clear();
184
+ iteratorRunning = false;
185
+ }
186
+ })();
106
187
  const engineProcess = {
107
188
  get pid() {
108
- return proc.pid;
189
+ return undefined;
109
190
  },
110
191
  get engineSessionId() {
111
192
  return discoveredSessionId;
112
193
  },
113
- sendMessage(text) {
114
- if (!proc.stdin?.writable)
115
- throw new Error('Agent stdin not writable');
116
- proc.stdin.write(`${text}\n`);
194
+ isAlive() {
195
+ return iteratorRunning;
117
196
  },
118
- interrupt() {
119
- if (proc.pid !== undefined)
120
- process.kill(proc.pid, 'SIGINT');
197
+ sendMessage() {
198
+ throw new Error('sendMessage not supported in single-shot SDK mode');
121
199
  },
122
- stop() {
123
- return new Promise((resolve) => {
124
- if (proc.killed || proc.exitCode !== null)
125
- return resolve();
126
- let resolved = false;
127
- let killTimer;
128
- let hardTimeout;
129
- const doResolve = () => {
130
- if (resolved)
131
- return;
132
- resolved = true;
133
- if (killTimer)
134
- clearTimeout(killTimer);
135
- if (hardTimeout)
136
- clearTimeout(hardTimeout);
137
- resolve();
138
- };
139
- proc.once('exit', doResolve);
200
+ interrupt() {
201
+ userInterrupted = true;
202
+ const qq = q;
203
+ if (typeof qq.interrupt === 'function') {
140
204
  try {
141
- proc.kill('SIGTERM');
205
+ const r = qq.interrupt();
206
+ if (r && typeof r.catch === 'function') {
207
+ ;
208
+ r.catch(() => {
209
+ /* ignore */
210
+ });
211
+ }
142
212
  }
143
213
  catch {
144
- // Already dead
214
+ abortController.abort();
145
215
  }
146
- killTimer = setTimeout(() => {
147
- try {
148
- if (!proc.killed)
149
- proc.kill('SIGKILL');
150
- }
151
- catch {
152
- // Ignore
153
- }
154
- }, 5000);
155
- killTimer.unref?.();
156
- // Hard-timeout safety net: if the process hasn't exited within 10s
157
- // (5s after SIGKILL), resolve anyway so callers never hang forever.
158
- hardTimeout = setTimeout(() => {
159
- console.warn('[claude-engine] stop() hard-timeout reached, resolving anyway');
160
- doResolve();
161
- }, 10000);
162
- hardTimeout.unref?.();
216
+ }
217
+ else {
218
+ abortController.abort();
219
+ }
220
+ },
221
+ async stop() {
222
+ abortController.abort();
223
+ try {
224
+ await iteratorPromise;
225
+ }
226
+ catch {
227
+ // swallow best effort
228
+ }
229
+ },
230
+ resolvePendingUserInput(toolCallId, response) {
231
+ const resolver = pendingResolvers.get(toolCallId);
232
+ if (!resolver)
233
+ return false;
234
+ pendingResolvers.delete(toolCallId);
235
+ if (response.kind === 'question') {
236
+ // Echo the original questions array + answers so the SDK
237
+ // reconstructs the AskUserQuestion tool input.
238
+ const original = resolver.input;
239
+ const questions = original.questions;
240
+ resolver.resolve({
241
+ behavior: 'allow',
242
+ updatedInput: {
243
+ ...(typeof questions !== 'undefined' ? { questions } : {}),
244
+ answers: response.answers,
245
+ },
246
+ });
247
+ return true;
248
+ }
249
+ if (response.kind === 'question-cancel') {
250
+ // Deny so the agent gets an error tool_result and can adapt.
251
+ resolver.resolve({
252
+ behavior: 'deny',
253
+ message: response.reason ?? 'User cancelled the question',
254
+ interrupt: false,
255
+ });
256
+ return true;
257
+ }
258
+ if (response.kind === 'permission-allow') {
259
+ resolver.resolve({ behavior: 'allow', updatedInput: resolver.input });
260
+ return true;
261
+ }
262
+ // permission-deny
263
+ resolver.resolve({
264
+ behavior: 'deny',
265
+ message: response.reason ?? 'denied by user',
266
+ interrupt: false,
163
267
  });
268
+ return true;
164
269
  },
165
270
  };
166
271
  return engineProcess;
@@ -0,0 +1,234 @@
1
+ // ── Helpers ───────────────────────────────────────────────────────────────────
2
+ // `rate_limit_info` is shaped for claude.ai subscriptions and may evolve.
3
+ // Keep the defensive normalisation so a schema bump doesn't drop bucket info.
4
+ function normalizeResetsAt(raw) {
5
+ if (typeof raw === 'string' && raw.length > 0)
6
+ return raw;
7
+ if (typeof raw === 'number' && Number.isFinite(raw))
8
+ return new Date(raw * 1000).toISOString();
9
+ return undefined;
10
+ }
11
+ function extractUsedPct(source) {
12
+ const raw = (source.utilization ?? source.used_percent ?? source.percent_used ?? source.usedPct);
13
+ if (typeof raw === 'number' && Number.isFinite(raw))
14
+ return raw <= 1 ? raw * 100 : raw;
15
+ const used = source.used ?? source.current ?? source.spent;
16
+ const limit = source.limit ?? source.max ?? source.allowed;
17
+ if (typeof used === 'number' && typeof limit === 'number' && limit > 0)
18
+ return (used / limit) * 100;
19
+ return null;
20
+ }
21
+ function makeBucket(id, source) {
22
+ const usedPct = extractUsedPct(source) ?? source.__fallbackPct ?? null;
23
+ if (usedPct === null)
24
+ return null;
25
+ const resetsAt = normalizeResetsAt(source.resets_at ?? source.reset_at ?? source.resetsAt ?? source.resetAt);
26
+ const label = (typeof source.label === 'string' && source.label) || undefined;
27
+ const used = source.used ?? source.current ?? source.spent;
28
+ const limit = source.limit ?? source.max ?? source.allowed;
29
+ const details = used !== undefined && limit !== undefined ? `${String(used)} / ${String(limit)}` : undefined;
30
+ return { id, label, usedPct: Math.max(0, Math.min(100, usedPct)), resetsAt, details };
31
+ }
32
+ function normalizeRateLimitInfo(info) {
33
+ const buckets = [];
34
+ if (typeof info.rateLimitType === 'string') {
35
+ const b = makeBucket(info.rateLimitType, { ...info, __fallbackPct: 0 });
36
+ if (b)
37
+ buckets.push(b);
38
+ }
39
+ if (Array.isArray(info.buckets)) {
40
+ for (const entry of info.buckets) {
41
+ if (!entry || typeof entry !== 'object')
42
+ continue;
43
+ const obj = entry;
44
+ const id = (typeof obj.id === 'string' && obj.id) ||
45
+ (typeof obj.name === 'string' && obj.name) ||
46
+ (typeof obj.rateLimitType === 'string' && obj.rateLimitType) ||
47
+ 'unknown';
48
+ const b = makeBucket(id, obj);
49
+ if (b)
50
+ buckets.push(b);
51
+ }
52
+ }
53
+ return { buckets };
54
+ }
55
+ export function createMapperState() {
56
+ return { sessionStartedEmitted: false, openMessages: new Map(), sawErrorResult: false };
57
+ }
58
+ /** Known SDK `result` subtypes that indicate the run failed. */
59
+ const KNOWN_ERROR_RESULT_SUBTYPES = new Set(['error_max_turns', 'error_during_execution']);
60
+ function isErrorResultSubtype(subtype) {
61
+ if (!subtype)
62
+ return false;
63
+ if (KNOWN_ERROR_RESULT_SUBTYPES.has(subtype))
64
+ return true;
65
+ return subtype.startsWith('error');
66
+ }
67
+ /**
68
+ * Maps a single typed `SDKMessage` to zero or more `AgentEvent`s, mutating
69
+ * `state` as needed.
70
+ */
71
+ export function mapSdkMessage(msg, state) {
72
+ // Treat as a generic record — the SDK discriminated union is too broad to
73
+ // narrow per branch here.
74
+ const parsed = msg;
75
+ const type = parsed.type;
76
+ const subtype = parsed.subtype;
77
+ const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id : undefined;
78
+ const events = [];
79
+ // Rate-limit events are top-level in the SDK (no longer nested under `system`).
80
+ if (type === 'rate_limit_event') {
81
+ const info = parsed.rate_limit_info;
82
+ if (info && typeof info === 'object') {
83
+ events.push({ kind: 'rate_limit', info: normalizeRateLimitInfo(info) });
84
+ }
85
+ return events;
86
+ }
87
+ if (type === 'system') {
88
+ if (subtype === 'compact' || subtype === 'compact_boundary') {
89
+ events.push({ kind: 'session:compacted' });
90
+ return events;
91
+ }
92
+ if (subtype === 'task_started' || subtype === 'task_progress' || subtype === 'task_notification') {
93
+ const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
94
+ if (toolCallId) {
95
+ const usage = parsed.usage;
96
+ const taskStatus = typeof parsed.status === 'string' ? parsed.status : undefined;
97
+ const isDone = subtype === 'task_notification' &&
98
+ taskStatus !== undefined &&
99
+ ['completed', 'stopped', 'failed', 'cancelled'].includes(taskStatus);
100
+ events.push({
101
+ kind: 'subagent:progress',
102
+ toolCallId,
103
+ status: isDone ? 'done' : 'running',
104
+ description: typeof parsed.description === 'string' ? parsed.description : undefined,
105
+ taskType: typeof parsed.task_type === 'string' ? parsed.task_type : undefined,
106
+ lastToolName: typeof parsed.last_tool_name === 'string' ? parsed.last_tool_name : undefined,
107
+ totalTokens: typeof usage?.total_tokens === 'number' ? usage.total_tokens : undefined,
108
+ toolUses: typeof usage?.tool_uses === 'number' ? usage.tool_uses : undefined,
109
+ durationMs: typeof usage?.duration_ms === 'number' ? usage.duration_ms : undefined,
110
+ });
111
+ }
112
+ return events;
113
+ }
114
+ if (subtype === 'init') {
115
+ if (sessionId && (!state.sessionStartedEmitted || state.sessionId !== sessionId)) {
116
+ events.push({
117
+ kind: 'session:started',
118
+ engineSessionId: sessionId,
119
+ model: typeof parsed.model === 'string' ? parsed.model : undefined,
120
+ });
121
+ state.sessionStartedEmitted = true;
122
+ state.sessionId = sessionId;
123
+ }
124
+ if (Array.isArray(parsed.slash_commands) && parsed.slash_commands.length > 0) {
125
+ events.push({ kind: 'skills:discovered', skills: parsed.slash_commands });
126
+ }
127
+ return events;
128
+ }
129
+ return events;
130
+ }
131
+ if (type === 'assistant') {
132
+ const message = parsed.message;
133
+ const messageId = typeof message?.id === 'string' ? message.id : 'unknown';
134
+ const content = Array.isArray(message?.content) ? message?.content : [];
135
+ // SDK runs sometimes finish implicitly when the next turn begins. Close
136
+ // stale openMessages so the UI's streaming spinner doesn't hang.
137
+ for (const openId of Array.from(state.openMessages.keys())) {
138
+ if (openId !== messageId) {
139
+ events.push({ kind: 'message:end', messageId: openId });
140
+ state.openMessages.delete(openId);
141
+ }
142
+ }
143
+ if (!state.openMessages.has(messageId)) {
144
+ state.openMessages.set(messageId, { sawText: false });
145
+ }
146
+ const msgState = state.openMessages.get(messageId);
147
+ if (!msgState)
148
+ return events;
149
+ for (const block of content) {
150
+ const blockType = block.type;
151
+ if (blockType === 'text' && typeof block.text === 'string') {
152
+ events.push({ kind: 'message:text', messageId, text: block.text, streaming: true });
153
+ msgState.sawText = true;
154
+ if (block.text.includes('[BRAINSTORM_COMPLETE]')) {
155
+ events.push({ kind: 'session:brainstorm-complete' });
156
+ }
157
+ }
158
+ if (blockType === 'tool_use') {
159
+ events.push({
160
+ kind: 'tool:call',
161
+ messageId,
162
+ toolCallId: typeof block.id === 'string' ? block.id : 'unknown',
163
+ name: typeof block.name === 'string' ? block.name : 'unknown',
164
+ input: block.input ?? {},
165
+ });
166
+ }
167
+ if (blockType === 'thinking') {
168
+ events.push({
169
+ kind: 'message:thinking',
170
+ messageId,
171
+ text: typeof block.thinking === 'string' ? block.thinking : '',
172
+ });
173
+ }
174
+ }
175
+ // Only terminal turns carry non-null `stop_reason`; intermediate deltas
176
+ // have `null` and must NOT trigger message:end.
177
+ const stopReason = message?.stop_reason;
178
+ const isStop = parsed.message_stop === true || (stopReason !== undefined && stopReason !== null);
179
+ if (isStop) {
180
+ events.push({ kind: 'message:end', messageId });
181
+ state.openMessages.delete(messageId);
182
+ }
183
+ return events;
184
+ }
185
+ if (type === 'user') {
186
+ const message = parsed.message;
187
+ const content = Array.isArray(message?.content) ? message?.content : [];
188
+ for (const block of content) {
189
+ if (block.type === 'tool_result') {
190
+ events.push({
191
+ kind: 'tool:result',
192
+ toolCallId: typeof block.tool_use_id === 'string' ? block.tool_use_id : 'unknown',
193
+ output: block.content ?? null,
194
+ isError: block.is_error === true,
195
+ });
196
+ }
197
+ }
198
+ return events;
199
+ }
200
+ if (type === 'result') {
201
+ // Terminal event — close any still-streaming messages.
202
+ for (const openId of Array.from(state.openMessages.keys())) {
203
+ events.push({ kind: 'message:end', messageId: openId });
204
+ state.openMessages.delete(openId);
205
+ }
206
+ // Detect error variants of `result` (e.g. `error_max_turns`,
207
+ // `error_during_execution`) and surface them as a proper `error` event so
208
+ // the orchestrator can transition the workspace to `error` instead of
209
+ // `completed`. The flag on `state` lets the engine override the
210
+ // post-loop session:ended reason.
211
+ if (isErrorResultSubtype(subtype)) {
212
+ state.sawErrorResult = true;
213
+ const detail = (typeof parsed.error === 'string' && parsed.error) || (typeof parsed.result === 'string' && parsed.result) || '';
214
+ const message = detail ? `Agent run failed (${subtype}): ${detail}` : `Agent run failed (${subtype})`;
215
+ events.push({ kind: 'error', category: 'other', message });
216
+ }
217
+ const usage = parsed.usage;
218
+ if (usage) {
219
+ const costUsd = typeof parsed.total_cost_usd === 'number' ? parsed.total_cost_usd : undefined;
220
+ events.push({
221
+ kind: 'usage',
222
+ inputTokens: Number(usage.input_tokens ?? 0),
223
+ outputTokens: Number(usage.output_tokens ?? 0),
224
+ cacheRead: typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : undefined,
225
+ cacheWrite: typeof usage.cache_creation_input_tokens === 'number'
226
+ ? usage.cache_creation_input_tokens
227
+ : undefined,
228
+ costUsd,
229
+ });
230
+ }
231
+ return events;
232
+ }
233
+ return events;
234
+ }