@loicngr/kobo 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/AGENTS.md +6 -1
  2. package/README.md +43 -29
  3. package/dist/server/db/index.js +17 -0
  4. package/dist/server/db/migrations.js +10 -0
  5. package/dist/server/db/schema.js +2 -1
  6. package/dist/server/index.js +26 -5
  7. package/dist/server/middleware/migration-guard.js +15 -0
  8. package/dist/server/routes/dev-server.js +3 -2
  9. package/dist/server/routes/documents.js +113 -0
  10. package/dist/server/routes/engines.js +9 -0
  11. package/dist/server/routes/migration.js +5 -0
  12. package/dist/server/routes/workspaces.js +35 -11
  13. package/dist/server/services/agent/engines/claude-code/args-builder.js +22 -0
  14. package/dist/server/services/agent/engines/claude-code/capabilities.js +17 -0
  15. package/dist/server/services/agent/engines/claude-code/engine.js +163 -0
  16. package/dist/server/services/agent/engines/claude-code/mcp-config.js +23 -0
  17. package/dist/server/services/agent/engines/claude-code/stream-parser.js +224 -0
  18. package/dist/server/services/agent/engines/registry.js +21 -0
  19. package/dist/server/services/agent/engines/types.js +18 -0
  20. package/dist/server/services/agent/event-router.js +4 -0
  21. package/dist/server/services/agent/orchestrator.js +582 -0
  22. package/dist/server/services/agent/session-controller.js +79 -0
  23. package/dist/server/services/content-migration-service.js +155 -0
  24. package/dist/server/services/db-backup-service.js +15 -0
  25. package/dist/server/services/websocket-service.js +81 -50
  26. package/dist/server/services/workspace-service.js +11 -5
  27. package/dist/server/utils/paths.js +1 -1
  28. package/dist/shared/models.js +50 -0
  29. package/package.json +1 -1
  30. package/src/client/dist/spa/assets/ActivityFeed-CFuT6H5u.js +7 -0
  31. package/src/client/dist/spa/assets/ActivityFeed-DXYafbn4.css +1 -0
  32. package/src/client/dist/spa/assets/ClosePopup-DhM1C4Zw.js +1 -0
  33. package/src/client/dist/spa/assets/CreatePage-sGrkfyOm.js +2 -0
  34. package/src/client/dist/spa/assets/CreatePage-yu2IH7GW.css +1 -0
  35. package/src/client/dist/spa/assets/DiffViewer-BVU58ujc.css +1 -0
  36. package/src/client/dist/spa/assets/DiffViewer-BwSRtVRI.js +2 -0
  37. package/src/client/dist/spa/assets/HealthPage-BO-bMpEu.js +1 -0
  38. package/src/client/dist/spa/assets/MainLayout-BiBJtDTk.css +1 -0
  39. package/src/client/dist/spa/assets/{MainLayout-_oPM07ln.js → MainLayout-BpqOChIX.js} +17 -17
  40. package/src/client/dist/spa/assets/QBadge-DqtcDv8D.js +1 -0
  41. package/src/client/dist/spa/assets/QBtn-CyzfM9-_.js +1 -0
  42. package/src/client/dist/spa/assets/QChip-KJoHYE6F.js +1 -0
  43. package/src/client/dist/spa/assets/QDialog-DQeAxY3-.js +1 -0
  44. package/src/client/dist/spa/assets/QExpansionItem-DCRks-Ra.js +1 -0
  45. package/src/client/dist/spa/assets/{QSpinner-CliSLjf8.js → QIcon-B0-pH3Qs.js} +1 -1
  46. package/src/client/dist/spa/assets/QItemLabel-Codqjisk.js +1 -0
  47. package/src/client/dist/spa/assets/QItemSection-CGpX7GcL.js +1 -0
  48. package/src/client/dist/spa/assets/QList-B-MkPF7n.js +1 -0
  49. package/src/client/dist/spa/assets/QPage-yqdKDG7-.js +1 -0
  50. package/src/client/dist/spa/assets/QScrollArea-e5qTqwcb.js +1 -0
  51. package/src/client/dist/spa/assets/QSeparator-rkjCbX2M.js +1 -0
  52. package/src/client/dist/spa/assets/QSlideTransition-BQxI8l5r.js +1 -0
  53. package/src/client/dist/spa/assets/QSpace-BNr0AftG.js +1 -0
  54. package/src/client/dist/spa/assets/QSpinnerDots-DEiRooBD.js +1 -0
  55. package/src/client/dist/spa/assets/QTabPanels--6cYe2US.js +1 -0
  56. package/src/client/dist/spa/assets/QTooltip-C4CPesBX.js +1 -0
  57. package/src/client/dist/spa/assets/SearchPage-BrUbbtgI.js +1 -0
  58. package/src/client/dist/spa/assets/SettingsPage-B3elO1PX.js +1 -0
  59. package/src/client/dist/spa/assets/TouchPan-BT6phK1f.js +1 -0
  60. package/src/client/dist/spa/assets/WorkspacePage-BHl17_tY.js +4 -0
  61. package/src/client/dist/spa/assets/WorkspacePage-DPGiH02q.css +1 -0
  62. package/src/client/dist/spa/assets/build-path-tree-Bgl2q74t.js +1 -0
  63. package/src/client/dist/spa/assets/{cssMode-DMX8jq8u.js → cssMode-B1wQ-79R.js} +1 -1
  64. package/src/client/dist/spa/assets/documents-CHc8t22V.js +60 -0
  65. package/src/client/dist/spa/assets/{editor.api-DirOkGGg.js → editor.api-CRb_5Zw6.js} +1 -1
  66. package/src/client/dist/spa/assets/{editor.main-DC4ezIu0.js → editor.main-C5sdCvGW.js} +3 -3
  67. package/src/client/dist/spa/assets/format-Bttc9ToS.js +1 -0
  68. package/src/client/dist/spa/assets/{formatters-BzaS4w0I.js → formatters-BDadphwz.js} +1 -1
  69. package/src/client/dist/spa/assets/{freemarker2-DI9xJfj0.js → freemarker2-CVSnsZk-.js} +1 -1
  70. package/src/client/dist/spa/assets/{handlebars-B9F-pScn.js → handlebars-uL_pucGI.js} +1 -1
  71. package/src/client/dist/spa/assets/{html-DTe2v8Q8.js → html-CatZVwWp.js} +1 -1
  72. package/src/client/dist/spa/assets/{htmlMode-F_XLjWfJ.js → htmlMode-DTDzEngo.js} +1 -1
  73. package/src/client/dist/spa/assets/i18n-D-VdPLEh.js +1 -0
  74. package/src/client/dist/spa/assets/index-CUI-zN26.js +2 -0
  75. package/src/client/dist/spa/assets/{javascript-B9xJRPC6.js → javascript-DeHBpolA.js} +1 -1
  76. package/src/client/dist/spa/assets/{jsonMode-DTZ6j6UO.js → jsonMode-Bma_YGGm.js} +1 -1
  77. package/src/client/dist/spa/assets/{liquid-BjU5MtW6.js → liquid-CW7xQEG_.js} +1 -1
  78. package/src/client/dist/spa/assets/{mdx-BMUpG7Be.js → mdx-BsYUhMzF.js} +1 -1
  79. package/src/client/dist/spa/assets/models-BbSRHL9b.js +1 -0
  80. package/src/client/dist/spa/assets/{monaco.contribution-D7JUf8DP.js → monaco.contribution-Du0atePv.js} +2 -2
  81. package/src/client/dist/spa/assets/private.use-form-D1RuEt2P.js +1 -0
  82. package/src/client/dist/spa/assets/{python-Dz0D4uSk.js → python-D7DQWXZm.js} +1 -1
  83. package/src/client/dist/spa/assets/{razor-D7CFxuwR.js → razor-B2ZxF301.js} +1 -1
  84. package/src/client/dist/spa/assets/scroll-JVVkg2Ng.js +1 -0
  85. package/src/client/dist/spa/assets/touch-CBLrR6_z.js +1 -0
  86. package/src/client/dist/spa/assets/{tsMode-DjscaxpS.js → tsMode-B4Xul5xA.js} +1 -1
  87. package/src/client/dist/spa/assets/{typescript-DozCWZl2.js → typescript-CdsKQuLT.js} +1 -1
  88. package/src/client/dist/spa/assets/use-checkbox-DYiZQsbF.js +1 -0
  89. package/src/client/dist/spa/assets/use-id-CeduaJbU.js +1 -0
  90. package/src/client/dist/spa/assets/use-portal-DBe4lcC2.js +1 -0
  91. package/src/client/dist/spa/assets/use-quasar-Ch82z8H5.js +1 -0
  92. package/src/client/dist/spa/assets/{xml-DFOJMT39.js → xml-Wap00dMv.js} +1 -1
  93. package/src/client/dist/spa/assets/{yaml-yEefnsXm.js → yaml-BxRDHC24.js} +1 -1
  94. package/src/client/dist/spa/index.html +12 -14
  95. package/src/mcp-server/README.md +1 -1
  96. package/dist/server/routes/plans.js +0 -89
  97. package/dist/server/services/agent-manager.js +0 -621
  98. package/src/client/dist/spa/assets/ActivityFeed-0GR1zPoc.js +0 -10
  99. package/src/client/dist/spa/assets/ActivityFeed-CfsKExt9.css +0 -1
  100. package/src/client/dist/spa/assets/ClosePopup-CdSn7HO8.js +0 -1
  101. package/src/client/dist/spa/assets/CreatePage-dMi4xVYN.css +0 -1
  102. package/src/client/dist/spa/assets/CreatePage-je_7dC5I.js +0 -2
  103. package/src/client/dist/spa/assets/DiffViewer-DREYX-8k.js +0 -2
  104. package/src/client/dist/spa/assets/DiffViewer-DiHFLSk4.css +0 -1
  105. package/src/client/dist/spa/assets/HealthPage-Do8QZdxw.js +0 -1
  106. package/src/client/dist/spa/assets/MainLayout-B5poKEy_.css +0 -1
  107. package/src/client/dist/spa/assets/QBadge-Bvh-hQ8K.js +0 -1
  108. package/src/client/dist/spa/assets/QBtn-BsD8vrWq.js +0 -1
  109. package/src/client/dist/spa/assets/QDialog-CkbLS1If.js +0 -1
  110. package/src/client/dist/spa/assets/QExpansionItem-UgkE560c.js +0 -1
  111. package/src/client/dist/spa/assets/QList-D80ms7bw.js +0 -1
  112. package/src/client/dist/spa/assets/QMenu-DU-wiY_A.js +0 -1
  113. package/src/client/dist/spa/assets/QPage-BKY2-sf-.js +0 -1
  114. package/src/client/dist/spa/assets/QSpace-C5Ebr0vq.js +0 -1
  115. package/src/client/dist/spa/assets/QSpinnerDots-Dp12eHrB.js +0 -1
  116. package/src/client/dist/spa/assets/QTabPanels-C7lWp1yU.js +0 -1
  117. package/src/client/dist/spa/assets/QToggle-B0HvuNEg.js +0 -1
  118. package/src/client/dist/spa/assets/QTooltip-kLXuUa_m.js +0 -1
  119. package/src/client/dist/spa/assets/SearchPage-CCfyqBKh.js +0 -1
  120. package/src/client/dist/spa/assets/SettingsPage-CmyIsV-S.js +0 -1
  121. package/src/client/dist/spa/assets/TouchPan-CVMnGs0y.js +0 -1
  122. package/src/client/dist/spa/assets/WorkspacePage-CWRMLYs-.css +0 -1
  123. package/src/client/dist/spa/assets/WorkspacePage-Cl7YrG51.js +0 -4
  124. package/src/client/dist/spa/assets/focus-manager-DYbz9jFW.js +0 -1
  125. package/src/client/dist/spa/assets/format-Cyg8IgRi.js +0 -1
  126. package/src/client/dist/spa/assets/i18n-B13zBh1H.js +0 -1
  127. package/src/client/dist/spa/assets/i18n-CCWLBc0p.js +0 -1
  128. package/src/client/dist/spa/assets/index-DoNZ_5QK.js +0 -5
  129. package/src/client/dist/spa/assets/marked.esm-DCmk6NO8.js +0 -60
  130. package/src/client/dist/spa/assets/models-B8fzv7K4.js +0 -1
  131. package/src/client/dist/spa/assets/pinia-C3JsrLkB.js +0 -1
  132. package/src/client/dist/spa/assets/private.use-form-BhKyDtO7.js +0 -1
  133. package/src/client/dist/spa/assets/scroll-CLibRGI-.js +0 -1
  134. package/src/client/dist/spa/assets/settings-B69lIVX0.js +0 -1
  135. package/src/client/dist/spa/assets/touch-ChrvzrnI.js +0 -1
  136. package/src/client/dist/spa/assets/use-dark-DnuCB6tC.js +0 -1
  137. package/src/client/dist/spa/assets/use-quasar-DBoizHBW.js +0 -1
  138. /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-Cxt1D8wE.js → _plugin-vue_export-helper-r4mAJOHR.js} +0 -0
  139. /package/src/client/dist/spa/assets/{abap-CFuyUYKP.js → abap-Bgec7Keq.js} +0 -0
  140. /package/src/client/dist/spa/assets/{apex-Ctq_xcrv.js → apex-VBlPwEoQ.js} +0 -0
  141. /package/src/client/dist/spa/assets/{azcli-BBQSVn-C.js → azcli-DKqrEFBx.js} +0 -0
  142. /package/src/client/dist/spa/assets/{bat-DbnqAfvr.js → bat-DdgQWy_0.js} +0 -0
  143. /package/src/client/dist/spa/assets/{bicep-BtDlIXop.js → bicep-CRMM43EB.js} +0 -0
  144. /package/src/client/dist/spa/assets/{cameligo-BLeJgKTj.js → cameligo-UatALtML.js} +0 -0
  145. /package/src/client/dist/spa/assets/{clojure-aZUQIUKP.js → clojure-D8JU08RA.js} +0 -0
  146. /package/src/client/dist/spa/assets/{coffee-Secadq9U.js → coffee-C56wu358.js} +0 -0
  147. /package/src/client/dist/spa/assets/{cpp-JicRPTRv.js → cpp-CyZLvhJG.js} +0 -0
  148. /package/src/client/dist/spa/assets/{csharp-C7NSOZyj.js → csharp-BJl3ixva.js} +0 -0
  149. /package/src/client/dist/spa/assets/{csp-CIje7830.js → csp-CxEKxmO-.js} +0 -0
  150. /package/src/client/dist/spa/assets/{css-G0bm1q_M.js → css-B0t_muXd.js} +0 -0
  151. /package/src/client/dist/spa/assets/{cypher-CldD5D0u.js → cypher-D1hqiMFD.js} +0 -0
  152. /package/src/client/dist/spa/assets/{dart-DIK3l8YT.js → dart-Bz550Pyv.js} +0 -0
  153. /package/src/client/dist/spa/assets/{dockerfile-czxaGh2L.js → dockerfile-CIXgVAuA.js} +0 -0
  154. /package/src/client/dist/spa/assets/{ecl-BqdYhwmw.js → ecl-D9qbvZoA.js} +0 -0
  155. /package/src/client/dist/spa/assets/{elixir-m52LePTW.js → elixir-b2M38fAy.js} +0 -0
  156. /package/src/client/dist/spa/assets/{flow9-B5QJ9GvZ.js → flow9-Dq1UYMkt.js} +0 -0
  157. /package/src/client/dist/spa/assets/{fsharp-B15czHsH.js → fsharp-CFNadkg7.js} +0 -0
  158. /package/src/client/dist/spa/assets/{go-BkoQxDo1.js → go-dSur1iB2.js} +0 -0
  159. /package/src/client/dist/spa/assets/{graphql-BnI6uRa_.js → graphql-qyhAo11d.js} +0 -0
  160. /package/src/client/dist/spa/assets/{hcl-CAwwENT7.js → hcl-DFzjMyzm.js} +0 -0
  161. /package/src/client/dist/spa/assets/{ini-BHM5zh1H.js → ini-TdzA8TIl.js} +0 -0
  162. /package/src/client/dist/spa/assets/{java-B5i95QvQ.js → java-CSGA9pkE.js} +0 -0
  163. /package/src/client/dist/spa/assets/{julia-DPDm885q.js → julia-9izz5OsY.js} +0 -0
  164. /package/src/client/dist/spa/assets/{kotlin-qoccd5BP.js → kotlin-DuPK7AtF.js} +0 -0
  165. /package/src/client/dist/spa/assets/{less-B6RU166D.js → less-B8d93iCg.js} +0 -0
  166. /package/src/client/dist/spa/assets/{lexon-YfUeoL1V.js → lexon-DWtEIyu7.js} +0 -0
  167. /package/src/client/dist/spa/assets/{lua-BIUI5y9b.js → lua-Ciq0OGgt.js} +0 -0
  168. /package/src/client/dist/spa/assets/{m3-D5SAbSdU.js → m3-Cki6JWj_.js} +0 -0
  169. /package/src/client/dist/spa/assets/{markdown-CVJLwHzJ.js → markdown-Cu47xwU0.js} +0 -0
  170. /package/src/client/dist/spa/assets/{mips-R-FZ3zOR.js → mips-BM8ui995.js} +0 -0
  171. /package/src/client/dist/spa/assets/{msdax-Blveyl9r.js → msdax-DqLio0_c.js} +0 -0
  172. /package/src/client/dist/spa/assets/{mysql-D4mY1AFx.js → mysql-v1wbjJOq.js} +0 -0
  173. /package/src/client/dist/spa/assets/{objective-c-BmXrLr4h.js → objective-c-CQl3PGSB.js} +0 -0
  174. /package/src/client/dist/spa/assets/{pascal-yxckoyvV.js → pascal-D4iW0ZtD.js} +0 -0
  175. /package/src/client/dist/spa/assets/{pascaligo-Q5JCwXMI.js → pascaligo-BdC9CZdj.js} +0 -0
  176. /package/src/client/dist/spa/assets/{perl-BF1Rrs5h.js → perl-BL10m4XD.js} +0 -0
  177. /package/src/client/dist/spa/assets/{pgsql-CnYB97wm.js → pgsql-Be_oqVo3.js} +0 -0
  178. /package/src/client/dist/spa/assets/{php-CdDfQfSg.js → php-BtvXSFRI.js} +0 -0
  179. /package/src/client/dist/spa/assets/{pla-whj-d71F.js → pla-B2vUy15C.js} +0 -0
  180. /package/src/client/dist/spa/assets/{postiats-ClfLr4I-.js → postiats-CbmTTfXr.js} +0 -0
  181. /package/src/client/dist/spa/assets/{powerquery-iRaBhuuk.js → powerquery-DszLhJGx.js} +0 -0
  182. /package/src/client/dist/spa/assets/{powershell-DjiEt5xK.js → powershell-B0dYktF6.js} +0 -0
  183. /package/src/client/dist/spa/assets/{protobuf-B6dcIEUr.js → protobuf-CZvaj1VX.js} +0 -0
  184. /package/src/client/dist/spa/assets/{pug-DtmHnjM9.js → pug-CPDx1B3S.js} +0 -0
  185. /package/src/client/dist/spa/assets/{qsharp-CELCyd79.js → qsharp-CDP9TFLl.js} +0 -0
  186. /package/src/client/dist/spa/assets/{r-ZpJXWV-o.js → r-8DbbFX2l.js} +0 -0
  187. /package/src/client/dist/spa/assets/{rate-limit-labels-dCPVjS61.js → rate-limit-labels-BoDORKFj.js} +0 -0
  188. /package/src/client/dist/spa/assets/{redis-BiHSNkAl.js → redis-DRWj9MtJ.js} +0 -0
  189. /package/src/client/dist/spa/assets/{redshift-DzuwYCHP.js → redshift-C6cElE_5.js} +0 -0
  190. /package/src/client/dist/spa/assets/{restructuredtext-YOT94bbS.js → restructuredtext-W9pS9n3m.js} +0 -0
  191. /package/src/client/dist/spa/assets/{ruby-BfiHr6Uu.js → ruby-BKnzWnk-.js} +0 -0
  192. /package/src/client/dist/spa/assets/{rust-JZ-uOoYM.js → rust-YPCclWwe.js} +0 -0
  193. /package/src/client/dist/spa/assets/{sb-CBglP1-t.js → sb-BgM4DTFb.js} +0 -0
  194. /package/src/client/dist/spa/assets/{scala-C9l41paw.js → scala-fz1OPLMl.js} +0 -0
  195. /package/src/client/dist/spa/assets/{scheme-B-InQ6hy.js → scheme-8Uz1RIbu.js} +0 -0
  196. /package/src/client/dist/spa/assets/{scss-v6OmJRN9.js → scss-Djo3IYXr.js} +0 -0
  197. /package/src/client/dist/spa/assets/{shell-Dyp6iwB6.js → shell-CINF5Tx_.js} +0 -0
  198. /package/src/client/dist/spa/assets/{solidity-D5epNWue.js → solidity-GgiNEuUm.js} +0 -0
  199. /package/src/client/dist/spa/assets/{sophia-Eva-79sB.js → sophia-Culj97P9.js} +0 -0
  200. /package/src/client/dist/spa/assets/{sparql-gvALLO1w.js → sparql-C2ZlpxOY.js} +0 -0
  201. /package/src/client/dist/spa/assets/{sql-COdamZYI.js → sql-BEf5Pg7Y.js} +0 -0
  202. /package/src/client/dist/spa/assets/{st-eMoImIwE.js → st-CT6UUoeH.js} +0 -0
  203. /package/src/client/dist/spa/assets/{swift-7R_T9RYH.js → swift-B5g0xTG3.js} +0 -0
  204. /package/src/client/dist/spa/assets/{symbols-CAg-nBkV.js → symbols-DCYodwb2.js} +0 -0
  205. /package/src/client/dist/spa/assets/{systemverilog-1pCEfaHU.js → systemverilog-CEgQz9DR.js} +0 -0
  206. /package/src/client/dist/spa/assets/{tcl-B_KgnhfE.js → tcl-D0qL2L0I.js} +0 -0
  207. /package/src/client/dist/spa/assets/{twig-CFZUJxb9.js → twig-BFUAVf1E.js} +0 -0
  208. /package/src/client/dist/spa/assets/{typespec-B1ZgHlud.js → typespec-CjVVcNKm.js} +0 -0
  209. /package/src/client/dist/spa/assets/{vb-DKdun5tL.js → vb-CZJr-DQz.js} +0 -0
  210. /package/src/client/dist/spa/assets/{vue-i18n-eUDnMrPl.js → vue-i18n-CeG0hR0Z.js} +0 -0
  211. /package/src/client/dist/spa/assets/{wgsl-CzNaxTrn.js → wgsl-ivoXUo2e.js} +0 -0
