@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
package/AGENTS.md CHANGED
@@ -58,7 +58,12 @@ src/
58
58
  │ │ └── migrations.ts # incremental migrations, bumped per feature
59
59
  │ ├── services/ # business logic — pure functions over db + external processes
60
60
  │ │ ├── workspace-service.ts # workspaces + tasks + agent_sessions CRUD
61
- │ │ ├── agent-manager.ts # spawns Claude Code CLI, streams stdout, tracks sessions, interrupt/stop
61
+ │ │ ├── agent/ # agent engine abstraction (replaces former agent-manager.ts)
62
+ │ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota handling, watchdog, public API
63
+ │ │ │ ├── session-controller.ts # lifecycle wrapper around an AgentEngine instance
64
+ │ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
65
+ │ │ │ └── engines/claude-code/ # Claude Code engine (spawn + stream-parser + args-builder + mcp-config + capabilities)
66
+ │ │ ├── content-migration-service.ts # runtime legacy ws_events → normalised AgentEvent migration
62
67
  │ │ ├── templates-service.ts # prompt templates CRUD (JSON file persistence, seeding)
63
68
  │ │ ├── dev-server-service.ts # per-workspace dev server lifecycle (docker or npm process)
64
69
  │ │ ├── websocket-service.ts # emit / emitEphemeral to subscribed clients
