@loicngr/kobo 1.6.0 → 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 +29 -16
- 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 +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-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-_oPM07ln.js → MainLayout-CFbMw65L.js} +17 -17
- 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-DMX8jq8u.js → cssMode-BYtqFZtm.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-DirOkGGg.js → editor.api-D6ZaO4A_.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-DC4ezIu0.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-DI9xJfj0.js → freemarker2-CBm--bBd.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-B9F-pScn.js → handlebars-whX2mkV5.js} +1 -1
- package/src/client/dist/spa/assets/{html-DTe2v8Q8.js → html-D7ga_o6c.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-F_XLjWfJ.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-B9xJRPC6.js → javascript-BwmzNMn5.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-DTZ6j6UO.js → jsonMode-CN5Z5bK_.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-BjU5MtW6.js → liquid-CzMNAPor.js} +1 -1
- package/src/client/dist/spa/assets/{marked.esm-DCmk6NO8.js → marked.esm-DW0ulF0a.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BMUpG7Be.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-D7JUf8DP.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-Dz0D4uSk.js → python-9DTZ8C3K.js} +1 -1
- package/src/client/dist/spa/assets/{razor-D7CFxuwR.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-DjscaxpS.js → tsMode-DI2bWo8r.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-DozCWZl2.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-DFOJMT39.js → xml-D6qm6rp0.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-yEefnsXm.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-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/HealthPage-Do8QZdxw.js +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/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-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,8 +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.
|
|
7
9
|
>
|
|
8
|
-
> **Engine refactor
|
|
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.
|
|
9
11
|
|
|
10
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.
|
|
11
13
|
|
|
@@ -92,7 +94,9 @@ npm start # runs the compiled server
|
|
|
92
94
|
### Test & lint
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
|
-
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
|
|
96
100
|
npm run lint # biome check (lint + format verification)
|
|
97
101
|
npm run lint:fix # biome check with safe auto-fixes
|
|
98
102
|
npm run format # biome format --write
|
|
@@ -207,22 +211,31 @@ Then start a new workspace in Kōbō — the agent will pick up the skills autom
|
|
|
207
211
|
|
|
208
212
|
```
|
|
209
213
|
src/
|
|
210
|
-
├── server/
|
|
211
|
-
│ ├── index.ts
|
|
212
|
-
│ ├── db/
|
|
213
|
-
│ ├── services/
|
|
214
|
-
│ ├──
|
|
215
|
-
│
|
|
216
|
-
├──
|
|
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
|
|
217
229
|
│ └── src/
|
|
218
|
-
│ ├── stores/
|
|
219
|
-
│ ├── components/
|
|
220
|
-
│ ├──
|
|
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
|
|
221
234
|
│ └── router/
|
|
222
|
-
├── mcp-server/
|
|
223
|
-
│ ├── kobo-tasks-server.ts
|
|
224
|
-
│ └── kobo-tasks-handlers.ts
|
|
225
|
-
└── __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, …)
|
|
226
239
|
```
|
|
227
240
|
|
|
228
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 }));
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CLAUDE_MODELS } from '../../../../../shared/models.js';
|
|
2
|
+
export const CLAUDE_CODE_CAPABILITIES = {
|
|
3
|
+
// Models come from the shared catalogue in `src/shared/models.ts` — the
|
|
4
|
+
// ONE source of truth, consumed both by this file (for /api/engines and
|
|
5
|
+
// for validation in POST /api/workspaces) and by the frontend selectors.
|
|
6
|
+
models: CLAUDE_MODELS.map((m) => ({ id: m.id, label: m.label })),
|
|
7
|
+
effortLevels: [
|
|
8
|
+
{ id: 'auto', label: 'Auto' },
|
|
9
|
+
{ id: 'low', label: 'Low' },
|
|
10
|
+
{ id: 'medium', label: 'Medium' },
|
|
11
|
+
{ id: 'high', label: 'High' },
|
|
12
|
+
],
|
|
13
|
+
permissionModes: ['auto-accept', 'plan'],
|
|
14
|
+
supportsResume: true,
|
|
15
|
+
supportsMcp: true,
|
|
16
|
+
supportsSkills: true,
|
|
17
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { buildClaudeArgs } from './args-builder.js';
|
|
4
|
+
import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
|
|
5
|
+
import { cleanupMcpConfig, writeMcpConfig } from './mcp-config.js';
|
|
6
|
+
import { createParserState, parseClaudeLine } from './stream-parser.js';
|
|
7
|
+
export function createClaudeCodeEngine() {
|
|
8
|
+
return {
|
|
9
|
+
id: 'claude-code',
|
|
10
|
+
displayName: 'Claude Code',
|
|
11
|
+
capabilities: CLAUDE_CODE_CAPABILITIES,
|
|
12
|
+
async start(options, onEvent) {
|
|
13
|
+
// Write MCP config if any servers requested + engine supports MCP
|
|
14
|
+
let mcpConfigPath;
|
|
15
|
+
if (options.mcpServers && options.mcpServers.length > 0) {
|
|
16
|
+
mcpConfigPath = writeMcpConfig(options.workingDir, options.mcpServers);
|
|
17
|
+
}
|
|
18
|
+
const { args } = buildClaudeArgs({
|
|
19
|
+
prompt: options.prompt,
|
|
20
|
+
model: options.model,
|
|
21
|
+
effort: options.effort,
|
|
22
|
+
permissionMode: options.permissionMode ?? 'auto-accept',
|
|
23
|
+
skipPermissions: options.settings.dangerouslySkipPermissions ?? true,
|
|
24
|
+
resumeFromEngineSessionId: options.resumeFromEngineSessionId,
|
|
25
|
+
mcpConfigPath,
|
|
26
|
+
});
|
|
27
|
+
const proc = spawn('claude', args, {
|
|
28
|
+
cwd: options.workingDir,
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
const parserState = createParserState();
|
|
32
|
+
if (!proc.stdout)
|
|
33
|
+
throw new Error('Claude process has no stdout');
|
|
34
|
+
const rl = readline.createInterface({
|
|
35
|
+
input: proc.stdout,
|
|
36
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
37
|
+
});
|
|
38
|
+
let discoveredSessionId;
|
|
39
|
+
rl.on('line', (line) => {
|
|
40
|
+
const { events } = parseClaudeLine(line, parserState);
|
|
41
|
+
for (const ev of events) {
|
|
42
|
+
if (ev.kind === 'session:started')
|
|
43
|
+
discoveredSessionId = ev.engineSessionId;
|
|
44
|
+
onEvent(ev);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// Line-buffer stderr so we see one event per log line instead of
|
|
48
|
+
// arbitrary byte chunks, and restrict quota detection to clear rate-
|
|
49
|
+
// limit signals (not every occurrence of the word "rate" or "quota").
|
|
50
|
+
// Non-quota stderr lines are logged to the console but do NOT emit
|
|
51
|
+
// an error event — this avoids false positives flooding the UI.
|
|
52
|
+
const stderrRl = proc.stderr
|
|
53
|
+
? readline.createInterface({
|
|
54
|
+
input: proc.stderr,
|
|
55
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
56
|
+
})
|
|
57
|
+
: undefined;
|
|
58
|
+
// Known benign stderr lines from the Claude CLI that should NOT be
|
|
59
|
+
// logged — they flood the dev console and carry no actionable info.
|
|
60
|
+
// Strip ANSI color codes before matching.
|
|
61
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes by design
|
|
62
|
+
const stripAnsi = (s) => s.replace(/\u001b\[\d+m/g, '');
|
|
63
|
+
function isBenignStderr(line) {
|
|
64
|
+
const cleaned = stripAnsi(line).trim();
|
|
65
|
+
return /^warning: no stdin data received in \d+s/i.test(cleaned);
|
|
66
|
+
}
|
|
67
|
+
stderrRl?.on('line', (line) => {
|
|
68
|
+
const lower = line.toLowerCase();
|
|
69
|
+
const isQuota = lower.includes('rate limit exceeded') ||
|
|
70
|
+
lower.includes('rate_limit_exceeded') ||
|
|
71
|
+
(lower.includes('429') && lower.includes('rate')) ||
|
|
72
|
+
lower.includes('quota exceeded');
|
|
73
|
+
if (isQuota) {
|
|
74
|
+
onEvent({ kind: 'error', category: 'quota', message: line });
|
|
75
|
+
}
|
|
76
|
+
else if (line.trim().length > 0 && !isBenignStderr(line)) {
|
|
77
|
+
console.warn(`[claude-engine stderr] ${line}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// 'error' fires when spawn itself fails (e.g. ENOENT if the `claude`
|
|
81
|
+
// binary is missing from PATH). In that case 'exit' never fires, so we
|
|
82
|
+
// emit the lifecycle pair here and clean the MCP config ourselves.
|
|
83
|
+
proc.on('error', (err) => {
|
|
84
|
+
onEvent({ kind: 'error', category: 'spawn_failed', message: err.message });
|
|
85
|
+
onEvent({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
86
|
+
cleanupMcpConfig(options.workingDir);
|
|
87
|
+
rl.close();
|
|
88
|
+
stderrRl?.close();
|
|
89
|
+
});
|
|
90
|
+
proc.on('exit', (code) => {
|
|
91
|
+
cleanupMcpConfig(options.workingDir);
|
|
92
|
+
rl.close();
|
|
93
|
+
stderrRl?.close();
|
|
94
|
+
onEvent({
|
|
95
|
+
kind: 'session:ended',
|
|
96
|
+
reason: code === 0 ? 'completed' : code === null ? 'killed' : 'error',
|
|
97
|
+
exitCode: code,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const engineProcess = {
|
|
101
|
+
get pid() {
|
|
102
|
+
return proc.pid;
|
|
103
|
+
},
|
|
104
|
+
get engineSessionId() {
|
|
105
|
+
return discoveredSessionId;
|
|
106
|
+
},
|
|
107
|
+
sendMessage(text) {
|
|
108
|
+
if (!proc.stdin?.writable)
|
|
109
|
+
throw new Error('Agent stdin not writable');
|
|
110
|
+
proc.stdin.write(`${text}\n`);
|
|
111
|
+
},
|
|
112
|
+
interrupt() {
|
|
113
|
+
if (proc.pid !== undefined)
|
|
114
|
+
process.kill(proc.pid, 'SIGINT');
|
|
115
|
+
},
|
|
116
|
+
stop() {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
if (proc.killed || proc.exitCode !== null)
|
|
119
|
+
return resolve();
|
|
120
|
+
let resolved = false;
|
|
121
|
+
let killTimer;
|
|
122
|
+
let hardTimeout;
|
|
123
|
+
const doResolve = () => {
|
|
124
|
+
if (resolved)
|
|
125
|
+
return;
|
|
126
|
+
resolved = true;
|
|
127
|
+
if (killTimer)
|
|
128
|
+
clearTimeout(killTimer);
|
|
129
|
+
if (hardTimeout)
|
|
130
|
+
clearTimeout(hardTimeout);
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
proc.once('exit', doResolve);
|
|
134
|
+
try {
|
|
135
|
+
proc.kill('SIGTERM');
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Already dead
|
|
139
|
+
}
|
|
140
|
+
killTimer = setTimeout(() => {
|
|
141
|
+
try {
|
|
142
|
+
if (!proc.killed)
|
|
143
|
+
proc.kill('SIGKILL');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore
|
|
147
|
+
}
|
|
148
|
+
}, 5000);
|
|
149
|
+
killTimer.unref?.();
|
|
150
|
+
// Hard-timeout safety net: if the process hasn't exited within 10s
|
|
151
|
+
// (5s after SIGKILL), resolve anyway so callers never hang forever.
|
|
152
|
+
hardTimeout = setTimeout(() => {
|
|
153
|
+
console.warn('[claude-engine] stop() hard-timeout reached, resolving anyway');
|
|
154
|
+
doResolve();
|
|
155
|
+
}, 10000);
|
|
156
|
+
hardTimeout.unref?.();
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
return engineProcess;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|