@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.
- package/AGENTS.md +6 -1
- package/README.md +43 -29
- package/dist/server/db/index.js +17 -0
- package/dist/server/db/migrations.js +10 -0
- package/dist/server/db/schema.js +2 -1
- package/dist/server/index.js +26 -5
- package/dist/server/middleware/migration-guard.js +15 -0
- package/dist/server/routes/dev-server.js +3 -2
- package/dist/server/routes/documents.js +113 -0
- package/dist/server/routes/engines.js +9 -0
- package/dist/server/routes/migration.js +5 -0
- package/dist/server/routes/workspaces.js +35 -11
- package/dist/server/services/agent/engines/claude-code/args-builder.js +22 -0
- package/dist/server/services/agent/engines/claude-code/capabilities.js +17 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +163 -0
- package/dist/server/services/agent/engines/claude-code/mcp-config.js +23 -0
- package/dist/server/services/agent/engines/claude-code/stream-parser.js +224 -0
- package/dist/server/services/agent/engines/registry.js +21 -0
- package/dist/server/services/agent/engines/types.js +18 -0
- package/dist/server/services/agent/event-router.js +4 -0
- package/dist/server/services/agent/orchestrator.js +582 -0
- package/dist/server/services/agent/session-controller.js +79 -0
- package/dist/server/services/content-migration-service.js +155 -0
- package/dist/server/services/db-backup-service.js +15 -0
- package/dist/server/services/websocket-service.js +81 -50
- package/dist/server/services/workspace-service.js +11 -5
- package/dist/server/utils/paths.js +1 -1
- package/dist/shared/models.js +50 -0
- package/package.json +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-CFuT6H5u.js +7 -0
- package/src/client/dist/spa/assets/ActivityFeed-DXYafbn4.css +1 -0
- package/src/client/dist/spa/assets/ClosePopup-DhM1C4Zw.js +1 -0
- package/src/client/dist/spa/assets/CreatePage-sGrkfyOm.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-yu2IH7GW.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-BVU58ujc.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-BwSRtVRI.js +2 -0
- package/src/client/dist/spa/assets/HealthPage-BO-bMpEu.js +1 -0
- package/src/client/dist/spa/assets/MainLayout-BiBJtDTk.css +1 -0
- package/src/client/dist/spa/assets/{MainLayout-_oPM07ln.js → MainLayout-BpqOChIX.js} +17 -17
- package/src/client/dist/spa/assets/QBadge-DqtcDv8D.js +1 -0
- package/src/client/dist/spa/assets/QBtn-CyzfM9-_.js +1 -0
- package/src/client/dist/spa/assets/QChip-KJoHYE6F.js +1 -0
- package/src/client/dist/spa/assets/QDialog-DQeAxY3-.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-DCRks-Ra.js +1 -0
- package/src/client/dist/spa/assets/{QSpinner-CliSLjf8.js → QIcon-B0-pH3Qs.js} +1 -1
- package/src/client/dist/spa/assets/QItemLabel-Codqjisk.js +1 -0
- package/src/client/dist/spa/assets/QItemSection-CGpX7GcL.js +1 -0
- package/src/client/dist/spa/assets/QList-B-MkPF7n.js +1 -0
- package/src/client/dist/spa/assets/QPage-yqdKDG7-.js +1 -0
- package/src/client/dist/spa/assets/QScrollArea-e5qTqwcb.js +1 -0
- package/src/client/dist/spa/assets/QSeparator-rkjCbX2M.js +1 -0
- package/src/client/dist/spa/assets/QSlideTransition-BQxI8l5r.js +1 -0
- package/src/client/dist/spa/assets/QSpace-BNr0AftG.js +1 -0
- package/src/client/dist/spa/assets/QSpinnerDots-DEiRooBD.js +1 -0
- package/src/client/dist/spa/assets/QTabPanels--6cYe2US.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-C4CPesBX.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-BrUbbtgI.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-B3elO1PX.js +1 -0
- package/src/client/dist/spa/assets/TouchPan-BT6phK1f.js +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-BHl17_tY.js +4 -0
- package/src/client/dist/spa/assets/WorkspacePage-DPGiH02q.css +1 -0
- package/src/client/dist/spa/assets/build-path-tree-Bgl2q74t.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-DMX8jq8u.js → cssMode-B1wQ-79R.js} +1 -1
- package/src/client/dist/spa/assets/documents-CHc8t22V.js +60 -0
- package/src/client/dist/spa/assets/{editor.api-DirOkGGg.js → editor.api-CRb_5Zw6.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-DC4ezIu0.js → editor.main-C5sdCvGW.js} +3 -3
- package/src/client/dist/spa/assets/format-Bttc9ToS.js +1 -0
- package/src/client/dist/spa/assets/{formatters-BzaS4w0I.js → formatters-BDadphwz.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-DI9xJfj0.js → freemarker2-CVSnsZk-.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-B9F-pScn.js → handlebars-uL_pucGI.js} +1 -1
- package/src/client/dist/spa/assets/{html-DTe2v8Q8.js → html-CatZVwWp.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-F_XLjWfJ.js → htmlMode-DTDzEngo.js} +1 -1
- package/src/client/dist/spa/assets/i18n-D-VdPLEh.js +1 -0
- package/src/client/dist/spa/assets/index-CUI-zN26.js +2 -0
- package/src/client/dist/spa/assets/{javascript-B9xJRPC6.js → javascript-DeHBpolA.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-DTZ6j6UO.js → jsonMode-Bma_YGGm.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-BjU5MtW6.js → liquid-CW7xQEG_.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BMUpG7Be.js → mdx-BsYUhMzF.js} +1 -1
- package/src/client/dist/spa/assets/models-BbSRHL9b.js +1 -0
- package/src/client/dist/spa/assets/{monaco.contribution-D7JUf8DP.js → monaco.contribution-Du0atePv.js} +2 -2
- package/src/client/dist/spa/assets/private.use-form-D1RuEt2P.js +1 -0
- package/src/client/dist/spa/assets/{python-Dz0D4uSk.js → python-D7DQWXZm.js} +1 -1
- package/src/client/dist/spa/assets/{razor-D7CFxuwR.js → razor-B2ZxF301.js} +1 -1
- package/src/client/dist/spa/assets/scroll-JVVkg2Ng.js +1 -0
- package/src/client/dist/spa/assets/touch-CBLrR6_z.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-DjscaxpS.js → tsMode-B4Xul5xA.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-DozCWZl2.js → typescript-CdsKQuLT.js} +1 -1
- package/src/client/dist/spa/assets/use-checkbox-DYiZQsbF.js +1 -0
- package/src/client/dist/spa/assets/use-id-CeduaJbU.js +1 -0
- package/src/client/dist/spa/assets/use-portal-DBe4lcC2.js +1 -0
- package/src/client/dist/spa/assets/use-quasar-Ch82z8H5.js +1 -0
- package/src/client/dist/spa/assets/{xml-DFOJMT39.js → xml-Wap00dMv.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-yEefnsXm.js → yaml-BxRDHC24.js} +1 -1
- package/src/client/dist/spa/index.html +12 -14
- package/src/mcp-server/README.md +1 -1
- package/dist/server/routes/plans.js +0 -89
- package/dist/server/services/agent-manager.js +0 -621
- package/src/client/dist/spa/assets/ActivityFeed-0GR1zPoc.js +0 -10
- package/src/client/dist/spa/assets/ActivityFeed-CfsKExt9.css +0 -1
- package/src/client/dist/spa/assets/ClosePopup-CdSn7HO8.js +0 -1
- package/src/client/dist/spa/assets/CreatePage-dMi4xVYN.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-je_7dC5I.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-DREYX-8k.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-DiHFLSk4.css +0 -1
- package/src/client/dist/spa/assets/HealthPage-Do8QZdxw.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-B5poKEy_.css +0 -1
- package/src/client/dist/spa/assets/QBadge-Bvh-hQ8K.js +0 -1
- package/src/client/dist/spa/assets/QBtn-BsD8vrWq.js +0 -1
- package/src/client/dist/spa/assets/QDialog-CkbLS1If.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-UgkE560c.js +0 -1
- package/src/client/dist/spa/assets/QList-D80ms7bw.js +0 -1
- package/src/client/dist/spa/assets/QMenu-DU-wiY_A.js +0 -1
- package/src/client/dist/spa/assets/QPage-BKY2-sf-.js +0 -1
- package/src/client/dist/spa/assets/QSpace-C5Ebr0vq.js +0 -1
- package/src/client/dist/spa/assets/QSpinnerDots-Dp12eHrB.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels-C7lWp1yU.js +0 -1
- package/src/client/dist/spa/assets/QToggle-B0HvuNEg.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-kLXuUa_m.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-CCfyqBKh.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CmyIsV-S.js +0 -1
- package/src/client/dist/spa/assets/TouchPan-CVMnGs0y.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-CWRMLYs-.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-Cl7YrG51.js +0 -4
- package/src/client/dist/spa/assets/focus-manager-DYbz9jFW.js +0 -1
- package/src/client/dist/spa/assets/format-Cyg8IgRi.js +0 -1
- package/src/client/dist/spa/assets/i18n-B13zBh1H.js +0 -1
- package/src/client/dist/spa/assets/i18n-CCWLBc0p.js +0 -1
- package/src/client/dist/spa/assets/index-DoNZ_5QK.js +0 -5
- package/src/client/dist/spa/assets/marked.esm-DCmk6NO8.js +0 -60
- package/src/client/dist/spa/assets/models-B8fzv7K4.js +0 -1
- package/src/client/dist/spa/assets/pinia-C3JsrLkB.js +0 -1
- package/src/client/dist/spa/assets/private.use-form-BhKyDtO7.js +0 -1
- package/src/client/dist/spa/assets/scroll-CLibRGI-.js +0 -1
- package/src/client/dist/spa/assets/settings-B69lIVX0.js +0 -1
- package/src/client/dist/spa/assets/touch-ChrvzrnI.js +0 -1
- package/src/client/dist/spa/assets/use-dark-DnuCB6tC.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-DBoizHBW.js +0 -1
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-Cxt1D8wE.js → _plugin-vue_export-helper-r4mAJOHR.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-CFuyUYKP.js → abap-Bgec7Keq.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-Ctq_xcrv.js → apex-VBlPwEoQ.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-BBQSVn-C.js → azcli-DKqrEFBx.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-DbnqAfvr.js → bat-DdgQWy_0.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-BtDlIXop.js → bicep-CRMM43EB.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-BLeJgKTj.js → cameligo-UatALtML.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-aZUQIUKP.js → clojure-D8JU08RA.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-Secadq9U.js → coffee-C56wu358.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-JicRPTRv.js → cpp-CyZLvhJG.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-C7NSOZyj.js → csharp-BJl3ixva.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CIje7830.js → csp-CxEKxmO-.js} +0 -0
- /package/src/client/dist/spa/assets/{css-G0bm1q_M.js → css-B0t_muXd.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-CldD5D0u.js → cypher-D1hqiMFD.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-DIK3l8YT.js → dart-Bz550Pyv.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-czxaGh2L.js → dockerfile-CIXgVAuA.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-BqdYhwmw.js → ecl-D9qbvZoA.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-m52LePTW.js → elixir-b2M38fAy.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-B5QJ9GvZ.js → flow9-Dq1UYMkt.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-B15czHsH.js → fsharp-CFNadkg7.js} +0 -0
- /package/src/client/dist/spa/assets/{go-BkoQxDo1.js → go-dSur1iB2.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-BnI6uRa_.js → graphql-qyhAo11d.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-CAwwENT7.js → hcl-DFzjMyzm.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-BHM5zh1H.js → ini-TdzA8TIl.js} +0 -0
- /package/src/client/dist/spa/assets/{java-B5i95QvQ.js → java-CSGA9pkE.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-DPDm885q.js → julia-9izz5OsY.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-qoccd5BP.js → kotlin-DuPK7AtF.js} +0 -0
- /package/src/client/dist/spa/assets/{less-B6RU166D.js → less-B8d93iCg.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-YfUeoL1V.js → lexon-DWtEIyu7.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-BIUI5y9b.js → lua-Ciq0OGgt.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-D5SAbSdU.js → m3-Cki6JWj_.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-CVJLwHzJ.js → markdown-Cu47xwU0.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-R-FZ3zOR.js → mips-BM8ui995.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-Blveyl9r.js → msdax-DqLio0_c.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-D4mY1AFx.js → mysql-v1wbjJOq.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-BmXrLr4h.js → objective-c-CQl3PGSB.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-yxckoyvV.js → pascal-D4iW0ZtD.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-Q5JCwXMI.js → pascaligo-BdC9CZdj.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-BF1Rrs5h.js → perl-BL10m4XD.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-CnYB97wm.js → pgsql-Be_oqVo3.js} +0 -0
- /package/src/client/dist/spa/assets/{php-CdDfQfSg.js → php-BtvXSFRI.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-whj-d71F.js → pla-B2vUy15C.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-ClfLr4I-.js → postiats-CbmTTfXr.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-iRaBhuuk.js → powerquery-DszLhJGx.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-DjiEt5xK.js → powershell-B0dYktF6.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-B6dcIEUr.js → protobuf-CZvaj1VX.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-DtmHnjM9.js → pug-CPDx1B3S.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-CELCyd79.js → qsharp-CDP9TFLl.js} +0 -0
- /package/src/client/dist/spa/assets/{r-ZpJXWV-o.js → r-8DbbFX2l.js} +0 -0
- /package/src/client/dist/spa/assets/{rate-limit-labels-dCPVjS61.js → rate-limit-labels-BoDORKFj.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-BiHSNkAl.js → redis-DRWj9MtJ.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-DzuwYCHP.js → redshift-C6cElE_5.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-YOT94bbS.js → restructuredtext-W9pS9n3m.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-BfiHr6Uu.js → ruby-BKnzWnk-.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-JZ-uOoYM.js → rust-YPCclWwe.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-CBglP1-t.js → sb-BgM4DTFb.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-C9l41paw.js → scala-fz1OPLMl.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-B-InQ6hy.js → scheme-8Uz1RIbu.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-v6OmJRN9.js → scss-Djo3IYXr.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-Dyp6iwB6.js → shell-CINF5Tx_.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-D5epNWue.js → solidity-GgiNEuUm.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-Eva-79sB.js → sophia-Culj97P9.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-gvALLO1w.js → sparql-C2ZlpxOY.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-COdamZYI.js → sql-BEf5Pg7Y.js} +0 -0
- /package/src/client/dist/spa/assets/{st-eMoImIwE.js → st-CT6UUoeH.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-7R_T9RYH.js → swift-B5g0xTG3.js} +0 -0
- /package/src/client/dist/spa/assets/{symbols-CAg-nBkV.js → symbols-DCYodwb2.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-1pCEfaHU.js → systemverilog-CEgQz9DR.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-B_KgnhfE.js → tcl-D0qL2L0I.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-CFZUJxb9.js → twig-BFUAVf1E.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-B1ZgHlud.js → typespec-CjVVcNKm.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-DKdun5tL.js → vb-CZJr-DQz.js} +0 -0
- /package/src/client/dist/spa/assets/{vue-i18n-eUDnMrPl.js → vue-i18n-CeG0hR0Z.js} +0 -0
- /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
|
|
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
|
-
> [!
|
|
6
|
-
> 🚧 **
|
|
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
|
-
- **
|
|
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`,
|
|
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
|
-
- **
|
|
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 #
|
|
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/
|
|
211
|
-
│ ├── index.ts
|
|
212
|
-
│ ├── db/
|
|
213
|
-
│ ├── services/
|
|
214
|
-
│ ├──
|
|
215
|
-
│
|
|
216
|
-
├──
|
|
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/
|
|
219
|
-
│ ├── components/
|
|
220
|
-
│ ├──
|
|
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/
|
|
223
|
-
│ ├── kobo-tasks-server.ts
|
|
224
|
-
│ └── kobo-tasks-handlers.ts
|
|
225
|
-
└── __tests__/
|
|
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,
|
|
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` |
|
|
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
|
|
package/dist/server/db/index.js
CHANGED
|
@@ -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;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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',
|
|
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
|
|
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
|
|
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
|
+
}
|