package/README.md CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  > **Kōbō** (工房) — Japanese for *workshop*. A multi-workspace agent manager for [Claude Code](https://claude.com/claude-code).
4
4
 
5
- > [!WARNING]
6
- > 🚧 **Work in progress** — This project is under active development. Breaking changes may occur at any time.
7
- >
8
- > **Engine refactor planned.** Kōbō currently drives the `claude` CLI via `spawn(..., ['-p', ...])` and parses stdout, which is brittle on edge cases (interrupts, long-running sessions, MCP lifecycle, tool-use streaming). A rewrite is planned to use the [Claude Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk/overview) directly so the agent runs in-process, with proper streaming primitives, structured tool-use events, and cleaner interrupt / resume semantics. Expect churn in `src/server/services/agent-manager.ts` and the WebSocket event shape during that migration.
5
+ > [!NOTE]
6
+ > 🚧 **Active development** — breaking changes may still land on `develop`. The database layer ships with forward-only migrations and a timestamped pre-migration backup of `kobo.db` before any schema change, so upgrades preserve your data even across invasive refactors.
9
7
 
10
8
  Kōbō lets you delegate multiple coding missions to Claude Code agents in parallel. Each workspace lives in its own isolated git worktree with its own branch, its own Claude session, optionally its own dev server, and a custom MCP tools server the agent uses to track progress. A Vue 3 dashboard shows live agent output, tasks, acceptance criteria, and git state across every workspace.
11
9
 
@@ -14,25 +12,31 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
14
12
  ## Features
15
13
 
16
14
  - **Isolated git worktrees** — every workspace runs on its own branch in its own directory, so concurrent Claude sessions never step on each other
17
- - **Live agent output** — stream `stdout`/`stderr` from Claude Code to the browser via WebSocket, with persisted event replay on reconnect
15
+ - **Pluggable agent engine** — Kōbō talks to agents through an `AgentEngine` contract with a normalised `AgentEvent` stream (`src/server/services/agent/engines/`). Claude Code is the first engine; dropping in another runtime (e.g. the Claude Agent SDK) only requires a new adapter, not a rewrite of the UI or orchestration layer
16
+ - **Rich chat feed** — live streaming text, thinking blocks, inline tool calls with expandable diffs for Edit/Write, per-turn session cards, markdown rendering, jump-to-previous-user-message button, and infinite scroll-up over persisted history
18
17
  - **Task & acceptance criteria tracking** — the agent reports progress through a dedicated MCP server (`kobo-tasks`) that reads and updates tasks directly from the SQLite database
18
+ - **Documents panel** — tree view in the right drawer that surfaces every AI-generated markdown file under `docs/plans/`, `docs/superpowers/`, and `.ai/thoughts/`. Paths mentioned in chat messages are auto-detected against the catalogue and become one-click deep-links into the panel
19
+ - **Git panel with inline diff viewer** — Monaco-powered side-by-side / inline diff of the working branch against its source, with file tree (same q-tree as Documents), inline rebase/merge conflict resolution, and a clean action bar: `Sync` split-button (pull / rebase / merge), `Push`, `Diff`, `Create PR`
19
20
  - **Notion integration** — pull workspace missions straight from Notion pages, extract markdown, and use it as the source of truth for acceptance criteria
20
21
  - **Sentry integration** — paste a Sentry issue URL to spin up a dedicated "fix workspace" with the stacktrace, tags, and offending spans written to `.ai/thoughts/SENTRY-<id>.md`; the agent is primed with a TDD fix workflow and has access to the Sentry MCP tools for deeper digging
21
22
  - **Per-workspace dev servers** — start/stop Docker or Node dev servers scoped to each branch, with log streaming
22
23
  - **Conventional-commit enforcement** — project-level git conventions are written to `.ai/.git-conventions.md` inside every workspace so Claude follows them during commits
23
- - **Pull request automation** — one-click `push`, `pull`, and `open-pr` endpoints integrate with the GitHub CLI, using a configurable prompt template
24
+ - **Pull request automation** — one-click `push`, `pull`, `open-pr`, and "change PR base" endpoints integrate with the GitHub CLI, using a configurable prompt template
24
25
  - **Multi-session support** — create multiple Claude agent sessions per workspace, each with its own chat history; resume completed sessions via `--resume`; sessions are named and persisted in localStorage
25
26
  - **Prompt templates** — personal library of reusable prompts with variable substitution (`{working_branch}`, `{commit_count}`, etc.), insertable from the chat input via `/` autocomplete; editable in Settings > Templates
26
- - **Plan browser** — read-only viewer for markdown plan files produced by agents, rendered directly in the right-side panel
27
+ - **Favorites and tags** — pin workspaces to the top via right-click favourite, organise with per-workspace tags filterable from the sidebar; a global tag catalogue keeps colours consistent across workspaces
28
+ - **Health panel + config export/import** — inspect backend health (agent sessions, migration state, dev servers, DB size) and roundtrip your Kōbō config (settings, templates, skills) between machines via JSON
29
+ - **Usage tracking** — rolling input/output token counts and cost estimates per workspace, aggregated across sessions and live-updated from `usage` events
30
+ - **Resizable right drawer** — drag-to-resize horizontally and vertically, with tab state and split ratio persisted to localStorage
27
31
  - **Soft interrupt** — pause an agent mid-execution (SIGINT, like pressing Escape in Claude Code) without killing the process; the agent stops the current tool and waits for the next message
28
32
  - **Archive instead of delete** — soft-remove workspaces without losing the worktree, branches, or history; unarchive restores the exact pre-archive state
29
33
 
30
34
  ## Tech stack
31
35
 
32
36
  - **Backend** — Node.js ≥ 20, [Hono](https://hono.dev/), [better-sqlite3](https://github.com/WiseLibs/better-sqlite3), [ws](https://github.com/websockets/ws), [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk)
33
- - **Frontend** — [Vue 3](https://vuejs.org/), [Quasar 2](https://quasar.dev/), [Pinia](https://pinia.vuejs.org/), `vue-router`
37
+ - **Frontend** — [Vue 3](https://vuejs.org/), [Quasar 2](https://quasar.dev/), [Pinia](https://pinia.vuejs.org/), `vue-router`, [Monaco Editor](https://microsoft.github.io/monaco-editor/) (git diff viewer), `marked` + `dompurify` (markdown rendering)
34
38
  - **Tooling** — TypeScript, [Vitest](https://vitest.dev/), [Biome](https://biomejs.dev/) (lint + format), `tsx` for dev
35
- - **Storage** — single SQLite file (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`) with WAL mode
39
+ - **Storage** — single SQLite file (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`) with WAL mode and forward-only migrations
36
40
 
37
41
  ## Quick start
38
42
 
@@ -49,7 +53,7 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
49
53
  ### Run via `npx` (recommended)
50
54
 
51
55
  ```bash
52
- PORT=9999 npx @loicngr/kobo@latest
56
+ SERVER_PORT=9998 PORT=9999 npx @loicngr/kobo@latest
53
57
  ```
54
58
 
55
59
  That's it. npm downloads the package, installs dependencies, starts the Kōbō server on the port you specified, and serves the web UI at `http://localhost:9999`. Data is persisted to `~/.config/kobo/` (overridable via `KOBO_HOME`).
@@ -92,7 +96,9 @@ npm start # runs the compiled server
92
96
  ### Test & lint
93
97
 
94
98
  ```bash
95
- npm test # full vitest suite (366+ tests)
99
+ npm test # backend vitest suite (740+ tests)
100
+ npm run test:client # client vitest suite (Pinia stores + pure utils, 85+ tests)
101
+ npm run test:all # backend + client suites
96
102
  npm run lint # biome check (lint + format verification)
97
103
  npm run lint:fix # biome check with safe auto-fixes
98
104
  npm run format # biome format --write
@@ -207,22 +213,31 @@ Then start a new workspace in Kōbō — the agent will pick up the skills autom
207
213
 
208
214
  ```
209
215
  src/
210
- ├── server/ # Hono backend
211
- │ ├── index.ts # app bootstrap + WS upgrade
212
- │ ├── db/ # SQLite schema and singleton
213
- │ ├── services/ # business logic (workspace, agent, dev-server, ws, notion, settings, pr-template)
214
- │ ├── routes/ # Hono handlers
215
- └── utils/ # git-ops, process-tracker
216
- ├── client/ # Vue 3 + Quasar SPA
216
+ ├── server/ # Hono backend
217
+ │ ├── index.ts # app bootstrap + WS upgrade
218
+ │ ├── db/ # SQLite schema, migrations, singleton
219
+ │ ├── services/
220
+ ├── agent/ # agent engine abstraction (replaces agent-manager.ts)
221
+ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota, watchdog, public API
222
+ │ │ │ ├── session-controller.ts # lifecycle wrapper around one AgentEngine instance
223
+ │ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
224
+ │ │ │ └── engines/claude-code/ # spawn + NDJSON stream-parser + args-builder + mcp-config + capabilities
225
+ │ │ ├── content-migration-service.ts # legacy ws_events → normalised AgentEvent rows, with DB backup
226
+ │ │ └── … # workspace, dev-server, ws, notion, sentry, settings, pr-template
227
+ │ ├── routes/ # Hono handlers (workspaces, engines, migration, templates, …)
228
+ │ └── utils/ # git-ops, process-tracker, paths
229
+ ├── shared/ # modules shared by backend and frontend (e.g. model catalogue)
230
+ ├── client/ # Vue 3 + Quasar SPA
217
231
  │ └── src/
218
- │ ├── stores/ # Pinia state management
219
- │ ├── components/ # WorkspaceList, NotionPanel, ChatInput, GitPanel, …
220
- │ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
232
+ │ ├── stores/ # Pinia: workspace, websocket, agent-stream, migration, settings, …
233
+ │ ├── components/ # ActivityFeed, TurnCard, WorkspaceList, ChatInput, GitPanel, …
234
+ │ ├── services/ # agent-event-view (foldEvents), conversation-turns (groupIntoTurns), inline-diff
235
+ │ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
221
236
  │ └── router/
222
- ├── mcp-server/ # Standalone MCP server spawned per workspace
223
- │ ├── kobo-tasks-server.ts # entry point, registers list_tasks & mark_task_done
224
- │ └── kobo-tasks-handlers.ts # pure handlers over SQLite
225
- └── __tests__/ # Vitest suite
237
+ ├── mcp-server/ # standalone MCP server spawned per workspace
238
+ │ ├── kobo-tasks-server.ts # entry point, registers list_tasks & mark_task_done
239
+ │ └── kobo-tasks-handlers.ts # pure handlers over SQLite
240
+ └── __tests__/ # Vitest suite (engines, orchestrator, migration, routes, …)
226
241
  ```
227
242
 
228
243
  See [`AGENTS.md`](./AGENTS.md) for a deeper dive into conventions, data model, WebSocket protocol, and contribution guidelines.
@@ -231,10 +246,10 @@ See [`AGENTS.md`](./AGENTS.md) for a deeper dive into conventions, data model, W
231
246
 
232
247
  | Table | Purpose |
233
248
  |---|---|
234
- | `workspaces` | the unit of work — branch, status, Notion link, model, `archived_at`, … |
249
+ | `workspaces` | the unit of work — branch, status, model, engine, `archived_at`, `favorited_at`, `tags`, Notion link, … |
235
250
  | `tasks` | workspace sub-items — tasks and acceptance criteria |
236
- | `agent_sessions` | Claude Code CLI invocations `claude_session_id`, pid, lifecycle |
237
- | `ws_events` | persisted WebSocket events for replay on reconnect |
251
+ | `agent_sessions` | agent runs pid, `engine_session_id`, lifecycle |
252
+ | `ws_events` | persisted WebSocket events (chat history, `agent:event` stream, user messages) for replay on reconnect |
238
253
 
239
254
  ## MCP server
240
255
 
@@ -261,7 +276,6 @@ This is a personal tool, but PRs and issues are welcome. Before submitting:
261
276
  1. Read [`AGENTS.md`](./AGENTS.md) — it covers the commit rules, branching model, and code conventions
262
277
  2. Run `npm run lint`, `npx tsc --noEmit`, and `npm test` locally
263
278
  3. Base your branch on `develop` (not `main`); PRs target `develop`
264
- 4. **Do not add `Co-Authored-By` trailers** in commits, even for AI-assisted work
265
279
 
266
280
  CI runs lint + type check + tests on every PR to `develop`.
267
281
 
@@ -1,9 +1,17 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
1
3
  import Database from 'better-sqlite3';
2
4
  import { ensureKoboHome, getDbPath } from '../utils/paths.js';
3
5
  let instance = null;
4
6
  /**
5
7
  * Return the singleton SQLite database connection, creating it on first call.
6
8
  * Configures WAL mode, busy timeout, and foreign keys.
9
+ *
10
+ * Safety guard: when running under vitest, refuse to open any DB located
11
+ * under the user's real home directory. If the caller didn't pass a
12
+ * custom path AND KOBO_HOME wasn't pinned to a temp dir, the vitest
13
+ * global setup is misconfigured — better to fail loudly than to silently
14
+ * mutate production data.
7
15
  */
8
16
  export function getDb(dbPath) {
9
17
  if (instance)
@@ -13,6 +21,15 @@ export function getDb(dbPath) {
13
21
  ensureKoboHome();
14
22
  resolvedPath = getDbPath();
15
23
  }
24
+ if (process.env.VITEST) {
25
+ const home = os.homedir();
26
+ const resolved = path.resolve(resolvedPath);
27
+ if (resolved.startsWith(home) && !resolved.startsWith(path.resolve(os.tmpdir()))) {
28
+ throw new Error(`[kobo-db] Refusing to open production DB under a user home directory while VITEST is active: ${resolved}. ` +
29
+ `This is a safety guard against tests leaking into the developer's ~/.config/kobo/. ` +
30
+ `Ensure vitest.setup.ts sets KOBO_HOME to a tmp directory, or pass an explicit dbPath to getDb().`);
31
+ }
32
+ }
16
33
  instance = new Database(resolvedPath);
17
34
  instance.pragma('journal_mode=WAL');
18
35
  instance.pragma('busy_timeout=5000');
@@ -78,6 +78,16 @@ export const migrations = [
78
78
  db.prepare("ALTER TABLE workspaces ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'").run();
79
79
  },
80
80
  },
81
+ {
82
+ version: 10,
83
+ name: 'agent-engine-abstraction',
84
+ migrate: (db) => {
85
+ db.transaction(() => {
86
+ db.prepare("ALTER TABLE workspaces ADD COLUMN engine TEXT NOT NULL DEFAULT 'claude-code'").run();
87
+ db.prepare('ALTER TABLE agent_sessions RENAME COLUMN claude_session_id TO engine_session_id').run();
88
+ })();
89
+ },
90
+ },
81
91
  ];
