@loicngr/kobo 1.5.7 → 1.6.1
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 +30 -15
- 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 +24 -3
- package/dist/server/middleware/migration-guard.js +15 -0
- package/dist/server/routes/dev-server.js +3 -2
- package/dist/server/routes/engines.js +9 -0
- package/dist/server/routes/migration.js +5 -0
- package/dist/server/routes/workspaces.js +138 -10
- 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/git-ops.js +78 -9
- 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-DYtAK49y.js +7 -0
- package/src/client/dist/spa/assets/ActivityFeed-DiwnrdKX.css +1 -0
- package/src/client/dist/spa/assets/ClosePopup-DqhgFbQo.js +1 -0
- package/src/client/dist/spa/assets/CreatePage-DENfwzPL.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-yu2IH7GW.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-C6q11kmw.js +2 -0
- package/src/client/dist/spa/assets/HealthPage-Cjc79NaA.js +1 -0
- package/src/client/dist/spa/assets/{MainLayout-rVleAIBi.css → MainLayout-B5poKEy_.css} +1 -1
- package/src/client/dist/spa/assets/MainLayout-CFbMw65L.js +37 -0
- package/src/client/dist/spa/assets/QBadge-BUkmTO0P.js +1 -0
- package/src/client/dist/spa/assets/QBtn-p1aZtrJH.js +1 -0
- package/src/client/dist/spa/assets/QDialog-D42GLa1i.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-5ekmpO-2.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-Czw5g0px.js +1 -0
- package/src/client/dist/spa/assets/QItemSection-GlMrLmz3.js +1 -0
- package/src/client/dist/spa/assets/QList-DNzlynsS.js +1 -0
- package/src/client/dist/spa/assets/QMenu-Q69oVX7b.js +1 -0
- package/src/client/dist/spa/assets/QPage-B09NY4Nf.js +1 -0
- package/src/client/dist/spa/assets/QScrollArea-L6wUiA20.js +1 -0
- package/src/client/dist/spa/assets/QSeparator-rkjCbX2M.js +1 -0
- package/src/client/dist/spa/assets/QSpace-PlDK6Fg3.js +1 -0
- package/src/client/dist/spa/assets/QSpinnerDots-By20ptst.js +1 -0
- package/src/client/dist/spa/assets/QTabPanels-Crs-ujNO.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-Cg9E3Dvw.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-Bf-iZnyE.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-BdcH3BSs.js +1 -0
- package/src/client/dist/spa/assets/TouchPan-DFx22dM3.js +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-DPGiH02q.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-UUE0pPCR.js +4 -0
- package/src/client/dist/spa/assets/{cssMode-C9wGTDAD.js → cssMode-BYtqFZtm.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-K7V5sl05.js → editor.api-D6ZaO4A_.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-vZ6V2hrP.js → editor.main-Cc_RDKsq.js} +3 -3
- package/src/client/dist/spa/assets/format-uvONOeL4.js +1 -0
- package/src/client/dist/spa/assets/{formatters-BzaS4w0I.js → formatters-DiJ12fKd.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-CRk6pTND.js → freemarker2-CBm--bBd.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-Cs3bFomb.js → handlebars-whX2mkV5.js} +1 -1
- package/src/client/dist/spa/assets/{html-BT4-1gwt.js → html-D7ga_o6c.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-DZ9LYDVG.js → htmlMode-BXImjcsv.js} +1 -1
- package/src/client/dist/spa/assets/i18n-BxLBrD1J.js +1 -0
- package/src/client/dist/spa/assets/index-D997aY4Y.js +2 -0
- package/src/client/dist/spa/assets/{javascript-C0nTLIDg.js → javascript-BwmzNMn5.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-LIrD4Pxq.js → jsonMode-CN5Z5bK_.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-BbaUnvHA.js → liquid-CzMNAPor.js} +1 -1
- package/src/client/dist/spa/assets/{marked.esm-gIBce057.js → marked.esm-DW0ulF0a.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-B6iNIRi2.js → mdx-DC_P05Da.js} +1 -1
- package/src/client/dist/spa/assets/models-DMQoi09X.js +1 -0
- package/src/client/dist/spa/assets/{monaco.contribution-CZ_PxrB6.js → monaco.contribution-BsBaFOOD.js} +2 -2
- package/src/client/dist/spa/assets/private.use-form-D1RuEt2P.js +1 -0
- package/src/client/dist/spa/assets/{python-C0uk6BYc.js → python-9DTZ8C3K.js} +1 -1
- package/src/client/dist/spa/assets/{razor-Bj__h6OQ.js → razor-B1LfM20o.js} +1 -1
- package/src/client/dist/spa/assets/scroll-Dh2g7BwR.js +1 -0
- package/src/client/dist/spa/assets/touch-D_A29lik.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-ZSZcAFKU.js → tsMode-DI2bWo8r.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-Bgw42jIH.js → typescript-BZ9QJ2_N.js} +1 -1
- package/src/client/dist/spa/assets/use-id-CeduaJbU.js +1 -0
- package/src/client/dist/spa/assets/use-portal-mhLq4Rqk.js +1 -0
- package/src/client/dist/spa/assets/use-quasar-BBrzedjR.js +1 -0
- package/src/client/dist/spa/assets/{xml-B9gVeW9V.js → xml-D6qm6rp0.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-DUqEwwz-.js → yaml-D2dUr_wY.js} +1 -1
- package/src/client/dist/spa/index.html +11 -14
- package/src/mcp-server/README.md +1 -1
- package/dist/server/services/agent-manager.js +0 -621
- package/src/client/dist/spa/assets/ActivityFeed-CfsKExt9.css +0 -1
- package/src/client/dist/spa/assets/ActivityFeed-Dc1oLbwJ.js +0 -10
- package/src/client/dist/spa/assets/ClosePopup-CdSn7HO8.js +0 -1
- package/src/client/dist/spa/assets/CreatePage-BDKfkW-N.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-dMi4xVYN.css +0 -1
- package/src/client/dist/spa/assets/DiffViewer-DZ9h2M2n.js +0 -2
- package/src/client/dist/spa/assets/HealthPage-BkqexlJb.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-VxUBOt-P.js +0 -37
- 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-C735ptO9.js +0 -1
- package/src/client/dist/spa/assets/QItem-DfoP6eYj.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-DV1b1MQb.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-ZDAo7WgD.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-D89evCuo.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-Ds5Dqxas.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-C0RbMxeL.js +0 -1
- package/src/client/dist/spa/assets/i18n-CkN9X6lQ.js +0 -1
- package/src/client/dist/spa/assets/index-Br4eMfSu.js +0 -5
- package/src/client/dist/spa/assets/models-C3h6lSte.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/{_plugin-vue_export-helper-Cxt1D8wE.js → _plugin-vue_export-helper-CEhRWsKN.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-CFuyUYKP.js → abap-DiwvWnMr.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-Ctq_xcrv.js → apex-CmtZjKlf.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-BBQSVn-C.js → azcli-DL2My_i-.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-DbnqAfvr.js → bat-B-nC98wG.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-BtDlIXop.js → bicep-Ju5MwOgh.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-BLeJgKTj.js → cameligo-8Eu1TyBr.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-aZUQIUKP.js → clojure-u-RpMkH3.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-Secadq9U.js → coffee-CdA7bbTe.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-JicRPTRv.js → cpp-CzNFP8ks.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-C7NSOZyj.js → csharp-j1LThmcE.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CIje7830.js → csp-CLRC61y6.js} +0 -0
- /package/src/client/dist/spa/assets/{css-G0bm1q_M.js → css-r6rC_7P2.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-CldD5D0u.js → cypher-CW08XVUh.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-DIK3l8YT.js → dart-Cs9aL5T_.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-czxaGh2L.js → dockerfile-BWM0M184.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-BqdYhwmw.js → ecl-MJJuer5P.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-m52LePTW.js → elixir-D2AIuXqn.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-B5QJ9GvZ.js → flow9-B2H24giC.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-B15czHsH.js → fsharp-CMk2OIJN.js} +0 -0
- /package/src/client/dist/spa/assets/{go-BkoQxDo1.js → go-BrMkuJg0.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-BnI6uRa_.js → graphql-PSR1UKGv.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-CAwwENT7.js → hcl-DAQrbDOW.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-BHM5zh1H.js → ini-0TG5BxW0.js} +0 -0
- /package/src/client/dist/spa/assets/{java-B5i95QvQ.js → java-rgorz17v.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-DPDm885q.js → julia-C8VMdHm8.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-qoccd5BP.js → kotlin-CllWo3gX.js} +0 -0
- /package/src/client/dist/spa/assets/{less-B6RU166D.js → less-Cgca25AP.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-YfUeoL1V.js → lexon-D0GHdBaw.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-BIUI5y9b.js → lua-DmRsNG-P.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-D5SAbSdU.js → m3-BgL5dNKT.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-CVJLwHzJ.js → markdown-BuJfycGS.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-R-FZ3zOR.js → mips-C9m_93PR.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-Blveyl9r.js → msdax-CpFHC9OI.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-D4mY1AFx.js → mysql-qFvltsqN.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-BmXrLr4h.js → objective-c-Bnmr858J.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-yxckoyvV.js → pascal-WP0_D5AO.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-Q5JCwXMI.js → pascaligo-Blom4Rij.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-BF1Rrs5h.js → perl-B-vk8g64.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-CnYB97wm.js → pgsql-Cgvz6v67.js} +0 -0
- /package/src/client/dist/spa/assets/{php-CdDfQfSg.js → php-8a3Lrw9m.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-whj-d71F.js → pla-DuFqEZ8V.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-ClfLr4I-.js → postiats-DkLtSgkp.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-iRaBhuuk.js → powerquery-BJ1aNepW.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-DjiEt5xK.js → powershell-rE98k687.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-B6dcIEUr.js → protobuf-CUheFacr.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-DtmHnjM9.js → pug-LDcAMD8w.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-CELCyd79.js → qsharp-DUKSQoR1.js} +0 -0
- /package/src/client/dist/spa/assets/{r-ZpJXWV-o.js → r-D-QApv87.js} +0 -0
- /package/src/client/dist/spa/assets/{rate-limit-labels-dCPVjS61.js → rate-limit-labels-BvYERsho.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-BiHSNkAl.js → redis-SXdDyWR9.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-DzuwYCHP.js → redshift-Y6lsCryn.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-YOT94bbS.js → restructuredtext-edObr9a8.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-BfiHr6Uu.js → ruby-CNnUfF-8.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-JZ-uOoYM.js → rust-IHUZWzBr.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-CBglP1-t.js → sb-DrUvY44N.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-C9l41paw.js → scala-B4hbXGLM.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-B-InQ6hy.js → scheme-BGrd12j3.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-v6OmJRN9.js → scss-x5G1ES4U.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-Dyp6iwB6.js → shell-DOehe2Y8.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-D5epNWue.js → solidity-BeRvcwWV.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-Eva-79sB.js → sophia-DZbkUNjy.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-gvALLO1w.js → sparql-B7_oi5-h.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-COdamZYI.js → sql-CTlsFWVE.js} +0 -0
- /package/src/client/dist/spa/assets/{st-eMoImIwE.js → st-DJVEJdPE.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-7R_T9RYH.js → swift-CwhT3fYa.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-BQN63pkN.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-B_KgnhfE.js → tcl-DqwfpskA.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-CFZUJxb9.js → twig-BiyenUgc.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-B1ZgHlud.js → typespec-CWOJribt.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-DKdun5tL.js → vb-Cq5F87m3.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-BAvW2lVr.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
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
6
|
> 🚧 **Work in progress** — This project is under active development. Breaking changes may occur at any time.
|
|
7
|
+
> ⚠️ **Planned refactor with potential data loss** — A major refactor is planned and may require database/schema changes that can cause data loss.
|
|
8
|
+
> ❌ **Do not use in production** until this refactor is complete and a stable migration path is documented.
|
|
9
|
+
>
|
|
10
|
+
> **Engine refactor in progress.** The legacy `agent-manager.ts` has been split into an agent engine abstraction (`src/server/services/agent/`) with a pluggable `AgentEngine` contract, a shared `Orchestrator`, and a normalised `AgentEvent` stream. The Claude Code CLI is now one engine among potentially several (`src/server/services/agent/engines/claude-code/`). A runtime migration (`content-migration-service.ts`) converts legacy `ws_events` into the new normalised form on first boot after upgrade; **a timestamped copy of `kobo.db` is written alongside it before the migration runs**, so you can roll back by restoring the backup if anything goes sideways. Expect continued churn until the WebSocket event surface and UI stream reducers stabilise.
|
|
7
11
|
|
|
8
12
|
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.
|
|
9
13
|
|
|
@@ -90,7 +94,9 @@ npm start # runs the compiled server
|
|
|
90
94
|
### Test & lint
|
|
91
95
|
|
|
92
96
|
```bash
|
|
93
|
-
npm test #
|
|
97
|
+
npm test # backend vitest suite (740+ tests)
|
|
98
|
+
npm run test:client # client vitest suite (Pinia stores + pure utils, 85+ tests)
|
|
99
|
+
npm run test:all # backend + client suites
|
|
94
100
|
npm run lint # biome check (lint + format verification)
|
|
95
101
|
npm run lint:fix # biome check with safe auto-fixes
|
|
96
102
|
npm run format # biome format --write
|
|
@@ -205,22 +211,31 @@ Then start a new workspace in Kōbō — the agent will pick up the skills autom
|
|
|
205
211
|
|
|
206
212
|
```
|
|
207
213
|
src/
|
|
208
|
-
├── server/
|
|
209
|
-
│ ├── index.ts
|
|
210
|
-
│ ├── db/
|
|
211
|
-
│ ├── services/
|
|
212
|
-
│ ├──
|
|
213
|
-
│
|
|
214
|
-
├──
|
|
214
|
+
├── server/ # Hono backend
|
|
215
|
+
│ ├── index.ts # app bootstrap + WS upgrade
|
|
216
|
+
│ ├── db/ # SQLite schema, migrations, singleton
|
|
217
|
+
│ ├── services/
|
|
218
|
+
│ │ ├── agent/ # agent engine abstraction (replaces agent-manager.ts)
|
|
219
|
+
│ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota, watchdog, public API
|
|
220
|
+
│ │ │ ├── session-controller.ts # lifecycle wrapper around one AgentEngine instance
|
|
221
|
+
│ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
|
|
222
|
+
│ │ │ └── engines/claude-code/ # spawn + NDJSON stream-parser + args-builder + mcp-config + capabilities
|
|
223
|
+
│ │ ├── content-migration-service.ts # legacy ws_events → normalised AgentEvent rows, with DB backup
|
|
224
|
+
│ │ └── … # workspace, dev-server, ws, notion, sentry, settings, pr-template
|
|
225
|
+
│ ├── routes/ # Hono handlers (workspaces, engines, migration, templates, …)
|
|
226
|
+
│ └── utils/ # git-ops, process-tracker, paths
|
|
227
|
+
├── shared/ # modules shared by backend and frontend (e.g. model catalogue)
|
|
228
|
+
├── client/ # Vue 3 + Quasar SPA
|
|
215
229
|
│ └── src/
|
|
216
|
-
│ ├── stores/
|
|
217
|
-
│ ├── components/
|
|
218
|
-
│ ├──
|
|
230
|
+
│ ├── stores/ # Pinia: workspace, websocket, agent-stream, migration, settings, …
|
|
231
|
+
│ ├── components/ # ActivityFeed, TurnCard, WorkspaceList, ChatInput, GitPanel, …
|
|
232
|
+
│ ├── services/ # agent-event-view (foldEvents), conversation-turns (groupIntoTurns), inline-diff
|
|
233
|
+
│ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
|
|
219
234
|
│ └── router/
|
|
220
|
-
├── mcp-server/
|
|
221
|
-
│ ├── kobo-tasks-server.ts
|
|
222
|
-
│ └── kobo-tasks-handlers.ts
|
|
223
|
-
└── __tests__/
|
|
235
|
+
├── mcp-server/ # standalone MCP server spawned per workspace
|
|
236
|
+
│ ├── kobo-tasks-server.ts # entry point, registers list_tasks & mark_task_done
|
|
237
|
+
│ └── kobo-tasks-handlers.ts # pure handlers over SQLite
|
|
238
|
+
└── __tests__/ # Vitest suite (engines, orchestrator, migration, routes, …)
|
|
224
239
|
```
|
|
225
240
|
|
|
226
241
|
See [`AGENTS.md`](./AGENTS.md) for a deeper dive into conventions, data model, WebSocket protocol, and contribution guidelines.
|
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,9 +8,11 @@ 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 { enginesRouter } from './routes/engines.js';
|
|
11
12
|
import gitRouter from './routes/git.js';
|
|
12
13
|
import healthRouter from './routes/health.js';
|
|
13
14
|
import imagesRouter from './routes/images.js';
|
|
15
|
+
import { migrationRouter } from './routes/migration.js';
|
|
14
16
|
import notionRouter from './routes/notion.js';
|
|
15
17
|
import plansRouter from './routes/plans.js';
|
|
16
18
|
import searchRouter from './routes/search.js';
|
|
@@ -18,7 +20,8 @@ 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
|
|
@@ -70,6 +74,8 @@ app.route('/api/templates', templatesRouter);
|
|
|
70
74
|
app.route('/api/workspaces', plansRouter);
|
|
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,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 }));
|
|
@@ -1225,6 +1249,110 @@ app.post('/:id/rebase', (c) => {
|
|
|
1225
1249
|
gitOps.rebaseBranch(worktreePath, workspace.sourceBranch);
|
|
1226
1250
|
return c.json({ success: true });
|
|
1227
1251
|
}
|
|
1252
|
+
catch (err) {
|
|
1253
|
+
if (err instanceof gitOps.GitConflictError) {
|
|
1254
|
+
return c.json({ error: err.message, conflict: true, operation: err.operation, files: err.files }, 409);
|
|
1255
|
+
}
|
|
1256
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1257
|
+
return c.json({ error: message }, 500);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
/** Merge the source branch into the workspace branch (non-fast-forward). */
|
|
1261
|
+
app.post('/:id/merge', (c) => {
|
|
1262
|
+
try {
|
|
1263
|
+
const id = c.req.param('id');
|
|
1264
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1265
|
+
if (!workspace)
|
|
1266
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1267
|
+
const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
|
|
1268
|
+
gitOps.mergeBranch(worktreePath, workspace.sourceBranch);
|
|
1269
|
+
return c.json({ success: true });
|
|
1270
|
+
}
|
|
1271
|
+
catch (err) {
|
|
1272
|
+
if (err instanceof gitOps.GitConflictError) {
|
|
1273
|
+
return c.json({ error: err.message, conflict: true, operation: err.operation, files: err.files }, 409);
|
|
1274
|
+
}
|
|
1275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1276
|
+
return c.json({ error: message }, 500);
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
/** Abort any in-progress merge or rebase in the worktree. */
|
|
1280
|
+
app.post('/:id/git/abort', (c) => {
|
|
1281
|
+
try {
|
|
1282
|
+
const id = c.req.param('id');
|
|
1283
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1284
|
+
if (!workspace)
|
|
1285
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1286
|
+
const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
|
|
1287
|
+
const aborted = gitOps.abortOngoingGitOperation(worktreePath);
|
|
1288
|
+
return c.json({ success: true, aborted });
|
|
1289
|
+
}
|
|
1290
|
+
catch (err) {
|
|
1291
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1292
|
+
return c.json({ error: message }, 500);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
/** Hand off merge/rebase conflicts to the workspace agent with an intelligent-resolution prompt. */
|
|
1296
|
+
app.post('/:id/git/resolve-with-agent', migrationGuard, async (c) => {
|
|
1297
|
+
try {
|
|
1298
|
+
const id = c.req.param('id');
|
|
1299
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1300
|
+
if (!workspace)
|
|
1301
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1302
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
1303
|
+
const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
|
|
1304
|
+
const operation = body.operation ?? gitOps.getOngoingGitOperation(worktreePath) ?? 'merge';
|
|
1305
|
+
const files = body.files && body.files.length > 0 ? body.files : gitOps.getConflictedFiles(worktreePath);
|
|
1306
|
+
if (files.length === 0) {
|
|
1307
|
+
return c.json({ error: 'No conflicted files detected — nothing for the agent to resolve' }, 400);
|
|
1308
|
+
}
|
|
1309
|
+
const fileList = files.map((f) => `- ${f}`).join('\n');
|
|
1310
|
+
const continueCmd = operation === 'merge' ? 'git merge --continue' : 'git rebase --continue';
|
|
1311
|
+
const prompt = `I started a \`git ${operation}\` of \`origin/${workspace.sourceBranch}\` into our working branch \`${workspace.workingBranch}\` and it produced conflicts that I need your help to resolve INTELLIGENTLY.
|
|
1312
|
+
|
|
1313
|
+
Conflicted files (${files.length}):
|
|
1314
|
+
${fileList}
|
|
1315
|
+
|
|
1316
|
+
## Resolution rules — read carefully
|
|
1317
|
+
|
|
1318
|
+
1. **Our branch is the source of truth for the feature we are building.** Its behavior must be preserved.
|
|
1319
|
+
2. **The source branch (\`${workspace.sourceBranch}\`) carries legitimate upstream changes** (bug fixes, refactors, dependency bumps). Integrate these where they don't conflict with our intent.
|
|
1320
|
+
3. **Do NOT blindly pick a side.** Neither \`--ours\` nor \`--theirs\` wholesale. Read each conflict hunk and reason about what the correct merged state is.
|
|
1321
|
+
4. **Think semantically, not syntactically.** If our branch renamed \`foo\` to \`bar\` and the source branch added a new call to \`foo\`, the correct resolution is a new call to \`bar\`, not "keep ours and drop the new call".
|
|
1322
|
+
5. **Preserve tests and contracts.** If both sides touched the same test, keep coverage from both.
|
|
1323
|
+
6. **Imports, versions, lock files:** prefer the superset (union) unless they genuinely conflict — in which case use the more recent / more restrictive.
|
|
1324
|
+
|
|
1325
|
+
## Steps
|
|
1326
|
+
|
|
1327
|
+
1. For each conflicted file, open it and read both conflict markers.
|
|
1328
|
+
2. Decide the merge intent. If unsure, investigate both sides' commit history (\`git log --oneline ours..HEAD <file>\` vs \`git log --oneline origin/${workspace.sourceBranch} <file>\`).
|
|
1329
|
+
3. Edit the file to the correct merged state and remove the conflict markers.
|
|
1330
|
+
4. Run the test suite to verify no regression (\`npm test\` or the project's equivalent).
|
|
1331
|
+
5. \`git add <resolved-files>\` then \`${continueCmd}\`.
|
|
1332
|
+
6. Report the summary: which files you touched, the key decisions you made, and the final test result.
|
|
1333
|
+
|
|
1334
|
+
Start now.`;
|
|
1335
|
+
// Persist the prompt in the chat feed so the user sees what was dispatched.
|
|
1336
|
+
const session = workspaceService.getActiveSession(workspace.id);
|
|
1337
|
+
wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, session?.id ?? undefined);
|
|
1338
|
+
let messageSent = false;
|
|
1339
|
+
try {
|
|
1340
|
+
agentManager.sendMessage(workspace.id, prompt);
|
|
1341
|
+
messageSent = true;
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
try {
|
|
1345
|
+
agentManager.startAgent(workspace.id, worktreePath, prompt, workspace.model, true, workspace.permissionMode, undefined, workspace.reasoningEffort);
|
|
1346
|
+
workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
|
|
1347
|
+
messageSent = true;
|
|
1348
|
+
}
|
|
1349
|
+
catch (resumeErr) {
|
|
1350
|
+
const resumeMsg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
|
|
1351
|
+
console.warn(`[workspaces] resolve-with-agent: agent resume failed: ${resumeMsg}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return c.json({ ok: true, operation, files, messageSent });
|
|
1355
|
+
}
|
|
1228
1356
|
catch (err) {
|
|
1229
1357
|
const message = err instanceof Error ? err.message : String(err);
|
|
1230
1358
|
return c.json({ error: message }, 500);
|
|
@@ -1385,7 +1513,7 @@ app.post('/:id/mark-read', (c) => {
|
|
|
1385
1513
|
}
|
|
1386
1514
|
});
|
|
1387
1515
|
// POST /api/workspaces/:id/stop — stop agent
|
|
1388
|
-
app.post('/:id/stop', (c) => {
|
|
1516
|
+
app.post('/:id/stop', migrationGuard, (c) => {
|
|
1389
1517
|
try {
|
|
1390
1518
|
const id = c.req.param('id');
|
|
1391
1519
|
const workspace = workspaceService.getWorkspace(id);
|
|
@@ -1413,7 +1541,7 @@ app.post('/:id/stop', (c) => {
|
|
|
1413
1541
|
}
|
|
1414
1542
|
});
|
|
1415
1543
|
// POST /api/workspaces/:id/interrupt — soft-interrupt agent (SIGINT, like Escape in Claude Code)
|
|
1416
|
-
app.post('/:id/interrupt', (c) => {
|
|
1544
|
+
app.post('/:id/interrupt', migrationGuard, (c) => {
|
|
1417
1545
|
try {
|
|
1418
1546
|
const id = c.req.param('id');
|
|
1419
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CLAUDE_MODELS } from '../../../../../shared/models.js';
|
|
2
|
+
export const CLAUDE_CODE_CAPABILITIES = {
|
|
3
|
+
// Models come from the shared catalogue in `src/shared/models.ts` — the
|
|
4
|
+
// ONE source of truth, consumed both by this file (for /api/engines and
|
|
5
|
+
// for validation in POST /api/workspaces) and by the frontend selectors.
|
|
6
|
+
models: CLAUDE_MODELS.map((m) => ({ id: m.id, label: m.label })),
|
|
7
|
+
effortLevels: [
|
|
8
|
+
{ id: 'auto', label: 'Auto' },
|
|
9
|
+
{ id: 'low', label: 'Low' },
|
|
10
|
+
{ id: 'medium', label: 'Medium' },
|
|
11
|
+
{ id: 'high', label: 'High' },
|
|
12
|
+
],
|
|
13
|
+
permissionModes: ['auto-accept', 'plan'],
|
|
14
|
+
supportsResume: true,
|
|
15
|
+
supportsMcp: true,
|
|
16
|
+
supportsSkills: true,
|
|
17
|
+
};
|