@loicngr/kobo 1.6.15 → 1.7.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/README.md +13 -7
- package/dist/mcp-server/kobo-tasks-handlers.js +2 -1
- package/dist/mcp-server/kobo-tasks-server.js +51 -0
- package/dist/server/db/migrations.js +40 -0
- package/dist/server/db/schema.js +7 -5
- package/dist/server/index.js +12 -11
- package/dist/server/routes/health.js +2 -2
- package/dist/server/routes/settings.js +2 -1
- package/dist/server/routes/workspaces.js +165 -32
- package/dist/server/services/agent/engines/claude-code/capabilities.js +1 -1
- package/dist/server/services/agent/engines/claude-code/engine.js +237 -132
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +234 -0
- package/dist/server/services/agent/engines/claude-code/options-builder.js +68 -0
- package/dist/server/services/agent/engines/claude-code/precompact-hook.js +27 -0
- package/dist/server/services/agent/engines/types.js +1 -0
- package/dist/server/services/agent/orchestrator.js +536 -94
- package/dist/server/services/agent/session-controller.js +14 -43
- package/dist/server/services/auto-loop-service.js +19 -8
- package/dist/server/services/content-migration-service.js +24 -94
- package/dist/server/services/settings-service.js +35 -1
- package/dist/server/services/wakeup-service.js +10 -11
- package/dist/server/services/workspace-service.js +42 -37
- package/dist/server/services/worktree-service.js +17 -7
- package/dist/server/utils/worktree-paths.js +134 -0
- package/dist/shared/consts.js +1 -0
- package/package.json +2 -1
- package/src/client/dist/spa/assets/ActivityFeed-Chn8aZvi.js +7 -0
- package/src/client/dist/spa/assets/ActivityFeed-LXnbg3ff.css +1 -0
- package/src/client/dist/spa/assets/{ClosePopup-D7BBEcaf.js → ClosePopup-BUlGXTqh.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-BE3xfQsC.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-BGtqoZ8d.js +2 -0
- package/src/client/dist/spa/assets/{DiffViewer-BJZADilo.js → DiffViewer-qjJ-biOw.js} +3 -3
- package/src/client/dist/spa/assets/HealthPage-CKyf7ky6.js +1 -0
- package/src/client/dist/spa/assets/MainLayout-B07zv82Z.css +1 -0
- package/src/client/dist/spa/assets/{MainLayout-CFHf3zKv.js → MainLayout-Br3jmaOw.js} +17 -17
- package/src/client/dist/spa/assets/QCheckbox-CcY7ZSk9.js +1 -0
- package/src/client/dist/spa/assets/{QChip-D905z6BM.js → QChip-BhT0W2Dg.js} +1 -1
- package/src/client/dist/spa/assets/QExpansionItem-BnIPCzXR.js +1 -0
- package/src/client/dist/spa/assets/{QInput-6U0_avSY.js → QInput-D4WJro4e.js} +1 -1
- package/src/client/dist/spa/assets/{QItemSection-Cloi4ErY.js → QItemSection-KFAnxzMK.js} +1 -1
- package/src/client/dist/spa/assets/QMenu-0LsqhRZT.js +1 -0
- package/src/client/dist/spa/assets/{QPage-C6h_ah5z.js → QPage-Cu7zkfc6.js} +1 -1
- package/src/client/dist/spa/assets/QRadio-DaZhdLCg.js +1 -0
- package/src/client/dist/spa/assets/{touch-DBLw8vQK.js → QResizeObserver-Cf79V-VZ.js} +1 -1
- package/src/client/dist/spa/assets/QScrollArea-BDCKOKuE.js +1 -0
- package/src/client/dist/spa/assets/QTabPanels-Ctnrqvp9.js +1 -0
- package/src/client/dist/spa/assets/QToggle-CGpiJLDJ.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-B3CmRx4j.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-Ce8Uc7Ol.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-8N0X7B7o.css +1 -0
- package/src/client/dist/spa/assets/SettingsPage-BaaSJ3eJ.js +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-DQxGe62K.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-wZUUTDzp.js +4 -0
- package/src/client/dist/spa/assets/build-path-tree-DRViYT3t.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-QQTtBrD_.js → cssMode-uAfRqG2Q.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-YqpktRoe.js → editor.api-5GUlxvcL.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-DDGqfxYm.js → editor.main-CSTJjBIa.js} +3 -3
- package/src/client/dist/spa/assets/expand-template-zA3pTyIP.js +1 -0
- package/src/client/dist/spa/assets/{formatters-DWeOzSfw.js → formatters-ejxELb0M.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-BC_Lt7t3.js → freemarker2-BxBnI8Nb.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-BphhRg2c.js → handlebars-DrbIsXmT.js} +1 -1
- package/src/client/dist/spa/assets/{html-C84Ufc1n.js → html-DH7u_g5l.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-CIlyKZJ4.js → htmlMode-BlY9QO3f.js} +1 -1
- package/src/client/dist/spa/assets/i18n-B41j--A3.js +1 -0
- package/src/client/dist/spa/assets/index-DoYBJtQA.js +2 -0
- package/src/client/dist/spa/assets/{javascript-D5LTZTWn.js → javascript-B-AL31ke.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-YBOBMJNl.js → jsonMode-Dx7CA4ag.js} +1 -1
- package/src/client/dist/spa/assets/{kobo-commands-DFflpxts.js → kobo-commands-DiUm1Y34.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-UNCP2Jl6.js → liquid--H7Vomnm.js} +1 -1
- package/src/client/dist/spa/assets/{marked.esm-D7ibHC_y.js → marked.esm-DLCrAGtO.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-CsHyBm_B.js → mdx-BOackeU6.js} +1 -1
- package/src/client/dist/spa/assets/{models-tXWASlTL.js → models-BPfFBcxr.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-Bv79M2zD.js → monaco.contribution-ydrMjZwK.js} +2 -2
- package/src/client/dist/spa/assets/{python-B3h-WTW0.js → python-BWGSV-nk.js} +1 -1
- package/src/client/dist/spa/assets/{razor-Cs79ULMl.js → razor-BGnl83cS.js} +1 -1
- package/src/client/dist/spa/assets/settings-lT4GB-uB.js +1 -0
- package/src/client/dist/spa/assets/symbols-BVRrMH2r.js +1 -0
- package/src/client/dist/spa/assets/touch-Co9pfjUU.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-238NR35q.js → tsMode-Chjqq1f3.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-C93UakWa.js → typescript-By7Y7PAP.js} +1 -1
- package/src/client/dist/spa/assets/{use-checkbox-w-raiu10.js → use-checkbox-DzHmcu7s.js} +1 -1
- package/src/client/dist/spa/assets/use-panel-DWX2aNMM.js +1 -0
- package/src/client/dist/spa/assets/{xml-24CcVrVJ.js → xml-DoAeCRiy.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-BLhB8_OL.js → yaml-DlT7YOhG.js} +1 -1
- package/src/client/dist/spa/index.html +10 -7
- package/src/mcp-server/kobo-tasks-handlers.ts +2 -1
- package/src/mcp-server/kobo-tasks-server.ts +60 -1
- package/dist/server/services/agent/engines/claude-code/args-builder.js +0 -57
- package/dist/server/services/agent/engines/claude-code/mcp-config.js +0 -23
- package/dist/server/services/agent/engines/claude-code/stream-parser.js +0 -386
- package/src/client/dist/spa/assets/ActivityFeed-BHdMJRwS.css +0 -1
- package/src/client/dist/spa/assets/ActivityFeed-D7MF6IK1.js +0 -8
- package/src/client/dist/spa/assets/CreatePage-DJbZH8wp.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-rp-9_jOF.js +0 -2
- package/src/client/dist/spa/assets/HealthPage-CZQB2pvh.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-Db3dwSTM.css +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-CUXuOfeR.js +0 -1
- package/src/client/dist/spa/assets/QMenu-BPzgTm2k.js +0 -1
- package/src/client/dist/spa/assets/QScrollArea-N10UpHIf.js +0 -1
- package/src/client/dist/spa/assets/QSlideTransition-BMX92yUu.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels-PPompnxw.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-DLT8jCHz.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-CfYy4vGJ.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-B8DhSZw7.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-ONWYC-Bn.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-B2VAbf6l.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-k2pgeRoy.css +0 -1
- package/src/client/dist/spa/assets/build-path-tree-DETFP2lL.js +0 -1
- package/src/client/dist/spa/assets/expand-template-CZkefibF.js +0 -1
- package/src/client/dist/spa/assets/i18n-CNdSgNP6.js +0 -1
- package/src/client/dist/spa/assets/index-pGAaG7Rh.js +0 -2
- package/src/client/dist/spa/assets/settings-Cw4mtk9x.js +0 -1
- package/src/client/dist/spa/assets/stats-BrLStQKj.js +0 -1
- package/src/client/dist/spa/assets/symbols-TAFELniU.js +0 -1
- /package/src/client/dist/spa/assets/{QBadge-BUkmTO0P.js → QBadge-fsQ2AokU.js} +0 -0
- /package/src/client/dist/spa/assets/{QBtn-CyzfM9-_.js → QBtn-DHwAb18J.js} +0 -0
- /package/src/client/dist/spa/assets/{QItemLabel-DwnV_S8y.js → QItemLabel-DWwenW2S.js} +0 -0
- /package/src/client/dist/spa/assets/{QList-DZfpUv3n.js → QList-NmIE6Rd9.js} +0 -0
- /package/src/client/dist/spa/assets/{QSpace-PlDK6Fg3.js → QSpace-COlmM_4F.js} +0 -0
- /package/src/client/dist/spa/assets/{QSpinnerDots-D7bo_KgI.js → QSpinnerDots-DwtnRN2r.js} +0 -0
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-CpNzZuug.js → _plugin-vue_export-helper-B8bB5DBd.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-DrZwwXZX.js → abap-DzK-OTGh.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-CrCz0btt.js → apex-Bj60_dRt.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-BapzKHay.js → azcli-B6NwaBAZ.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-C_NRAiA1.js → bat-bf7wXV68.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-C7pp2CNk.js → bicep-C_bg8UgA.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-BhhK9vxZ.js → cameligo-CTWw4D4B.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-D0ujmUyE.js → clojure-CgdPoH0r.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-DHEl7Jbb.js → coffee-gHQfdA5M.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-Iil-3nzZ.js → cpp-BM4Jj4aW.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-Dh0Ee7SY.js → csharp-D8-bh4Cd.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-mwzjw0JL.js → csp-CXBxRx0n.js} +0 -0
- /package/src/client/dist/spa/assets/{css-COIa8ZTR.js → css-DKjIxrmY.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-GVc17FC4.js → cypher-C5e5inIh.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-phiCaE7_.js → dart-BhRHHm4x.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-BMaDhdim.js → dockerfile-DW5REF8E.js} +0 -0
- /package/src/client/dist/spa/assets/{documents-BMdAS6h8.js → documents-D6A3wRry.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-Cj47kvqp.js → ecl-Bw4Hg3n_.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-DBbstcE1.js → elixir-DHmoBvpZ.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-ChHb1adO.js → flow9-BsFExz3v.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-CDI_AxQw.js → fsharp-BaeLhgfq.js} +0 -0
- /package/src/client/dist/spa/assets/{go-DmsC2k-Y.js → go-Bd-NFKIC.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-C8hjT6Ki.js → graphql-DZVerJfy.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-C15cAQOZ.js → hcl-CAVzrZfH.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-CKrAe0ag.js → ini-CyXdX58t.js} +0 -0
- /package/src/client/dist/spa/assets/{java-BVhjILyl.js → java-B5pNgvhy.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-BzPDHDOG.js → julia-XRhmV3AN.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-DQMAn-b6.js → kotlin-DOd3J5vr.js} +0 -0
- /package/src/client/dist/spa/assets/{less-428mfr1h.js → less-veZSnyw6.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-B09dCO6A.js → lexon-QWGkuK0H.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-CVQ0BJif.js → lua-CYGpjuO5.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-CiPQ1ljw.js → m3-yNnrZkdc.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown--G0dqL-7.js → markdown-BCSWEPSX.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-BaboCM3T.js → mips-OpYmcC30.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-DUaqkqre.js → msdax-2oxoTO9Z.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-CUE6XF4r.js → mysql-5KlC-K_9.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-C4MUnzeT.js → objective-c-CcDCgtLx.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-CWMUMx__.js → pascal-BZGsbaEV.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-DLCVutek.js → pascaligo-DtD5qU3G.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-JYoirQpx.js → perl-C1jNNS3E.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-BqOy7sqx.js → pgsql-CT0fhiZa.js} +0 -0
- /package/src/client/dist/spa/assets/{php-PZqsysO1.js → php-D6DrXoPM.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-BiwqVlg6.js → pla-b3-HN2pF.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-COxQtXCD.js → postiats-Bin2ApVS.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-DdXUmaWa.js → powerquery-7ASnn-ZG.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-D05yu9sz.js → powershell-t4p7sU1H.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-BDsm0ZB_.js → protobuf-BUGeWa_j.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-3CmTiGoi.js → pug-BuKcgC9s.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-C4eHfCpJ.js → qsharp-DSMtI_O7.js} +0 -0
- /package/src/client/dist/spa/assets/{r-Decg_RIU.js → r-DMlFgn7A.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-Cl3EBA4R.js → redis-cXItkC5u.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-5ZsNLhOp.js → redshift-BZVbW7HE.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-BulNNF_e.js → restructuredtext-BzjxwS8h.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-D3Axi_9w.js → ruby-C5nyLV4l.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-Csys1Tos.js → rust-BcmMsHdf.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-C_iBPphi.js → sb-Dnb1iy6B.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-Cg4p-EZ2.js → scala-anMIFYpA.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-BlVnEL_j.js → scheme-BItQTe08.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-CmLW8ojr.js → scss-BOv51BJ5.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-B1DV_gpl.js → shell-BsRYRTNN.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-glFpNhe3.js → solidity-BtuLgGDx.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-D9j4cFkA.js → sophia-B0Vkc5MF.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-DV5Ux9cO.js → sparql-B7lvkZQM.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-K8tNKFcf.js → sql-DvP5MpA3.js} +0 -0
- /package/src/client/dist/spa/assets/{st-BhIdE2hj.js → st-GVUeyB3U.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-B0pzSmmx.js → swift-DSPIoCjm.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-CeBgixbN.js → systemverilog-Icj2-k23.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-B0Ji3IbZ.js → tcl-Cd8KQcm-.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-KUgPCP41.js → twig-CBHmt8z3.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-ryrhjid6.js → typespec-Ckc037mq.js} +0 -0
- /package/src/client/dist/spa/assets/{use-quasar-Clv5nVxk.js → use-quasar-Cc4smfg5.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-Z68-YtMY.js → vb-B97GW9Wb.js} +0 -0
- /package/src/client/dist/spa/assets/{vue-i18n-BVrBmgZa.js → vue-i18n-eUDnMrPl.js} +0 -0
- /package/src/client/dist/spa/assets/{wgsl-bH-W-d_T.js → wgsl-DIKmb3YH.js} +0 -0
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
- **Isolated git worktrees** — every workspace runs on its own branch in its own directory, so concurrent Claude sessions never step on each other
|
|
15
|
-
- **Pluggable agent engine** — Kōbō talks to agents through an `AgentEngine` contract with a normalised `AgentEvent` stream (`src/server/services/agent/engines/`).
|
|
14
|
+
- **Isolated git worktrees** — every workspace runs on its own branch in its own directory, with a configurable global worktrees root for new workspaces, so concurrent Claude sessions never step on each other
|
|
15
|
+
- **Pluggable agent engine** — Kōbō talks to agents through an `AgentEngine` contract with a normalised `AgentEvent` stream (`src/server/services/agent/engines/`). The `claude-code` engine runs on the official [`@anthropic-ai/claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-typescript); adding a second runtime (e.g. Codex) only requires a new adapter, not a rewrite of the UI or orchestration layer
|
|
16
|
+
- **Interactive `AskUserQuestion`** — when the agent invokes `AskUserQuestion`, Kōbō pauses the session via the SDK's `defer` pattern, surfaces a question panel in the UI, and resumes the agent once the user answers. The session does not occupy any resources while it waits
|
|
16
17
|
- **Rich chat feed** — live streaming text, thinking blocks, inline tool calls with expandable diffs for Edit/Write, per-turn session cards, markdown rendering, jump-to-previous-user-message button, and infinite scroll-up over persisted history
|
|
17
18
|
- **Task & acceptance criteria tracking** — the agent reports progress through a dedicated MCP server (`kobo-tasks`) that reads and updates tasks directly from the SQLite database
|
|
18
19
|
- **Documents panel** — tree view in the right drawer that surfaces every AI-generated markdown file under `docs/plans/`, `docs/superpowers/`, and `.ai/thoughts/`. Paths mentioned in chat messages are auto-detected against the catalogue and become one-click deep-links into the panel
|
|
@@ -31,7 +32,7 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
|
|
|
31
32
|
- **Soft interrupt** — pause an agent mid-execution (SIGINT, like pressing Escape in Claude Code) without killing the process; the agent stops the current tool and waits for the next message
|
|
32
33
|
- **Archive instead of delete** — soft-remove workspaces without losing the worktree, branches, or history; unarchive restores the exact pre-archive state
|
|
33
34
|
- **Auto-loop mode** — opt-in, per-workspace: when enabled, Kōbō spawns a fresh Claude session for the next pending task after every `session:ended`, walking through the task list until all are `done`. Stops automatically on error, on stall (3 consecutive sessions with no task completed), or when the user clicks Stop. A grooming step (`/kobo-prep-autoloop`) ensures tasks are atomic before the loop runs; Notion-imported workspaces with both todos and acceptance criteria are auto-unlocked. **E2E grooming** — when a project declares an E2E framework in Settings (Cypress, Playwright, Vitest, etc.), the grooming phase injects an `[E2E] ` test sub-task between every parent task; each iteration then runs the matching E2E suite as part of its acceptance check
|
|
34
|
-
- **Attach existing worktrees** — Kōbō detects orphan worktrees
|
|
35
|
+
- **Attach existing worktrees** — Kōbō detects orphan git worktrees for the selected project (created outside Kōbō, or left over from an earlier install) and lets you attach them to a new workspace from the creation form, picking up the existing branch and folder instead of cloning a new one
|
|
35
36
|
- **Quota-aware retry backoff** — when a Claude rate limit is hit mid-session, Kōbō schedules the retry at the actual reset time reported by the API (via `rate_limit.info.buckets[].resetsAt`), falling back to a 15 → 30 → 60 → 180 → 300 min ladder only when the reset info is missing or implausible
|
|
36
37
|
- **Scheduled wakeups** — the `ScheduleWakeup` tool is honoured server-side: Kōbō persists the wakeup in SQLite, rehydrates on restart, and respawns the agent with `--resume` at the target time
|
|
37
38
|
|
|
@@ -47,7 +48,7 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
|
|
|
47
48
|
### Prerequisites
|
|
48
49
|
|
|
49
50
|
- Node.js ≥ 20
|
|
50
|
-
- [Claude Code
|
|
51
|
+
- [Claude Code](https://claude.com/claude-code) authenticated via `claude /login` once. The `claude` CLI is **no longer required at runtime** — Kōbō embeds the official [`@anthropic-ai/claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-typescript), which reuses the same login.
|
|
51
52
|
- Git
|
|
52
53
|
- Optional: Docker (if you configure per-workspace dev servers)
|
|
53
54
|
- Optional: `gh` CLI (if you use the PR automation)
|
|
@@ -62,7 +63,7 @@ SERVER_PORT=9998 PORT=9999 npx @loicngr/kobo@latest
|
|
|
62
63
|
|
|
63
64
|
That's it. npm downloads the package, installs dependencies, starts the Kōbō server on the port you specified, and serves the web UI at `http://localhost:9999`. Data is persisted to `~/.config/kobo/` (overridable via `KOBO_HOME`).
|
|
64
65
|
|
|
65
|
-
On first launch Kōbō creates `~/.config/kobo/` if it doesn't exist. If
|
|
66
|
+
On first launch Kōbō creates `~/.config/kobo/` if it doesn't exist. If you have not yet logged in to Claude Code (`claude /login`), the SDK will prompt for an `ANTHROPIC_API_KEY` instead — log in once to share the same authentication across Claude Code, the embedded SDK, and the quota poller.
|
|
66
67
|
|
|
67
68
|
### Run from source (contributors)
|
|
68
69
|
|
|
@@ -251,7 +252,7 @@ See [`AGENTS.md`](./AGENTS.md) for a deeper dive into conventions, data model, W
|
|
|
251
252
|
|
|
252
253
|
| Table | Purpose |
|
|
253
254
|
|---|---|
|
|
254
|
-
| `workspaces` | the unit of work — branch, status, model, engine, `archived_at`, `favorited_at`, `tags`, Notion link, … |
|
|
255
|
+
| `workspaces` | the unit of work — branch, `worktree_path`, status, model, engine, `archived_at`, `favorited_at`, `tags`, Notion link, … |
|
|
255
256
|
| `tasks` | workspace sub-items — tasks and acceptance criteria |
|
|
256
257
|
| `agent_sessions` | agent runs — pid, `engine_session_id`, lifecycle |
|
|
257
258
|
| `ws_events` | persisted WebSocket events (chat history, `agent:event` stream, user messages) for replay on reconnect |
|
|
@@ -268,9 +269,10 @@ The MCP server reads and writes the same SQLite database as the main backend. Is
|
|
|
268
269
|
|
|
269
270
|
## Configuration
|
|
270
271
|
|
|
271
|
-
Kōbō reads settings from `~/.config/kobo/settings.json` (or falls back to defaults). Global settings
|
|
272
|
+
Kōbō reads settings from `~/.config/kobo/settings.json` (or falls back to defaults). Global settings define defaults, with per-project overrides for project-scoped fields:
|
|
272
273
|
|
|
273
274
|
- `defaultModel` — Claude model to use (e.g. `claude-opus-4-6`)
|
|
275
|
+
- `worktreesPath` — where new workspace worktrees are created. Defaults to `.worktrees`, resolved relative to the project. Absolute Linux/macOS paths, Windows paths (`C:\kobo\worktrees`, UNC shares), `$HOME/...`, `${HOME}/...`, `~/...`, and `%USERPROFILE%\...` are accepted. Paths containing parent-directory traversal (`..`) or drive-relative Windows syntax (`C:foo`) are rejected.
|
|
274
276
|
- `prPromptTemplate` — template rendered when opening a PR via the `/open-pr` endpoint; supports `{{pr_number}}`, `{{pr_url}}`, `{{branch_name}}`, `{{diff_stats}}`, `{{commits}}`, etc.
|
|
275
277
|
- `gitConventions` — markdown-formatted git conventions written to `.ai/.git-conventions.md` in every workspace so the agent follows them when committing
|
|
276
278
|
- `devServer` — per-project `startCommand` / `stopCommand` for launching workspace-scoped dev servers
|
|
@@ -287,6 +289,10 @@ This is a personal tool, but PRs and issues are welcome. Before submitting:
|
|
|
287
289
|
|
|
288
290
|
CI runs lint + type check + tests on every PR to `develop`.
|
|
289
291
|
|
|
292
|
+
## Release
|
|
293
|
+
|
|
294
|
+
Releases are cut from `main`. Bump `package.json` and `package-lock.json` on `develop`, merge `develop` into `main`, then push `main`. The release workflow builds, tests, publishes the current package version to npm, tags it as `v<version>`, and creates the GitHub Release. If the npm version or tag already exists, the workflow fails before publishing.
|
|
295
|
+
|
|
290
296
|
## License
|
|
291
297
|
|
|
292
298
|
GNU General Public License v3.0 or later. See [`LICENSE`](./LICENSE) for the full text.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
|
+
import { resolveWorkspaceWorktreePath } from '../server/utils/worktree-paths.js';
|
|
4
5
|
/** Allowed task status values. */
|
|
5
6
|
export const VALID_TASK_STATUSES = ['pending', 'in_progress', 'done'];
|
|
6
7
|
function rowToDto(row) {
|
|
@@ -163,7 +164,7 @@ export function getWorkspaceInfoHandler(db, workspaceId) {
|
|
|
163
164
|
projectPath: row.project_path,
|
|
164
165
|
sourceBranch: row.source_branch,
|
|
165
166
|
workingBranch: row.working_branch,
|
|
166
|
-
worktreePath: row.worktree_path ??
|
|
167
|
+
worktreePath: row.worktree_path ?? resolveWorkspaceWorktreePath(row.project_path, row.working_branch),
|
|
167
168
|
status: row.status,
|
|
168
169
|
model: row.model,
|
|
169
170
|
notionUrl: row.notion_url,
|
|
@@ -292,6 +292,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
292
292
|
description: 'CALL when you need to self-regulate on long missions — returns token/cost totals for the workspace lifetime and for the currently running agent_session. Useful before spawning heavy subagents or deep reasoning on already-expensive sessions.',
|
|
293
293
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
294
294
|
},
|
|
295
|
+
{
|
|
296
|
+
name: 'schedule_wakeup',
|
|
297
|
+
description: 'CALL to schedule a follow-up turn on THIS workspace after a delay. End the current turn normally; once it finishes and the workspace is idle, Kōbō waits `delaySeconds`, then resumes the same conversation by injecting `prompt` as the next user message. The wakeup is scoped to the current workspace and resumes its latest session — you cannot target another workspace or another session. If a turn is still active when the timer fires, the wakeup is skipped (status: `session-active`). Replaces any previously pending wakeup on this workspace. Delay is clamped to [60, 3600] seconds. Prefer this over the built-in `ScheduleWakeup` tool — it is the SDK-supported entry point.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
delaySeconds: {
|
|
302
|
+
type: 'number',
|
|
303
|
+
description: 'Seconds from now until the wakeup fires. Clamped to [60, 3600].',
|
|
304
|
+
},
|
|
305
|
+
prompt: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Prompt sent to the agent when the wakeup fires.',
|
|
308
|
+
},
|
|
309
|
+
reason: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'Short label shown to the user explaining the wakeup (optional).',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
required: ['delaySeconds', 'prompt'],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'cancel_wakeup',
|
|
319
|
+
description: 'CALL to cancel any pending wakeup on this workspace (e.g. the condition you were waiting on resolved early, or you decided not to continue). Idempotent — safe to call when nothing is pending.',
|
|
320
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
321
|
+
},
|
|
295
322
|
],
|
|
296
323
|
}));
|
|
297
324
|
/** Wrap a successful result as an MCP tool response with JSON text content. */
|
|
@@ -441,6 +468,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
441
468
|
if (name === 'get_session_usage') {
|
|
442
469
|
return ok(getSessionUsageHandler(db, workspaceId));
|
|
443
470
|
}
|
|
471
|
+
if (name === 'schedule_wakeup') {
|
|
472
|
+
const delaySeconds = a.delaySeconds;
|
|
473
|
+
const prompt = a.prompt;
|
|
474
|
+
if (typeof delaySeconds !== 'number' || !Number.isFinite(delaySeconds) || delaySeconds <= 0) {
|
|
475
|
+
return fail('delaySeconds must be a positive number');
|
|
476
|
+
}
|
|
477
|
+
if (typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
478
|
+
return fail('prompt is required');
|
|
479
|
+
}
|
|
480
|
+
const reason = a.reason;
|
|
481
|
+
if (reason !== undefined && typeof reason !== 'string') {
|
|
482
|
+
return fail('reason must be a string when provided');
|
|
483
|
+
}
|
|
484
|
+
const result = await backendRequest('POST', `/api/workspaces/${workspaceId}/pending-wakeup`, {
|
|
485
|
+
delaySeconds,
|
|
486
|
+
prompt,
|
|
487
|
+
reason,
|
|
488
|
+
});
|
|
489
|
+
return ok(result);
|
|
490
|
+
}
|
|
491
|
+
if (name === 'cancel_wakeup') {
|
|
492
|
+
const result = await backendRequest('DELETE', `/api/workspaces/${workspaceId}/pending-wakeup`);
|
|
493
|
+
return ok(result);
|
|
494
|
+
}
|
|
444
495
|
if (name === 'search_codebase') {
|
|
445
496
|
const query = a.query;
|
|
446
497
|
if (!query)
|
|
@@ -154,6 +154,46 @@ export const migrations = [
|
|
|
154
154
|
)`).run();
|
|
155
155
|
},
|
|
156
156
|
},
|
|
157
|
+
{
|
|
158
|
+
version: 17,
|
|
159
|
+
name: 'add-agent-permission-mode',
|
|
160
|
+
migrate: (db) => {
|
|
161
|
+
// Unifies the legacy `permission_mode` (auto-accept | plan) and
|
|
162
|
+
// `permission_profile` (bypass | strict | interactive) into a single
|
|
163
|
+
// SDK-aligned column with four values: plan | bypass | strict | interactive.
|
|
164
|
+
//
|
|
165
|
+
// Migration rule (preserves user-visible behaviour):
|
|
166
|
+
// permission_mode='plan' → 'plan'
|
|
167
|
+
// permission_mode='auto-accept' + permission_profile=* → permission_profile (default 'bypass')
|
|
168
|
+
//
|
|
169
|
+
// The two legacy columns are kept for backward compatibility — they are
|
|
170
|
+
// no longer the source of truth but stay readable so older code paths
|
|
171
|
+
// (or in-flight requests during deploy) don't crash.
|
|
172
|
+
db.transaction(() => {
|
|
173
|
+
db.prepare("ALTER TABLE workspaces ADD COLUMN agent_permission_mode TEXT NOT NULL DEFAULT 'bypass'").run();
|
|
174
|
+
// Plan mode is preserved verbatim.
|
|
175
|
+
db.prepare("UPDATE workspaces SET agent_permission_mode = 'plan' WHERE permission_mode = 'plan'").run();
|
|
176
|
+
// For 'auto-accept' rows, promote the profile (or fall back to bypass).
|
|
177
|
+
db.prepare(`UPDATE workspaces
|
|
178
|
+
SET agent_permission_mode = CASE
|
|
179
|
+
WHEN permission_profile IN ('bypass', 'strict', 'interactive') THEN permission_profile
|
|
180
|
+
ELSE 'bypass'
|
|
181
|
+
END
|
|
182
|
+
WHERE permission_mode != 'plan'`).run();
|
|
183
|
+
})();
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
version: 18,
|
|
188
|
+
name: 'add-pending-wakeup-agent-session-id',
|
|
189
|
+
migrate: (db) => {
|
|
190
|
+
// Pin a wakeup to the session that scheduled it, so the wakeup resumes
|
|
191
|
+
// that conversation instead of whichever session happens to be the
|
|
192
|
+
// latest at fire time. Nullable: pre-migration rows fall back to the
|
|
193
|
+
// legacy "last session" behaviour.
|
|
194
|
+
db.prepare('ALTER TABLE pending_wakeups ADD COLUMN agent_session_id TEXT').run();
|
|
195
|
+
},
|
|
196
|
+
},
|
|
157
197
|
];
|
|
158
198
|
/** Current schema version — always equals the highest migration version. */
|
|
159
199
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -26,6 +26,7 @@ export function initSchema(db) {
|
|
|
26
26
|
auto_loop_ready INTEGER NOT NULL DEFAULT 0,
|
|
27
27
|
no_progress_streak INTEGER NOT NULL DEFAULT 0,
|
|
28
28
|
permission_profile TEXT NOT NULL DEFAULT 'bypass',
|
|
29
|
+
agent_permission_mode TEXT NOT NULL DEFAULT 'bypass',
|
|
29
30
|
created_at TEXT NOT NULL,
|
|
30
31
|
updated_at TEXT NOT NULL
|
|
31
32
|
);
|
|
@@ -62,11 +63,12 @@ export function initSchema(db) {
|
|
|
62
63
|
);
|
|
63
64
|
|
|
64
65
|
CREATE TABLE IF NOT EXISTS pending_wakeups (
|
|
65
|
-
workspace_id
|
|
66
|
-
target_at
|
|
67
|
-
prompt
|
|
68
|
-
reason
|
|
69
|
-
created_at
|
|
66
|
+
workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
67
|
+
target_at TEXT NOT NULL,
|
|
68
|
+
prompt TEXT NOT NULL,
|
|
69
|
+
reason TEXT,
|
|
70
|
+
created_at TEXT NOT NULL,
|
|
71
|
+
agent_session_id TEXT
|
|
70
72
|
);
|
|
71
73
|
|
|
72
74
|
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
package/dist/server/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { serve } from '@hono/node-server';
|
|
@@ -34,14 +33,6 @@ import { emit, emitEphemeral, handleConnection, setMessageHandler } from './serv
|
|
|
34
33
|
import { getActiveSession, getWorkspace, updateWorkspaceStatus } from './services/workspace-service.js';
|
|
35
34
|
import { getClientSpaPath, getDbPath, getKoboHome, getPackageVersion } from './utils/paths.js';
|
|
36
35
|
import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/process-tracker.js';
|
|
37
|
-
// Runtime prerequisite check — warn if the claude CLI is missing. Don't block
|
|
38
|
-
// startup: the user may still want to configure settings or browse workspaces.
|
|
39
|
-
{
|
|
40
|
-
const check = spawnSync('claude', ['--version'], { stdio: 'ignore' });
|
|
41
|
-
if (check.error && check.error.code === 'ENOENT') {
|
|
42
|
-
console.warn("[kobo] WARNING: 'claude' CLI not found on PATH. Kōbō will fail to spawn agents until Claude Code is installed. See https://claude.com/claude-code");
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
36
|
console.log(`[kobo] Kōbō home: ${getKoboHome()}`);
|
|
46
37
|
// Initialize DB + run migrations
|
|
47
38
|
const db = getDb();
|
|
@@ -250,6 +241,16 @@ setMessageHandler((type, payload) => {
|
|
|
250
241
|
});
|
|
251
242
|
return;
|
|
252
243
|
}
|
|
244
|
+
// Reject chat input while paused on canUseTool — sending here would spawn
|
|
245
|
+
// a parallel session and orphan the pending callback.
|
|
246
|
+
const wsRow = getWorkspace(p.workspaceId);
|
|
247
|
+
if (wsRow?.status === 'awaiting-user') {
|
|
248
|
+
emitEphemeral(p.workspaceId, 'chat:rejected', {
|
|
249
|
+
reason: 'awaiting-user',
|
|
250
|
+
message: 'Answer via the question panel — typing in chat would orphan the pending callback',
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
253
254
|
// Prefer the session explicitly selected by the client (sessionId hint),
|
|
254
255
|
// falling back to the running/most-recent non-idle session so idle sessions
|
|
255
256
|
// never steal the tagging.
|
|
@@ -278,7 +279,7 @@ setMessageHandler((type, payload) => {
|
|
|
278
279
|
// Plan mode blocks MCP tools — when the caller knows the message
|
|
279
280
|
// requires them (e.g. grooming), it sets the override to bypass the
|
|
280
281
|
// workspace default for this spawn only.
|
|
281
|
-
const effectiveMode = p.
|
|
282
|
+
const effectiveMode = p.agentPermissionModeOverride ?? workspace.agentPermissionMode;
|
|
282
283
|
startAgent(p.workspaceId, worktreePath, p.content, workspace.model, true, effectiveMode, p.sessionId, workspace.reasoningEffort);
|
|
283
284
|
updateWorkspaceStatus(p.workspaceId, 'executing');
|
|
284
285
|
}
|
|
@@ -297,7 +298,7 @@ setMessageHandler((type, payload) => {
|
|
|
297
298
|
}
|
|
298
299
|
const worktreePath = workspace.worktreePath;
|
|
299
300
|
const prompt = p.prompt ?? 'Continue the previous task where you left off.';
|
|
300
|
-
startAgent(p.workspaceId, worktreePath, prompt, workspace.model, false, workspace.
|
|
301
|
+
startAgent(p.workspaceId, worktreePath, prompt, workspace.model, false, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
|
|
301
302
|
}
|
|
302
303
|
catch (err) {
|
|
303
304
|
console.error('[ws] Failed to start agent:', err);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
3
|
import { Hono } from 'hono';
|
|
5
4
|
import { getDb } from '../db/index.js';
|
|
6
5
|
import { SCHEMA_VERSION } from '../db/migrations.js';
|
|
7
6
|
import { getGlobalSettings, SETTINGS_SCHEMA_VERSION } from '../services/settings-service.js';
|
|
8
7
|
import { getDbPath, getKoboHome } from '../utils/paths.js';
|
|
8
|
+
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
9
9
|
const app = new Hono();
|
|
10
10
|
function checkClaudeCli() {
|
|
11
11
|
try {
|
|
@@ -51,7 +51,7 @@ app.get('/report', (c) => {
|
|
|
51
51
|
for (const ws of workspaces) {
|
|
52
52
|
if (ws.archived_at)
|
|
53
53
|
continue;
|
|
54
|
-
const wtPath = ws.worktree_path ??
|
|
54
|
+
const wtPath = ws.worktree_path ?? resolveWorkspaceWorktreePath(ws.project_path, ws.working_branch);
|
|
55
55
|
if (!fs.existsSync(wtPath)) {
|
|
56
56
|
worktreesMissing.push({ workspaceId: ws.id, name: ws.name, path: wtPath, exists: false });
|
|
57
57
|
}
|
|
@@ -45,7 +45,8 @@ app.put('/global', async (c) => {
|
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
47
|
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
-
|
|
48
|
+
const status = err instanceof Error && err.name === 'InvalidWorktreesPathError' ? 400 : 500;
|
|
49
|
+
return c.json({ error: message }, status);
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
52
|
// GET /api/settings/projects — list all projects
|
|
@@ -23,10 +23,34 @@ import * as wsService from '../services/websocket-service.js';
|
|
|
23
23
|
import * as workspaceService from '../services/workspace-service.js';
|
|
24
24
|
import * as worktreeService from '../services/worktree-service.js';
|
|
25
25
|
import * as gitOps from '../utils/git-ops.js';
|
|
26
|
+
import { resolveSiblingWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
26
27
|
/** Hono sub-router for workspace CRUD, tasks, agent lifecycle, git operations, and PR creation. */
|
|
27
28
|
const app = new Hono();
|
|
28
29
|
/** Tracks workspaces currently running a setup script to prevent concurrent executions. */
|
|
29
30
|
const setupScriptRunning = new Set();
|
|
31
|
+
const VALID_AGENT_PERMISSION_MODES = ['plan', 'bypass', 'strict', 'interactive'];
|
|
32
|
+
function isAgentPermissionMode(value) {
|
|
33
|
+
return typeof value === 'string' && VALID_AGENT_PERMISSION_MODES.includes(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the unified permission mode for a new workspace.
|
|
37
|
+
*
|
|
38
|
+
* Cascade: explicit body field → global default (validated) → 'bypass'.
|
|
39
|
+
*
|
|
40
|
+
* Legacy `defaultPermissionMode` values ('plan' / 'auto-accept') are honored:
|
|
41
|
+
* 'plan' stays 'plan'; 'auto-accept' falls through to 'bypass' (the safest
|
|
42
|
+
* non-plan default — matches the pre-refactor "skip prompts" behaviour).
|
|
43
|
+
*/
|
|
44
|
+
function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSettings) {
|
|
45
|
+
if (isAgentPermissionMode(bodyValue))
|
|
46
|
+
return bodyValue;
|
|
47
|
+
const global = globalSettings.defaultPermissionMode;
|
|
48
|
+
if (isAgentPermissionMode(global))
|
|
49
|
+
return global;
|
|
50
|
+
if (global === 'plan')
|
|
51
|
+
return 'plan';
|
|
52
|
+
return 'bypass';
|
|
53
|
+
}
|
|
30
54
|
app.get('/', (c) => {
|
|
31
55
|
try {
|
|
32
56
|
const workspaces = workspaceService.listWorkspaces();
|
|
@@ -172,8 +196,9 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
172
196
|
...(useReusedWorktree ? { worktreePath: body.worktreePath, worktreeOwned: false } : {}),
|
|
173
197
|
model: body.model,
|
|
174
198
|
reasoningEffort: body.reasoningEffort,
|
|
175
|
-
|
|
199
|
+
agentPermissionMode: resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, globalSettings),
|
|
176
200
|
engine: body.engine,
|
|
201
|
+
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
177
202
|
});
|
|
178
203
|
// Auto-tag the workspace based on its creation source — `notion` when
|
|
179
204
|
// imported from a Notion page, `sentry` when bootstrapped from a Sentry
|
|
@@ -264,7 +289,7 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
264
289
|
}
|
|
265
290
|
else {
|
|
266
291
|
try {
|
|
267
|
-
worktreePath = worktreeService.createWorktree(body.projectPath, workingBranch, body.sourceBranch);
|
|
292
|
+
worktreePath = worktreeService.createWorktree(body.projectPath, workingBranch, body.sourceBranch, globalSettings.worktreesPath);
|
|
268
293
|
}
|
|
269
294
|
catch (err) {
|
|
270
295
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -544,7 +569,7 @@ ${AUTO_LOOP_HARD_RULES}`;
|
|
|
544
569
|
brainstormPrompt += `\n\nThen brainstorm the implementation approach. Explore the codebase to understand the existing structure. Ask clarifying questions if needed. When you're done brainstorming and have a clear plan, create a plan file and proceed with implementation. Once you have completed the brainstorming phase, output [BRAINSTORM_COMPLETE] on its own line.`;
|
|
545
570
|
}
|
|
546
571
|
try {
|
|
547
|
-
const agent = agentManager.startAgent(workspace.id, worktreePath, brainstormPrompt, workspace.model, false, workspace.
|
|
572
|
+
const agent = agentManager.startAgent(workspace.id, worktreePath, brainstormPrompt, workspace.model, false, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
|
|
548
573
|
// Persist the initial prompt in the feed so it's visible in the chat,
|
|
549
574
|
// tagged with the freshly created session id so the strict session filter shows it.
|
|
550
575
|
wsService.emit(workspace.id, 'user:message', { content: brainstormPrompt, sender: 'system-prompt' }, agent.agentSessionId);
|
|
@@ -723,7 +748,8 @@ app.get('/:id/pending-wakeup', (c) => {
|
|
|
723
748
|
return c.json({ error: message }, 500);
|
|
724
749
|
}
|
|
725
750
|
});
|
|
726
|
-
// DELETE /api/workspaces/:id/pending-wakeup — user-initiated cancel ("×" button)
|
|
751
|
+
// DELETE /api/workspaces/:id/pending-wakeup — user-initiated cancel ("×" button)
|
|
752
|
+
// or agent-initiated cancel via the `kobo__cancel_wakeup` MCP tool.
|
|
727
753
|
app.delete('/:id/pending-wakeup', (c) => {
|
|
728
754
|
try {
|
|
729
755
|
const id = c.req.param('id');
|
|
@@ -735,6 +761,41 @@ app.delete('/:id/pending-wakeup', (c) => {
|
|
|
735
761
|
return c.json({ error: message }, 500);
|
|
736
762
|
}
|
|
737
763
|
});
|
|
764
|
+
// POST /api/workspaces/:id/pending-wakeup — agent-initiated schedule via the
|
|
765
|
+
// `kobo__schedule_wakeup` MCP tool. Replaces any existing pending wakeup.
|
|
766
|
+
app.post('/:id/pending-wakeup', async (c) => {
|
|
767
|
+
try {
|
|
768
|
+
const id = c.req.param('id');
|
|
769
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
770
|
+
const delaySeconds = body.delaySeconds;
|
|
771
|
+
const prompt = body.prompt;
|
|
772
|
+
const reason = body.reason;
|
|
773
|
+
if (typeof delaySeconds !== 'number' || !Number.isFinite(delaySeconds) || delaySeconds <= 0) {
|
|
774
|
+
return c.json({ error: 'delaySeconds must be a positive number' }, 400);
|
|
775
|
+
}
|
|
776
|
+
if (typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
777
|
+
return c.json({ error: 'prompt is required' }, 400);
|
|
778
|
+
}
|
|
779
|
+
if (reason !== undefined && typeof reason !== 'string') {
|
|
780
|
+
return c.json({ error: 'reason must be a string when provided' }, 400);
|
|
781
|
+
}
|
|
782
|
+
// Pin the wakeup to the session that scheduled it, so the resume targets
|
|
783
|
+
// that conversation instead of whichever session happens to be the latest
|
|
784
|
+
// at fire time. The MCP tool is invoked from inside an active session, so
|
|
785
|
+
// a missing controller signals misuse — reject explicitly.
|
|
786
|
+
const agentSessionId = agentManager.getActiveSessionId(id);
|
|
787
|
+
if (!agentSessionId) {
|
|
788
|
+
return c.json({ error: 'no active agent session for this workspace' }, 409);
|
|
789
|
+
}
|
|
790
|
+
wakeupService.schedule(id, delaySeconds, prompt, reason, agentSessionId);
|
|
791
|
+
const pending = wakeupService.getPending(id);
|
|
792
|
+
return c.json({ ok: true, pending });
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
796
|
+
return c.json({ error: message }, 500);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
738
799
|
// PATCH /api/workspaces/:id/sessions/:sessionId — rename a session
|
|
739
800
|
app.patch('/:id/sessions/:sessionId', async (c) => {
|
|
740
801
|
try {
|
|
@@ -759,6 +820,94 @@ app.patch('/:id/sessions/:sessionId', async (c) => {
|
|
|
759
820
|
return c.json({ error: message }, 500);
|
|
760
821
|
}
|
|
761
822
|
});
|
|
823
|
+
// POST /api/workspaces/:id/deferred-tool-use/answer — resume a deferred
|
|
824
|
+
// AskUserQuestion call by feeding the user's answers back to the SDK.
|
|
825
|
+
app.post('/:id/deferred-tool-use/answer', async (c) => {
|
|
826
|
+
try {
|
|
827
|
+
const id = c.req.param('id');
|
|
828
|
+
const body = await c.req
|
|
829
|
+
.json()
|
|
830
|
+
.catch(() => ({}));
|
|
831
|
+
if (!body?.answers || typeof body.answers !== 'object') {
|
|
832
|
+
return c.json({ error: 'answers payload required' }, 400);
|
|
833
|
+
}
|
|
834
|
+
await agentManager.answerPendingQuestion(id, body.answers, body.toolCallId);
|
|
835
|
+
return c.json({ ok: true });
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
const message = err instanceof Error ? err.message : 'unknown';
|
|
839
|
+
// "No deferred tool use pending" is a benign race — the frontend
|
|
840
|
+
// self-heals on this string. Use 409 (Conflict) so dev tools don't
|
|
841
|
+
// surface it as a 400 validation failure.
|
|
842
|
+
const status = /no deferred tool use pending/i.test(message) ? 409 : 400;
|
|
843
|
+
return c.json({ error: message }, status);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
// POST /api/workspaces/:id/deferred-tool-use/cancel — cancel a deferred
|
|
847
|
+
// AskUserQuestion. The SDK callback resolves with deny + a message; the
|
|
848
|
+
// agent sees an error tool_result and adapts. Does NOT abort the session.
|
|
849
|
+
app.post('/:id/deferred-tool-use/cancel', async (c) => {
|
|
850
|
+
try {
|
|
851
|
+
const id = c.req.param('id');
|
|
852
|
+
const body = await c.req
|
|
853
|
+
.json()
|
|
854
|
+
.catch(() => ({}));
|
|
855
|
+
await agentManager.cancelPendingQuestion(id, body.reason, body.toolCallId);
|
|
856
|
+
return c.json({ ok: true });
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
const message = err instanceof Error ? err.message : 'unknown';
|
|
860
|
+
const status = /no deferred tool use pending/i.test(message) ? 409 : 400;
|
|
861
|
+
return c.json({ error: message }, status);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
// POST /api/workspaces/:id/deferred-permission/decision — resume a deferred
|
|
865
|
+
// interactive permission request with the user's allow/deny decision.
|
|
866
|
+
app.post('/:id/deferred-permission/decision', async (c) => {
|
|
867
|
+
try {
|
|
868
|
+
const id = c.req.param('id');
|
|
869
|
+
const body = await c.req
|
|
870
|
+
.json()
|
|
871
|
+
.catch(() => ({}));
|
|
872
|
+
if (!body?.toolCallId || typeof body.toolCallId !== 'string') {
|
|
873
|
+
return c.json({ error: 'toolCallId required' }, 400);
|
|
874
|
+
}
|
|
875
|
+
if (body.decision !== 'allow' && body.decision !== 'deny') {
|
|
876
|
+
return c.json({ error: "decision must be 'allow' or 'deny'" }, 400);
|
|
877
|
+
}
|
|
878
|
+
await agentManager.answerPendingPermission(id, {
|
|
879
|
+
toolCallId: body.toolCallId,
|
|
880
|
+
decision: body.decision,
|
|
881
|
+
reason: typeof body.reason === 'string' ? body.reason : undefined,
|
|
882
|
+
});
|
|
883
|
+
return c.json({ ok: true });
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
const message = err instanceof Error ? err.message : 'unknown';
|
|
887
|
+
const status = /no deferred tool use pending/i.test(message) ? 409 : 400;
|
|
888
|
+
return c.json({ error: message }, status);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
// DELETE /api/workspaces/:id/events/:eventId — permanently dismiss a single
|
|
892
|
+
// persisted ws_events row (used today by the agent error banner so a closed
|
|
893
|
+
// error doesn't replay on F5 / reconnect). Defensive: only deletes if the row
|
|
894
|
+
// exists in this workspace; idempotent on missing row (returns 200).
|
|
895
|
+
app.delete('/:id/events/:eventId', (c) => {
|
|
896
|
+
try {
|
|
897
|
+
const workspaceId = c.req.param('id');
|
|
898
|
+
const eventId = c.req.param('eventId');
|
|
899
|
+
if (!workspaceService.getWorkspace(workspaceId)) {
|
|
900
|
+
return c.json({ error: `Workspace '${workspaceId}' not found` }, 404);
|
|
901
|
+
}
|
|
902
|
+
const db = getDb();
|
|
903
|
+
db.prepare('DELETE FROM ws_events WHERE id = ? AND workspace_id = ?').run(eventId, workspaceId);
|
|
904
|
+
return c.json({ ok: true });
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
const message = err instanceof Error ? err.message : 'unknown';
|
|
908
|
+
return c.json({ error: message }, 500);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
762
911
|
// POST /api/workspaces/:id/tasks — create a new task
|
|
763
912
|
app.post('/:id/tasks', async (c) => {
|
|
764
913
|
try {
|
|
@@ -1065,7 +1214,7 @@ app.put('/:id/tags', async (c) => {
|
|
|
1065
1214
|
return c.json({ error: msg }, status);
|
|
1066
1215
|
}
|
|
1067
1216
|
});
|
|
1068
|
-
// PATCH /api/workspaces/:id — update workspace fields (status, model,
|
|
1217
|
+
// PATCH /api/workspaces/:id — update workspace fields (status, model, agentPermissionMode, name)
|
|
1069
1218
|
app.patch('/:id', migrationGuard, async (c) => {
|
|
1070
1219
|
try {
|
|
1071
1220
|
const id = c.req.param('id');
|
|
@@ -1081,18 +1230,11 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
1081
1230
|
if (body.reasoningEffort !== undefined) {
|
|
1082
1231
|
updated = workspaceService.updateWorkspaceReasoningEffort(id, body.reasoningEffort);
|
|
1083
1232
|
}
|
|
1084
|
-
if (body.
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
return c.json({ error: `Invalid permission mode. Must be one of: ${validModes.join(', ')}` }, 400);
|
|
1088
|
-
}
|
|
1089
|
-
updated = workspaceService.updateWorkspacePermissionMode(id, body.permissionMode);
|
|
1090
|
-
}
|
|
1091
|
-
if (body.permissionProfile !== undefined) {
|
|
1092
|
-
if (body.permissionProfile !== 'bypass' && body.permissionProfile !== 'strict') {
|
|
1093
|
-
return c.json({ error: "Invalid permission profile. Must be 'bypass' or 'strict'." }, 400);
|
|
1233
|
+
if (body.agentPermissionMode !== undefined) {
|
|
1234
|
+
if (!isAgentPermissionMode(body.agentPermissionMode)) {
|
|
1235
|
+
return c.json({ error: `Invalid agentPermissionMode. Must be one of: ${VALID_AGENT_PERMISSION_MODES.join(', ')}` }, 400);
|
|
1094
1236
|
}
|
|
1095
|
-
updated = workspaceService.
|
|
1237
|
+
updated = workspaceService.updateAgentPermissionMode(id, body.agentPermissionMode);
|
|
1096
1238
|
}
|
|
1097
1239
|
if (body.status) {
|
|
1098
1240
|
updated = workspaceService.updateWorkspaceStatus(id, body.status);
|
|
@@ -1103,10 +1245,9 @@ app.patch('/:id', migrationGuard, async (c) => {
|
|
|
1103
1245
|
if (!body.status &&
|
|
1104
1246
|
body.model === undefined &&
|
|
1105
1247
|
body.reasoningEffort === undefined &&
|
|
1106
|
-
body.
|
|
1107
|
-
body.permissionProfile === undefined &&
|
|
1248
|
+
body.agentPermissionMode === undefined &&
|
|
1108
1249
|
body.name === undefined) {
|
|
1109
|
-
return c.json({ error: 'Missing field: status, model, reasoningEffort,
|
|
1250
|
+
return c.json({ error: 'Missing field: status, model, reasoningEffort, agentPermissionMode, or name' }, 400);
|
|
1110
1251
|
}
|
|
1111
1252
|
return c.json(updated);
|
|
1112
1253
|
}
|
|
@@ -1394,7 +1535,7 @@ app.post('/:id/start', migrationGuard, async (c) => {
|
|
|
1394
1535
|
// Agent may not be running — ignore
|
|
1395
1536
|
}
|
|
1396
1537
|
const worktreePath = workspace.worktreePath;
|
|
1397
|
-
const agent = agentManager.startAgent(id, worktreePath, prompt, workspace.model, resume, workspace.
|
|
1538
|
+
const agent = agentManager.startAgent(id, worktreePath, prompt, workspace.model, resume, workspace.agentPermissionMode, agentSessionId, workspace.reasoningEffort);
|
|
1398
1539
|
workspaceService.updateWorkspaceStatus(id, 'executing');
|
|
1399
1540
|
// Persist the user prompt so it survives page refresh.
|
|
1400
1541
|
// When agentSessionId is provided (idle-session flow), the prompt was typed
|
|
@@ -1586,11 +1727,7 @@ app.post('/:id/rename-branch', async (c) => {
|
|
|
1586
1727
|
// Sibling rename: keep the same worktrees-root, swap the branch leaf.
|
|
1587
1728
|
// Cannot use `path.dirname` directly because branches with slashes
|
|
1588
1729
|
// (e.g. `feature/x`) make the dirname end one level too deep.
|
|
1589
|
-
const
|
|
1590
|
-
const worktreesRoot = oldWorktreePath.endsWith(oldSuffix)
|
|
1591
|
-
? oldWorktreePath.slice(0, -oldSuffix.length)
|
|
1592
|
-
: path.join(workspace.projectPath, '.worktrees');
|
|
1593
|
-
const newWorktreePath = path.join(worktreesRoot, newName);
|
|
1730
|
+
const newWorktreePath = resolveSiblingWorkspaceWorktreePath(workspace.projectPath, oldWorktreePath, workspace.workingBranch, newName);
|
|
1594
1731
|
// Reject early if the target name is already in use — either as a local
|
|
1595
1732
|
// branch or on origin. Avoids git's generic "already exists" error and
|
|
1596
1733
|
// protects against the same silent-fallback trap the create flow has.
|
|
@@ -1660,11 +1797,7 @@ app.post('/:id/resync-branch', (c) => {
|
|
|
1660
1797
|
// if the move fails (dir already moved, lockfile, dirty tree), we still
|
|
1661
1798
|
// update the DB so git ops stay aligned with the current ref name — the
|
|
1662
1799
|
// user can repair the dir manually.
|
|
1663
|
-
const
|
|
1664
|
-
const worktreesRoot = worktreePath.endsWith(oldSuffix)
|
|
1665
|
-
? worktreePath.slice(0, -oldSuffix.length)
|
|
1666
|
-
: path.join(workspace.projectPath, '.worktrees');
|
|
1667
|
-
const newWorktreePath = path.join(worktreesRoot, actual);
|
|
1800
|
+
const newWorktreePath = resolveSiblingWorkspaceWorktreePath(workspace.projectPath, worktreePath, workspace.workingBranch, actual);
|
|
1668
1801
|
try {
|
|
1669
1802
|
gitOps.moveWorktree(workspace.projectPath, worktreePath, newWorktreePath);
|
|
1670
1803
|
workspaceService.updateWorktreePath(id, newWorktreePath);
|
|
@@ -1850,7 +1983,7 @@ Start now.`;
|
|
|
1850
1983
|
}
|
|
1851
1984
|
catch {
|
|
1852
1985
|
try {
|
|
1853
|
-
agentManager.startAgent(workspace.id, worktreePath, prompt, workspace.model, true, workspace.
|
|
1986
|
+
agentManager.startAgent(workspace.id, worktreePath, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
|
|
1854
1987
|
workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
|
|
1855
1988
|
messageSent = true;
|
|
1856
1989
|
}
|
|
@@ -1989,7 +2122,7 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
1989
2122
|
// Agent not running — resume it with the PR prompt
|
|
1990
2123
|
try {
|
|
1991
2124
|
const worktreePathForResume = workspace.worktreePath;
|
|
1992
|
-
agentManager.startAgent(workspace.id, worktreePathForResume, rendered, workspace.model, true, workspace.
|
|
2125
|
+
agentManager.startAgent(workspace.id, worktreePathForResume, rendered, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
|
|
1993
2126
|
workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
|
|
1994
2127
|
messageSent = true;
|
|
1995
2128
|
}
|
|
@@ -10,7 +10,7 @@ export const CLAUDE_CODE_CAPABILITIES = {
|
|
|
10
10
|
{ id: 'medium', label: 'Medium' },
|
|
11
11
|
{ id: 'high', label: 'High' },
|
|
12
12
|
],
|
|
13
|
-
permissionModes: ['
|
|
13
|
+
permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
|
|
14
14
|
supportsResume: true,
|
|
15
15
|
supportsMcp: true,
|
|
16
16
|
supportsSkills: true,
|