82
92
  /** Current schema version — always equals the highest migration version. */
83
93
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -18,6 +18,7 @@ export function initSchema(db) {
18
18
  archived_at TEXT,
19
19
  favorited_at TEXT,
20
20
  tags TEXT NOT NULL DEFAULT '[]',
21
+ engine TEXT NOT NULL DEFAULT 'claude-code',
21
22
  created_at TEXT NOT NULL,
22
23
  updated_at TEXT NOT NULL
23
24
  );
@@ -37,7 +38,7 @@ export function initSchema(db) {
37
38
  id TEXT PRIMARY KEY,
38
39
  workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
39
40
  pid INTEGER,
40
- claude_session_id TEXT,
41
+ engine_session_id TEXT,
41
42
  status TEXT NOT NULL DEFAULT 'running',
42
43
  started_at TEXT NOT NULL,
43
44
  ended_at TEXT,
@@ -8,17 +8,20 @@ import WebSocket, { WebSocketServer } from 'ws';
8
8
  import { closeDb, getDb } from './db/index.js';
9
9
  import { runMigrations } from './db/migrations.js';
10
10
  import devServerRouter from './routes/dev-server.js';
11
+ import documentsRouter from './routes/documents.js';
12
+ import { enginesRouter } from './routes/engines.js';
11
13
  import gitRouter from './routes/git.js';
12
14
  import healthRouter from './routes/health.js';
13
15
  import imagesRouter from './routes/images.js';
16
+ import { migrationRouter } from './routes/migration.js';
14
17
  import notionRouter from './routes/notion.js';
15
- import plansRouter from './routes/plans.js';
16
18
  import searchRouter from './routes/search.js';
17
19
  import sentryRouter from './routes/sentry.js';
18
20
  import settingsRouter from './routes/settings.js';
19
21
  import templatesRouter from './routes/templates.js';
20
22
  import workspacesRouter from './routes/workspaces.js';
21
- import { getAvailableSkills, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopWatchdog, } from './services/agent-manager.js';
23
+ import { getAvailableSkills, reconcileOrphanSessions, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopWatchdog, } from './services/agent/orchestrator.js';
24
+ import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
22
25
  import { createDailyDbBackupIfNeeded } from './services/db-backup-service.js';
23
26
  import { startDevServer, stopDevServer } from './services/dev-server-service.js';
24
27
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
@@ -52,6 +55,7 @@ void createDailyDbBackupIfNeeded(db, getDbPath()).then((r) => {
52
55
  });
53
56
  // Initialize process cleanup, agent watchdog, and PR watcher
54
57
  initProcessCleanup();
58
+ reconcileOrphanSessions();
55
59
  startWatchdog();
56
60
  startPrWatcher();
57
61
  // Create Hono app
@@ -67,9 +71,11 @@ app.route('/api/git', gitRouter);
67
71
  app.route('/api/settings', settingsRouter);
68
72
  app.route('/api/dev-server', devServerRouter);
69
73
  app.route('/api/templates', templatesRouter);
70
- app.route('/api/workspaces', plansRouter);
74
+ app.route('/api/workspaces', documentsRouter);
71
75
  app.route('/api/search', searchRouter);
72
76
  app.route('/api/health', healthRouter);
77
+ app.route('/api/engines', enginesRouter);
78
+ app.route('/api/migration', migrationRouter);
73
79
  // Skills endpoint
74
80
  app.get('/api/skills', (c) => c.json(getAvailableSkills()));
75
81
  const PORT = parseInt(process.env.SERVER_PORT || process.env.PORT || '3000', 10);
@@ -120,6 +126,13 @@ const server = serve({
120
126
  }, (info) => {
121
127
  setBackendPort(info.port);
122
128
  console.log(`Server running at http://localhost:${info.port}`);
129
+ // Content migration runs AFTER the HTTP listener is up so the frontend
130
+ // can observe progress via WS broadcasts + GET /api/migration/status.
131
+ // Not awaited — the callback returns quickly, the migration runs in the
132
+ // background.
133
+ void runContentMigrationIfNeeded(getDb(), getDbPath()).catch((err) => {
134
+ console.error('[boot] content migration failed:', err);
135
+ });
123
136
  });
124
137
  // Create WebSocketServer attached to the HTTP server
125
138
  const wss = new WebSocketServer({ noServer: true });
@@ -212,7 +225,7 @@ terminalWss.on('connection', (ws, workspaceId) => {
212
225
  }
213
226
  });