@@ -0,0 +1,17 @@
1
+ import { CLAUDE_MODELS } from '../../../../../shared/models.js';
2
+ export const CLAUDE_CODE_CAPABILITIES = {
3
+ // Models come from the shared catalogue in `src/shared/models.ts` — the
4
+ // ONE source of truth, consumed both by this file (for /api/engines and
5
+ // for validation in POST /api/workspaces) and by the frontend selectors.
6
+ models: CLAUDE_MODELS.map((m) => ({ id: m.id, label: m.label })),
7
+ effortLevels: [
8
+ { id: 'auto', label: 'Auto' },
9
+ { id: 'low', label: 'Low' },
10
+ { id: 'medium', label: 'Medium' },
11
+ { id: 'high', label: 'High' },
12
+ ],
13
+ permissionModes: ['auto-accept', 'plan'],
14
+ supportsResume: true,
15
+ supportsMcp: true,
16
+ supportsSkills: true,
17
+ };
@@ -0,0 +1,163 @@
1
+ import { spawn } from 'node:child_process';
2
+ import readline from 'node:readline';
3
+ import { buildClaudeArgs } from './args-builder.js';
4
+ import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
5
+ import { cleanupMcpConfig, writeMcpConfig } from './mcp-config.js';
6
+ import { createParserState, parseClaudeLine } from './stream-parser.js';
7
+ export function createClaudeCodeEngine() {
8
+ return {
9
+ id: 'claude-code',
10
+ displayName: 'Claude Code',
11
+ capabilities: CLAUDE_CODE_CAPABILITIES,
12
+ 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({
19
+ prompt: options.prompt,
20
+ model: options.model,
21
+ effort: options.effort,
22
+ permissionMode: options.permissionMode ?? 'auto-accept',
23
+ skipPermissions: options.settings.dangerouslySkipPermissions ?? true,
24
+ resumeFromEngineSessionId: options.resumeFromEngineSessionId,
25
+ mcpConfigPath,
26
+ });
27
+ const proc = spawn('claude', args, {
28
+ cwd: options.workingDir,
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ });
31
+ const parserState = createParserState();
32
+ if (!proc.stdout)
33
+ throw new Error('Claude process has no stdout');
34
+ const rl = readline.createInterface({
35
+ input: proc.stdout,
36
+ crlfDelay: Number.POSITIVE_INFINITY,
37
+ });
38
+ let discoveredSessionId;
39
+ rl.on('line', (line) => {
40
+ const { events } = parseClaudeLine(line, parserState);
41
+ for (const ev of events) {
42
+ if (ev.kind === 'session:started')
43
+ discoveredSessionId = ev.engineSessionId;
44
+ onEvent(ev);
45
+ }
46
+ });
47
+ // Line-buffer stderr so we see one event per log line instead of
48
+ // arbitrary byte chunks, and restrict quota detection to clear rate-
49
+ // limit signals (not every occurrence of the word "rate" or "quota").
50
+ // Non-quota stderr lines are logged to the console but do NOT emit
51
+ // an error event — this avoids false positives flooding the UI.
52
+ const stderrRl = proc.stderr
53
+ ? readline.createInterface({
54
+ input: proc.stderr,
55
+ crlfDelay: Number.POSITIVE_INFINITY,
56
+ })
57
+ : undefined;
58
+ // Known benign stderr lines from the Claude CLI that should NOT be
59
+ // logged — they flood the dev console and carry no actionable info.
60
+ // Strip ANSI color codes before matching.
61
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes by design
62
+ const stripAnsi = (s) => s.replace(/\u001b\[\d+m/g, '');
63
+ function isBenignStderr(line) {
64
+ const cleaned = stripAnsi(line).trim();
65
+ return /^warning: no stdin data received in \d+s/i.test(cleaned);
66
+ }
67
+ stderrRl?.on('line', (line) => {
68
+ const lower = line.toLowerCase();
69
+ const isQuota = lower.includes('rate limit exceeded') ||
70
+ lower.includes('rate_limit_exceeded') ||
71
+ (lower.includes('429') && lower.includes('rate')) ||
72
+ lower.includes('quota exceeded');
73
+ if (isQuota) {
74
+ onEvent({ kind: 'error', category: 'quota', message: line });
75
+ }
76
+ else if (line.trim().length > 0 && !isBenignStderr(line)) {
77
+ console.warn(`[claude-engine stderr] ${line}`);
78
+ }
79
+ });
80
+ // 'error' fires when spawn itself fails (e.g. ENOENT if the `claude`
81
+ // binary is missing from PATH). In that case 'exit' never fires, so we
82
+ // emit the lifecycle pair here and clean the MCP config ourselves.
83
+ proc.on('error', (err) => {
84
+ onEvent({ kind: 'error', category: 'spawn_failed', message: err.message });
85
+ onEvent({ kind: 'session:ended', reason: 'error', exitCode: null });
86
+ cleanupMcpConfig(options.workingDir);
87
+ rl.close();
88
+ stderrRl?.close();
89
+ });
90
+ proc.on('exit', (code) => {
91
+ cleanupMcpConfig(options.workingDir);
92
+ rl.close();
93
+ stderrRl?.close();
94
+ onEvent({
95
+ kind: 'session:ended',
96
+ reason: code === 0 ? 'completed' : code === null ? 'killed' : 'error',
97
+ exitCode: code,
98
+ });
99
+ });
100
+ const engineProcess = {
101
+ get pid() {
102
+ return proc.pid;
103
+ },
104
+ get engineSessionId() {
105
+ return discoveredSessionId;
106
+ },
107
+ sendMessage(text) {
108
+ if (!proc.stdin?.writable)
109
+ throw new Error('Agent stdin not writable');
110
+ proc.stdin.write(`${text}\n`);
111
+ },
112
+ interrupt() {
113
+ if (proc.pid !== undefined)
114
+ process.kill(proc.pid, 'SIGINT');
115
+ },
116
+ stop() {
117
+ return new Promise((resolve) => {
118
+ if (proc.killed || proc.exitCode !== null)
119
+ return resolve();
120
+ let resolved = false;
121
+ let killTimer;
122
+ let hardTimeout;
123
+ const doResolve = () => {
124
+ if (resolved)
125
+ return;
126
+ resolved = true;
127
+ if (killTimer)
128
+ clearTimeout(killTimer);
129
+ if (hardTimeout)
130
+ clearTimeout(hardTimeout);
131
+ resolve();
132
+ };
133
+ proc.once('exit', doResolve);
134
+ try {
135
+ proc.kill('SIGTERM');
136
+ }
137
+ catch {
138
+ // Already dead
139
+ }
140
+ killTimer = setTimeout(() => {
141
+ try {
142
+ if (!proc.killed)
143
+ proc.kill('SIGKILL');
144
+ }
145
+ catch {
146
+ // Ignore
147
+ }
148
+ }, 5000);
149
+ killTimer.unref?.();
150
+ // Hard-timeout safety net: if the process hasn't exited within 10s
151
+ // (5s after SIGKILL), resolve anyway so callers never hang forever.
152
+ hardTimeout = setTimeout(() => {
153
+ console.warn('[claude-engine] stop() hard-timeout reached, resolving anyway');
154
+ doResolve();
155
+ }, 10000);
156
+ hardTimeout.unref?.();
157
+ });
158
+ },
159
+ };
160
+ return engineProcess;
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,23 @@
1
+ import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const MCP_FILENAME = '.mcp.json';
4
+ export function writeMcpConfig(workingDir, servers) {
5
+ const filePath = join(workingDir, MCP_FILENAME);
6
+ const mcpServers = {};
7
+ for (const s of servers) {
8
+ mcpServers[s.name] = { command: s.command, args: s.args, env: s.env };
9
+ }
10
+ writeFileSync(filePath, JSON.stringify({ mcpServers }, null, 2));
11
+ return filePath;
12
+ }
13
+ export function cleanupMcpConfig(workingDir) {
14
+ const filePath = join(workingDir, MCP_FILENAME);
15
+ if (existsSync(filePath)) {
16
+ try {
17
+ unlinkSync(filePath);
18
+ }
19
+ catch {
20
+ // Best-effort cleanup
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,224 @@
1
+ function normalizeResetsAt(raw) {
2
+ if (typeof raw === 'string' && raw.length > 0)
3
+ return raw;
4
+ if (typeof raw === 'number' && Number.isFinite(raw))
5
+ return new Date(raw * 1000).toISOString();
6
+ return undefined;
7
+ }
8
+ function extractUsedPct(source) {
9
+ const raw = (source.utilization ?? source.used_percent ?? source.percent_used ?? source.usedPct);
10
+ if (typeof raw === 'number' && Number.isFinite(raw))
11
+ return raw <= 1 ? raw * 100 : raw;
12
+ const used = source.used ?? source.current ?? source.spent;
13
+ const limit = source.limit ?? source.max ?? source.allowed;
14
+ if (typeof used === 'number' && typeof limit === 'number' && limit > 0)
15
+ return (used / limit) * 100;
16
+ return null;
17
+ }
18
+ function makeBucket(id, source) {
19
+ const usedPct = extractUsedPct(source) ?? source.__fallbackPct ?? null;
20
+ if (usedPct === null)
21
+ return null;
22
+ const resetsAt = normalizeResetsAt(source.resets_at ?? source.reset_at ?? source.resetsAt ?? source.resetAt);
23
+ const label = (typeof source.label === 'string' && source.label) || undefined;
24
+ const used = source.used ?? source.current ?? source.spent;
25
+ const limit = source.limit ?? source.max ?? source.allowed;
26
+ const details = used !== undefined && limit !== undefined ? `${String(used)} / ${String(limit)}` : undefined;
27
+ return { id, label, usedPct: Math.max(0, Math.min(100, usedPct)), resetsAt, details };
28
+ }
29
+ function normalizeRateLimitInfo(info) {
30
+ const buckets = [];
31
+ if (typeof info.rateLimitType === 'string') {
32
+ const b = makeBucket(info.rateLimitType, { ...info, __fallbackPct: 0 });
33
+ if (b)
34
+ buckets.push(b);
35
+ }
36
+ if (Array.isArray(info.buckets)) {
37
+ for (const entry of info.buckets) {
38
+ if (!entry || typeof entry !== 'object')
39
+ continue;
40
+ const obj = entry;
41
+ const id = (typeof obj.id === 'string' && obj.id) ||
42
+ (typeof obj.name === 'string' && obj.name) ||
43
+ (typeof obj.rateLimitType === 'string' && obj.rateLimitType) ||
44
+ 'unknown';
45
+ const b = makeBucket(id, obj);
46
+ if (b)
47
+ buckets.push(b);
48
+ }
49
+ }
50
+ return { buckets };
51
+ }
52
+ export function createParserState() {
53
+ return { sessionStartedEmitted: false, openMessages: new Map() };
54
+ }
55
+ export function parseClaudeLine(line, state) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed)
58
+ return { events: [], state };
59
+ // The marker can appear as a raw stdout line OR inside an assistant text block.
60
+ // We detect it in the raw line first so even unparseable lines that contain it
61
+ // still emit the signal. The assistant-branch handling below catches the
62
+ // structured case.
63
+ const markerDetected = trimmed.includes('[BRAINSTORM_COMPLETE]');
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(trimmed);
67
+ }
68
+ catch {
69
+ const events = [{ kind: 'message:raw', content: line }];
70
+ if (markerDetected)
71
+ events.push({ kind: 'session:brainstorm-complete' });
72
+ return { events, state };
73
+ }
74
+ const events = [];
75
+ const type = parsed.type;
76
+ const subtype = parsed.subtype;
77
+ const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id : undefined;
78
+ if (type === 'system') {
79
+ if (subtype === 'compact' || subtype === 'compact_boundary') {
80
+ events.push({ kind: 'session:compacted' });
81
+ return { events, state };
82
+ }
83
+ if (subtype === 'rate_limit_event') {
84
+ const info = parsed.rate_limit_info;
85
+ if (info && typeof info === 'object') {
86
+ events.push({ kind: 'rate_limit', info: normalizeRateLimitInfo(info) });
87
+ }
88
+ return { events, state };
89
+ }
90
+ if (subtype === 'task_started' || subtype === 'task_progress' || subtype === 'task_notification') {
91
+ const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
92
+ if (toolCallId) {
93
+ const usage = parsed.usage;
94
+ const taskStatus = typeof parsed.status === 'string' ? parsed.status : undefined;
95
+ const isDone = subtype === 'task_notification' &&
96
+ taskStatus !== undefined &&
97
+ ['completed', 'stopped', 'failed', 'cancelled'].includes(taskStatus);
98
+ events.push({
99
+ kind: 'subagent:progress',
100
+ toolCallId,
101
+ status: isDone ? 'done' : 'running',
102
+ description: typeof parsed.description === 'string' ? parsed.description : undefined,
103
+ taskType: typeof parsed.task_type === 'string' ? parsed.task_type : undefined,
104
+ lastToolName: typeof parsed.last_tool_name === 'string' ? parsed.last_tool_name : undefined,
105
+ totalTokens: typeof usage?.total_tokens === 'number' ? usage.total_tokens : undefined,
106
+ toolUses: typeof usage?.tool_uses === 'number' ? usage.tool_uses : undefined,
107
+ durationMs: typeof usage?.duration_ms === 'number' ? usage.duration_ms : undefined,
108
+ });
109
+ }
110
+ return { events, state };
111
+ }
112
+ }
113
+ if (type === 'system' && subtype === 'init') {
114
+ if (sessionId && (!state.sessionStartedEmitted || state.sessionId !== sessionId)) {
115
+ events.push({
116
+ kind: 'session:started',
117
+ engineSessionId: sessionId,
118
+ model: typeof parsed.model === 'string' ? parsed.model : undefined,
119
+ });
120
+ state.sessionStartedEmitted = true;
121
+ state.sessionId = sessionId;
122
+ }
123
+ if (Array.isArray(parsed.slash_commands) && parsed.slash_commands.length > 0) {
124
+ events.push({ kind: 'skills:discovered', skills: parsed.slash_commands });
125
+ }
126
+ return { events, state };
127
+ }
128
+ if (type === 'assistant') {
129
+ const message = parsed.message;
130
+ const messageId = typeof message?.id === 'string' ? message.id : 'unknown';
131
+ const content = Array.isArray(message?.content) ? message?.content : [];
132
+ // A new messageId arriving means any previously-open message is done.
133
+ // Claude CLI's stream-json output doesn't always carry an explicit
134
+ // `stop_reason` or `message_stop` on the last chunk; some runs finish
135
+ // implicitly when the next turn begins. Close stale openMessages here
136
+ // so the UI's streaming spinner doesn't hang forever.
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
+ for (const block of content) {
148
+ const blockType = block.type;
149
+ if (blockType === 'text' && typeof block.text === 'string') {
150
+ events.push({ kind: 'message:text', messageId, text: block.text, streaming: true });
151
+ msgState.sawText = true;
152
+ }
153
+ if (blockType === 'tool_use') {
154
+ events.push({
155
+ kind: 'tool:call',
156
+ messageId,
157
+ toolCallId: typeof block.id === 'string' ? block.id : 'unknown',
158
+ name: typeof block.name === 'string' ? block.name : 'unknown',
159
+ input: block.input ?? {},
160
+ });
161
+ }
162
+ if (blockType === 'thinking') {
163
+ events.push({
164
+ kind: 'message:thinking',
165
+ messageId,
166
+ text: typeof block.thinking === 'string' ? block.thinking : '',
167
+ });
168
+ }
169
+ if (blockType === 'text' &&
170
+ typeof block.text === 'string' &&
171
+ block.text.includes('[BRAINSTORM_COMPLETE]')) {
172
+ events.push({ kind: 'session:brainstorm-complete' });
173
+ }
174
+ }
175
+ // Claude CLI sends many intermediate deltas for the same message; most of
176
+ // them carry `stop_reason: null`. Only a truly terminal event has either
177
+ // `message_stop: true` at the root, or a non-null `stop_reason`. Checking
178
+ // `!== undefined` would spuriously emit message:end on every delta.
179
+ const stopReason = message?.stop_reason;
180
+ const isStop = parsed.message_stop === true || (stopReason !== undefined && stopReason !== null);
181
+ if (isStop) {
182
+ events.push({ kind: 'message:end', messageId });
183
+ state.openMessages.delete(messageId);
184
+ }
185
+ return { events, state };
186
+ }
187
+ if (type === 'user') {
188
+ const message = parsed.message;
189
+ const content = Array.isArray(message?.content) ? message?.content : [];
190
+ for (const block of content) {
191
+ if (block.type === 'tool_result') {
192
+ events.push({
193
+ kind: 'tool:result',
194
+ toolCallId: typeof block.tool_use_id === 'string' ? block.tool_use_id : 'unknown',
195
+ output: block.content ?? null,
196
+ isError: block.is_error === true,
197
+ });
198
+ }
199
+ }
200
+ return { events, state };
201
+ }
202
+ if (type === 'result') {
203
+ // Terminal event — close any message still considered "streaming".
204
+ for (const openId of Array.from(state.openMessages.keys())) {
205
+ events.push({ kind: 'message:end', messageId: openId });
206
+ state.openMessages.delete(openId);
207
+ }
208
+ const usage = parsed.usage;
209
+ if (usage) {
210
+ events.push({
211
+ kind: 'usage',
212
+ inputTokens: Number(usage.input_tokens ?? 0),
213
+ outputTokens: Number(usage.output_tokens ?? 0),
214
+ cacheRead: typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : undefined,
215
+ cacheWrite: typeof usage.cache_creation_input_tokens === 'number'
216
+ ? usage.cache_creation_input_tokens
217
+ : undefined,
218
+ costUsd: typeof parsed.cost_usd === 'number' ? parsed.cost_usd : undefined,
219
+ });
220
+ }
221
+ return { events, state };
222
+ }
223
+ return { events, state };
224
+ }
@@ -0,0 +1,21 @@
1
+ import { createClaudeCodeEngine } from './claude-code/engine.js';
2
+ const ENGINES = {
3
+ 'claude-code': createClaudeCodeEngine(),
4
+ };
5
+ export function listEngines() {
6
+ return Object.values(ENGINES);
7
+ }
8
+ export function resolveEngine(id) {
9
+ const engine = ENGINES[id];
10
+ if (!engine)
11
+ throw new Error(`Unknown agent engine '${id}'`);
12
+ return engine;
13
+ }
14
+ /**
15
+ * Test-only seam. Replaces or adds an engine at runtime. Do not use in
16
+ * production — the static `ENGINES` map is the source of truth; this helper
17
+ * exists only so unit tests can inject fakes without wiring a DI container.
18
+ */
19
+ export function _registerEngineForTest(engine) {
20
+ ENGINES[engine.id] = engine;
21
+ }
@@ -0,0 +1,18 @@
1
+ /** Every AgentEvent kind, as a const for exhaustive iteration in tests. */
2
+ export const ALL_AGENT_EVENT_KINDS = [
3
+ 'session:started',
4
+ 'session:ended',
5
+ 'session:compacted',
6
+ 'session:brainstorm-complete',
7
+ 'message:text',
8
+ 'message:thinking',
9
+ 'message:end',
10
+ 'message:raw',
11
+ 'tool:call',
12
+ 'tool:result',
13
+ 'subagent:progress',
14
+ 'skills:discovered',
15
+ 'usage',
16
+ 'rate_limit',
17
+ 'error',
18
+ ];
@@ -0,0 +1,4 @@
1
+ import { emit } from '../websocket-service.js';
2
+ export function routeEvent(workspaceId, agentSessionId, event) {
3
+ emit(workspaceId, 'agent:event', event, agentSessionId);
4
+ }