214
227
  });
215
- // Wire websocket-service message handler to agent-manager
228
+ // Wire websocket-service message handler to the agent orchestrator
216
229
  setMessageHandler((type, payload) => {
217
230
  const p = payload;
218
231
  if (type === 'chat:message' && p?.workspaceId && p?.content) {
@@ -226,7 +239,15 @@ setMessageHandler((type, payload) => {
226
239
  try {
227
240
  sendMessage(p.workspaceId, p.content);
228
241
  }
229
- catch {
242
+ catch (err) {
243
+ const msg = err instanceof Error ? err.message : String(err);
244
+ // Only resume on the specific "No agent running" path. Other errors
245
+ // (stdin closed, process dead mid-write, etc.) should surface to the
246
+ // logs instead of silently respawning a fresh agent.
247
+ if (!msg.includes('No agent running')) {
248
+ console.error(`[ws] chat:message failed for workspace ${p.workspaceId}:`, err);
249
+ return;
250
+ }
230
251
  // Agent not running — resume the session hinted by the client if any,
231
252
  // otherwise the most-recent active session.
232
253
  try {
@@ -0,0 +1,15 @@
1
+ import { getContentMigrationStatus } from '../services/content-migration-service.js';
2
+ /**
3
+ * Blocks mutating requests while the content migration is running.
4
+ *
5
+ * Returns 503 with `{ error: 'migration-in-progress' }` whenever the migration
6
+ * state is anything other than `idle` or `done`. Mounted per-handler on routes
7
+ * that write to `ws_events` or spawn agents so the user can still observe
8
+ * progress through GETs and `/api/migration/status` while the migration runs.
9
+ */
10
+ export const migrationGuard = async (c, next) => {
11
+ const state = getContentMigrationStatus().state;
12
+ if (state === 'idle' || state === 'done')
13
+ return next();
14
+ return c.json({ error: 'migration-in-progress' }, 503);
15
+ };
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono';
2
+ import { migrationGuard } from '../middleware/migration-guard.js';
2
3
  import { getDevServerLogs, getStatus, startDevServer, stopDevServer } from '../services/dev-server-service.js';
3
4
  import { getWorkspace } from '../services/workspace-service.js';
4
5
  /** Hono sub-router for per-workspace dev server lifecycle (start, stop, status, logs). */
@@ -24,7 +25,7 @@ app.get('/:workspaceId/status', (c) => {
24
25
  }
25
26
  });
26
27
  // POST /api/dev-server/:workspaceId/start
27
- app.post('/:workspaceId/start', (c) => {
28
+ app.post('/:workspaceId/start', migrationGuard, (c) => {
28
29
  try {
29
30
  const workspaceId = c.req.param('workspaceId');
30
31
  const workspace = getWorkspace(workspaceId);
@@ -40,7 +41,7 @@ app.post('/:workspaceId/start', (c) => {
40
41
  }
41
42
  });
42
43
  // POST /api/dev-server/:workspaceId/stop
43
- app.post('/:workspaceId/stop', (c) => {
44
+ app.post('/:workspaceId/stop', migrationGuard, (c) => {
44
45
  try {
45
46
  const workspaceId = c.req.param('workspaceId');
46
47
  const workspace = getWorkspace(workspaceId);
@@ -0,0 +1,113 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Hono } from 'hono';
4
+ import * as workspaceService from '../services/workspace-service.js';
5
+ /** Hono sub-router for workspace document browsing (read-only). */
6
+ const app = new Hono();
7
+ /**
8
+ * Directories (relative to the worktree root) where AI-generated documents
9
+ * may live. Scanned recursively — any `.md` file found below one of these
10
+ * roots is surfaced in the documents panel.
11
+ *
12
+ * Kept intentionally narrow to avoid leaking unrelated project docs
13
+ * (README, product specs, …) into the panel.
14
+ */
15
+ const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
16
+ /** Only .md files are listed. */
17
+ const MD_EXT = '.md';
18
+ /** Depth cap to keep recursion bounded even on pathological symlink loops. */
19
+ const MAX_DEPTH = 8;
20
+ function walkMarkdownFiles(rootAbs, rootRel, out, depth = 0) {
21
+ if (depth > MAX_DEPTH)
22
+ return;
23
+ let entries;
24
+ try {
25
+ entries = readdirSync(rootAbs);
26
+ }
27
+ catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.startsWith('.') && entry !== '.ai')
32
+ continue; // skip hidden except `.ai`
33
+ const absEntry = path.join(rootAbs, entry);
34
+ const relEntry = `${rootRel}/${entry}`;
35
+ let stat;
36
+ try {
37
+ stat = statSync(absEntry);
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ if (stat.isDirectory()) {
43
+ walkMarkdownFiles(absEntry, relEntry, out, depth + 1);
44
+ }
45
+ else if (stat.isFile() && entry.endsWith(MD_EXT)) {
46
+ out.push({
47
+ path: relEntry,
48
+ name: entry,
49
+ modifiedAt: stat.mtime.toISOString(),
50
+ });
51
+ }
52
+ }
53
+ }
54
+ // GET /:id/documents — list every .md file under DOCUMENT_DIRS in the workspace worktree
55
+ app.get('/:id/documents', (c) => {
56
+ try {
57
+ const id = c.req.param('id');
58
+ const workspace = workspaceService.getWorkspace(id);
59
+ if (!workspace) {
60
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
61
+ }
62
+ const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
63
+ const documents = [];
64
+ for (const dir of DOCUMENT_DIRS) {
65
+ const absDir = path.join(worktreePath, dir);
66
+ if (!existsSync(absDir))
67
+ continue;
68
+ walkMarkdownFiles(absDir, dir, documents);
69
+ }
70
+ // Sort by modifiedAt descending (most recent first)
71
+ documents.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
72
+ return c.json({ documents });
73
+ }
74
+ catch (err) {
75
+ const message = err instanceof Error ? err.message : String(err);
76
+ return c.json({ error: message }, 500);
77
+ }
78
+ });
79
+ // GET /:id/document?path=<relative> — read a single document
80
+ app.get('/:id/document', (c) => {
81
+ try {
82
+ const id = c.req.param('id');
83
+ const filePath = c.req.query('path');
84
+ if (!filePath) {
85
+ return c.json({ error: 'Missing path query parameter' }, 400);
86
+ }
87
+ const workspace = workspaceService.getWorkspace(id);
88
+ if (!workspace) {
89
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
90
+ }
91
+ // Security: normalize the path and verify it stays within allowed roots.
92
+ const normalized = path.normalize(filePath);
93
+ if (normalized.includes('..') ||
94
+ !DOCUMENT_DIRS.some((dir) => normalized.startsWith(`${dir}/`) || normalized === dir)) {
95
+ return c.json({ error: `Invalid path: must be under ${DOCUMENT_DIRS.map((d) => `${d}/`).join(', ')}` }, 400);
96
+ }
97
+ if (!normalized.endsWith(MD_EXT)) {
98
+ return c.json({ error: 'Only .md files can be read' }, 400);
99
+ }
100
+ const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
101
+ const absPath = path.join(worktreePath, normalized);
102
+ if (!existsSync(absPath)) {
103
+ return c.json({ error: `Document not found: ${normalized}` }, 404);
104
+ }
105
+ const content = readFileSync(absPath, 'utf-8');
106
+ return c.json({ content, path: normalized });
107
+ }
108
+ catch (err) {
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ return c.json({ error: message }, 500);
111
+ }
112
+ });
113
+ export default app;
@@ -0,0 +1,9 @@
1
+ import { Hono } from 'hono';
2
+ import { listEngines } from '../services/agent/engines/registry.js';
3
+ /** Hono sub-router exposing `GET /` — the list of registered agent engines with capabilities. */
4
+ export const enginesRouter = new Hono();
5
+ enginesRouter.get('/', (c) => c.json(listEngines().map((e) => ({
6
+ id: e.id,
7
+ displayName: e.displayName,
8
+ capabilities: e.capabilities,
9
+ }))));
@@ -0,0 +1,5 @@
1
+ import { Hono } from 'hono';
2
+ import { getContentMigrationStatus } from '../services/content-migration-service.js';
3
+ /** Hono sub-router exposing `GET /status` for the runtime content migration. */
4
+ export const migrationRouter = new Hono();
5
+ migrationRouter.get('/status', (c) => c.json(getContentMigrationStatus()));
@@ -5,7 +5,9 @@ import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { Hono } from 'hono';
7
7
  import { getDb } from '../db/index.js';
8
- import * as agentManager from '../services/agent-manager.js';
8
+ import { migrationGuard } from '../middleware/migration-guard.js';
9
+ import { listEngines } from '../services/agent/engines/registry.js';
10
+ import * as agentManager from '../services/agent/orchestrator.js';
9
11
  import * as devServerService from '../services/dev-server-service.js';
10
12
  import * as notionService from '../services/notion-service.js';
11
13
  import { renderPrTemplate } from '../services/pr-template-service.js';
@@ -32,12 +34,21 @@ app.get('/', (c) => {
32
34
  }
33
35
  });
34
36
  // POST /api/workspaces — create workspace
35
- app.post('/', async (c) => {
37
+ app.post('/', migrationGuard, async (c) => {
36
38
  try {
37
39
  const body = await c.req.json();
38
40
  if (!body.name || !body.projectPath || !body.sourceBranch || !body.workingBranch) {
39
41
  return c.json({ error: 'Missing required fields: name, projectPath, sourceBranch, workingBranch' }, 400);
40
42
  }
43
+ // Validate the engine id (if provided) against the registry. An unknown
44
+ // engine is rejected up-front so we don't create orphan workspaces that
45
+ // can't spawn an agent.
46
+ if (body.engine) {
47
+ const validEngineIds = listEngines().map((e) => e.id);
48
+ if (!validEngineIds.includes(body.engine)) {
49
+ return c.json({ error: `Unknown engine '${body.engine}'. Valid engines: ${validEngineIds.join(', ')}` }, 400);
50
+ }
51
+ }
41
52
  // Fetch the source branch from origin first — if this fails, block creation
42
53
  // immediately (no DB records created, user stays on the create page).
43
54
  try {
@@ -61,6 +72,7 @@ app.post('/', async (c) => {
61
72
  model: body.model,
62
73
  reasoningEffort: body.reasoningEffort,
63
74
  permissionMode: body.permissionMode || globalSettings.defaultPermissionMode || 'plan',
75
+ engine: body.engine,
64
76
  });
65
77
  let notionContent = null;
66
78
  let sentryContent = null;
@@ -446,7 +458,7 @@ app.post('/', async (c) => {
446
458
  }
447
459
  });
448
460
  // POST /api/workspaces/:id/sessions — create a new idle agent session
449
- app.post('/:id/sessions', (c) => {
461
+ app.post('/:id/sessions', migrationGuard, (c) => {
450
462
  try {
451
463
  const id = c.req.param('id');
452
464
  const workspace = workspaceService.getWorkspace(id);
@@ -803,7 +815,7 @@ app.put('/:id/tags', async (c) => {
803
815
  }
804
816
  });
805
817
  // PATCH /api/workspaces/:id — update workspace fields (status, model, permissionMode, name)
806
- app.patch('/:id', async (c) => {
818
+ app.patch('/:id', migrationGuard, async (c) => {
807
819
  try {
808
820
  const id = c.req.param('id');
809
821
  const body = await c.req.json();
@@ -930,7 +942,7 @@ app.post('/:id/run-setup-script', async (c) => {
930
942
  }
931
943
  });
932
944
  // POST /api/workspaces/:id/archive — mark workspace as archived (soft-delete)
933
- app.post('/:id/archive', (c) => {
945
+ app.post('/:id/archive', migrationGuard, (c) => {
934
946
  try {
935
947
  const id = c.req.param('id');
936
948
  const workspace = workspaceService.getWorkspace(id);
@@ -969,7 +981,7 @@ app.post('/:id/archive', (c) => {
969
981
  }
970
982
  });
971
983
  // POST /api/workspaces/:id/unarchive — restore an archived workspace
972
- app.post('/:id/unarchive', (c) => {
984
+ app.post('/:id/unarchive', migrationGuard, (c) => {
973
985
  try {
974
986
  const id = c.req.param('id');
975
987
  const workspace = workspaceService.getWorkspace(id);
@@ -989,7 +1001,7 @@ app.post('/:id/unarchive', (c) => {
989
1001
  }
990
1002
  });
991
1003
  // DELETE /api/workspaces/:id — delete workspace
992
- app.delete('/:id', async (c) => {
1004
+ app.delete('/:id', migrationGuard, async (c) => {
993
1005
  try {
994
1006
  const id = c.req.param('id');
995
1007
  const workspace = workspaceService.getWorkspace(id);
@@ -1053,13 +1065,25 @@ app.delete('/:id', async (c) => {
1053
1065
  }
1054
1066
  });
1055
1067
  // POST /api/workspaces/:id/start — start/restart agent
1056
- app.post('/:id/start', async (c) => {
1068
+ app.post('/:id/start', migrationGuard, async (c) => {
1057
1069
  try {
1058
1070
  const id = c.req.param('id');
1059
1071
  const workspace = workspaceService.getWorkspace(id);
1060
1072
  if (!workspace) {
1061
1073
  return c.json({ error: `Workspace '${id}' not found` }, 404);
1062
1074
  }
1075
+ // If the workspace declares an engine, ensure it is still registered.
1076
+ // Otherwise startAgent() would throw from deep inside resolveEngine and
1077
+ // surface as an opaque 500 — better to fail fast with a clear 400.
1078
+ const workspaceEngine = workspace.engine;
1079
+ if (workspaceEngine) {
1080
+ const validEngineIds = listEngines().map((e) => e.id);
1081
+ if (!validEngineIds.includes(workspaceEngine)) {
1082
+ return c.json({
1083
+ error: `Workspace uses engine '${workspaceEngine}' which is no longer available. Recreate or reconfigure the workspace.`,
1084
+ }, 400);
1085
+ }
1086
+ }
1063
1087
  const body = await c.req
1064
1088
  .json()
1065
1089
  .catch(() => ({ prompt: undefined, agentSessionId: undefined, resume: undefined }));
@@ -1269,7 +1293,7 @@ app.post('/:id/git/abort', (c) => {
1269
1293
  }
1270
1294
  });
1271
1295
  /** Hand off merge/rebase conflicts to the workspace agent with an intelligent-resolution prompt. */
1272
- app.post('/:id/git/resolve-with-agent', async (c) => {
1296
+ app.post('/:id/git/resolve-with-agent', migrationGuard, async (c) => {
1273
1297
  try {
1274
1298
  const id = c.req.param('id');
1275
1299
  const workspace = workspaceService.getWorkspace(id);
@@ -1489,7 +1513,7 @@ app.post('/:id/mark-read', (c) => {
1489
1513
  }
1490
1514
  });
1491
1515
  // POST /api/workspaces/:id/stop — stop agent
1492
- app.post('/:id/stop', (c) => {
1516
+ app.post('/:id/stop', migrationGuard, (c) => {
1493
1517
  try {
1494
1518
  const id = c.req.param('id');
1495
1519
  const workspace = workspaceService.getWorkspace(id);
@@ -1517,7 +1541,7 @@ app.post('/:id/stop', (c) => {
1517
1541
  }
1518
1542
  });
1519
1543
  // POST /api/workspaces/:id/interrupt — soft-interrupt agent (SIGINT, like Escape in Claude Code)
1520
- app.post('/:id/interrupt', (c) => {
1544
+ app.post('/:id/interrupt', migrationGuard, (c) => {
1521
1545
  try {
1522
1546
  const id = c.req.param('id');
1523
1547
  const workspace = workspaceService.getWorkspace(id);
@@ -0,0 +1,22 @@
1
+ export function buildClaudeArgs(input) {
2
+ const args = ['--output-format', 'stream-json', '--verbose'];
3
+ if (input.skipPermissions)
4
+ args.push('--dangerously-skip-permissions');
5
+ let prompt = input.prompt;
6
+ if (input.permissionMode === 'plan') {
7
+ prompt = `[PLAN MODE] You are in PLAN/READ-ONLY mode. You MUST NOT create, edit, write, or delete any files. Only use read-only tools (Read, Grep, Glob, LS, Bash for read-only commands). Analyze the codebase, plan your approach, and present your findings — but do NOT execute any changes.\n\n${prompt}`;
8
+ }
9
+ if (input.model && input.model !== 'auto')
10
+ args.push('--model', input.model);
11
+ if (input.effort && input.effort !== 'auto')
12
+ args.push('--effort', input.effort);
13
+ if (input.mcpConfigPath)
14
+ args.push('--mcp-config', input.mcpConfigPath);
15
+ if (input.resumeFromEngineSessionId) {
16
+ args.push('--resume', input.resumeFromEngineSessionId, '-p', prompt);
17
+ }
18
+ else {
19
+ args.push('-p', prompt);
20
+ }
21
+ return { args, effectivePrompt: prompt };
22
+ }