@loicngr/kobo 1.7.34 → 1.8.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 +53 -52
- package/CHANGELOG.md +12 -1
- package/README.md +59 -37
- package/dist/mcp-server/kobo-tasks-server.js +4 -0
- package/dist/server/index.js +39 -1
- package/dist/server/middleware/network-auth-middleware.js +27 -0
- package/dist/server/routes/changelog.js +1 -1
- package/dist/server/routes/settings.js +49 -0
- package/dist/server/routes/workspaces.js +28 -9
- package/dist/server/services/agent/engines/claude-code/engine.js +37 -4
- package/dist/server/services/agent/engines/claude-code/stop-hook.js +56 -0
- package/dist/server/services/agent/orchestrator.js +4 -0
- package/dist/server/services/cron-service.js +12 -6
- package/dist/server/services/network-access-service.js +67 -0
- package/dist/server/services/settings-service.js +50 -1
- package/dist/server/services/templates-service.js +80 -11
- package/dist/server/services/wakeup-service.js +9 -0
- package/dist/server/services/worktree-purge-service.js +2 -2
- package/dist/server/services/worktree-service.js +64 -2
- package/package.json +7 -7
- package/src/client/dist/spa/assets/ActivityFeed-qE7kgNNI.js +8 -0
- package/src/client/dist/spa/assets/ChangelogPage-P-cSkc52.js +1 -0
- package/src/client/dist/spa/assets/ClosePopup-BWi4AXm0.js +1 -0
- package/src/client/dist/spa/assets/CreatePage-CdeIZxPs.js +2 -0
- package/src/client/dist/spa/assets/DiffViewer-aEMR55x8.js +8 -0
- package/src/client/dist/spa/assets/HealthPage-Oq6n1qu0.js +1 -0
- package/src/client/dist/spa/assets/{MainLayout-DtTxmFXf.css → MainLayout-DXz5Vnxf.css} +1 -1
- package/src/client/dist/spa/assets/MainLayout-y1VMkOwI.js +37 -0
- package/src/client/dist/spa/assets/{QBadge-CIC5n8w7.js → QBadge-DoPfOZ_x.js} +1 -1
- package/src/client/dist/spa/assets/{QBanner-BxBEdhfp.js → QBanner-C5hABcB2.js} +1 -1
- package/src/client/dist/spa/assets/QChip-xeL4Nxy_.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-QIQTvw7U.js +1 -0
- package/src/client/dist/spa/assets/{QIcon-C6C3QeM4.js → QIcon-CfOEsLsF.js} +1 -1
- package/src/client/dist/spa/assets/QInput-7G0OXYP-.js +1 -0
- package/src/client/dist/spa/assets/{QList-DRW_oyZ4.js → QList-kGlJAb-p.js} +1 -1
- package/src/client/dist/spa/assets/{QPage-C6xc9fOe.js → QPage-B-ixPPKx.js} +1 -1
- package/src/client/dist/spa/assets/QScrollArea-cWkysQ9Q.js +1 -0
- package/src/client/dist/spa/assets/QSelect-PP69fnCX.js +36 -0
- package/src/client/dist/spa/assets/{use-id-By86THzm.js → QSeparator-Vt0kN1I8.js} +1 -1
- package/src/client/dist/spa/assets/QSpace-e68xsYE1.js +1 -0
- package/src/client/dist/spa/assets/{QSpinnerDots-atHe_AUn.js → QSpinnerDots-BOryc_9y.js} +1 -1
- package/src/client/dist/spa/assets/QTooltip-Brh5sChu.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-BcXygFyE.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-Co97HPUU.js +16 -0
- package/src/client/dist/spa/assets/SettingsPage-DfYhuzv0.css +1 -0
- package/src/client/dist/spa/assets/TouchPan-BfqX_pZK.js +1 -0
- package/src/client/dist/spa/assets/{WorkspacePage-36QGRRCt.css → WorkspacePage-Bo6aKwl_.css} +1 -1
- package/src/client/dist/spa/assets/WorkspacePage-e-ktRS-M.js +4 -0
- package/src/client/dist/spa/assets/build-path-tree-D7rqJH7g.js +1 -0
- package/src/client/dist/spa/assets/chunk-QTnfLwEv.js +1 -0
- package/src/client/dist/spa/assets/{css.worker-BtW9exzf.js → css.worker-CBlhdqTa.js} +31 -31
- package/src/client/dist/spa/assets/{cssMode-CdNIff6x.js → cssMode-DwJTukMi.js} +1 -1
- package/src/client/dist/spa/assets/documents-eaoEREpp.js +1 -0
- package/src/client/dist/spa/assets/{editor.api2-CEAFHkwY.js → editor.api2-CCQ74UFa.js} +163 -163
- package/src/client/dist/spa/assets/{editor.main-DuQa2C4S.js → editor.main-sdLzVGjZ.js} +2 -2
- package/src/client/dist/spa/assets/editor.worker-Cu3tR8iJ.js +26 -0
- package/src/client/dist/spa/assets/expand-template-DTSWVfFm.js +1 -0
- package/src/client/dist/spa/assets/formatters-DnqeH9c3.js +1 -0
- package/src/client/dist/spa/assets/{freemarker2-DXz2Sr_a.js → freemarker2-DnJdVbFY.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-B6DHJ8jd.js → handlebars-CQ9FsB3q.js} +1 -1
- package/src/client/dist/spa/assets/{html-_UvZHIUo.js → html-DbwSidb2.js} +1 -1
- package/src/client/dist/spa/assets/{html.worker-Dg1SpGQ4.js → html.worker-BKBrNna1.js} +24 -24
- package/src/client/dist/spa/assets/{htmlMode-DZ0ixGc6.js → htmlMode-TsbX8xv5.js} +1 -1
- package/src/client/dist/spa/assets/i18n-CrwtYRcs.js +1 -0
- package/src/client/dist/spa/assets/index-UPQqj74q.js +84 -0
- package/src/client/dist/spa/assets/{javascript-C6Gf4Ys2.js → javascript-CdAxNn6v.js} +1 -1
- package/src/client/dist/spa/assets/json.worker-BaLYt9Ss.js +58 -0
- package/src/client/dist/spa/assets/{jsonMode-aE8EPidy.js → jsonMode-DsX17jzq.js} +1 -1
- package/src/client/dist/spa/assets/kobo-commands-Cu30N0Ht.js +9 -0
- package/src/client/dist/spa/assets/layout-DOF1L6Vf.js +1 -0
- package/src/client/dist/spa/assets/{liquid-CSp27lUC.js → liquid-Bc7-UBG5.js} +1 -1
- package/src/client/dist/spa/assets/{lspLanguageFeatures-UxO-LpRp.js → lspLanguageFeatures-yZgUujPG.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BSCEgQSy.js → mdx-BvDiub4z.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-DqqsS1kc.js → monaco.contribution-CFTqxJMP.js} +2 -2
- package/src/client/dist/spa/assets/network-auth-DtdN0iy_.js +1 -0
- package/src/client/dist/spa/assets/{permissionModes-CJN6Olox.js → permissionModes-CUZkcBev.js} +1 -1
- package/src/client/dist/spa/assets/{python-CA5lSk1U.js → python-DZZeQhwZ.js} +1 -1
- package/src/client/dist/spa/assets/{razor-BznM4hXD.js → razor-RaxEa4MG.js} +1 -1
- package/src/client/dist/spa/assets/render-chat-markdown-D1XyjSW1.js +66 -0
- package/src/client/dist/spa/assets/{ts.worker-DI5g4t5j.js → ts.worker-PmaSgaZk.js} +185 -170
- package/src/client/dist/spa/assets/{tsMode-NbGeOvoN.js → tsMode-C6gZAb22.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-kHDph5jb.js → typescript-BCAqEI1V.js} +1 -1
- package/src/client/dist/spa/assets/{use-checkbox-BnkSQgTJ.js → use-checkbox-DE50asz4.js} +1 -1
- package/src/client/dist/spa/assets/{use-onboarding-C4tGLHsr.js → use-onboarding-gBmXW7wm.js} +2 -2
- package/src/client/dist/spa/assets/use-quasar-Dyujo9Ue.js +1 -0
- package/src/client/dist/spa/assets/{vue.runtime.esm-bundler-BAtKyT0Y.js → vue.runtime.esm-bundler-JZnIeD9D.js} +2 -2
- package/src/client/dist/spa/assets/{workers-DFjmWZva.js → workers-okv2EabB.js} +1 -1
- package/src/client/dist/spa/assets/{xml-BXzZhhmL.js → xml-iWTTgx9j.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-DQNlz7_W.js → yaml-am8-T4BQ.js} +1 -1
- package/src/client/dist/spa/index.html +7 -13
- package/src/mcp-server/kobo-tasks-server.ts +4 -0
- package/src/client/dist/spa/assets/ActivityFeed-COdkQaiZ.js +0 -8
- package/src/client/dist/spa/assets/ChangelogPage-BXD8H3j-.js +0 -1
- package/src/client/dist/spa/assets/ClosePopup-DD10nToj.js +0 -1
- package/src/client/dist/spa/assets/CreatePage-DX4TjLqr.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-BUjVXGyZ.js +0 -8
- package/src/client/dist/spa/assets/HealthPage-DM7fvP4v.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-CbUZOTwz.js +0 -37
- package/src/client/dist/spa/assets/QBtn-DwemGTZv.js +0 -1
- package/src/client/dist/spa/assets/QCheckbox-o3UHW596.js +0 -1
- package/src/client/dist/spa/assets/QChip-BpS8c1sW.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-C9vmJqEO.js +0 -1
- package/src/client/dist/spa/assets/QInput-CLZtb8E0.js +0 -1
- package/src/client/dist/spa/assets/QItemLabel-BYSjzk-t.js +0 -1
- package/src/client/dist/spa/assets/QItemSection-O9WBXftL.js +0 -1
- package/src/client/dist/spa/assets/QMenu-D_9kEp2i.js +0 -1
- package/src/client/dist/spa/assets/QRadio-B_TurTzx.js +0 -1
- package/src/client/dist/spa/assets/QScrollArea-B5jf9S4y.js +0 -1
- package/src/client/dist/spa/assets/QScrollObserver-Cxj52Zfg.js +0 -1
- package/src/client/dist/spa/assets/QSelect-DERXhq6x.js +0 -36
- package/src/client/dist/spa/assets/QSpace-CrVsndpV.js +0 -1
- package/src/client/dist/spa/assets/QToggle-Dwr3hSLw.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-CyRLTG6i.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-OaTRqd2Q.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-B7H6sD7r.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-BBk3MB8w.js +0 -9
- package/src/client/dist/spa/assets/TouchPan-BmfIMD00.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-k_qzLoLC.js +0 -4
- package/src/client/dist/spa/assets/build-path-tree-D4_LR3mz.js +0 -1
- package/src/client/dist/spa/assets/chunk-DtRyYLXJ.js +0 -1
- package/src/client/dist/spa/assets/documents-N8PwB_Gh.js +0 -1
- package/src/client/dist/spa/assets/editor.worker-DWlYVeeX.js +0 -26
- package/src/client/dist/spa/assets/engineFeatures-DChekJQO.js +0 -1
- package/src/client/dist/spa/assets/expand-template-BXNt_oWH.js +0 -1
- package/src/client/dist/spa/assets/formatters-wq5wP2If.js +0 -1
- package/src/client/dist/spa/assets/i18n-DXMuiAyu.js +0 -1
- package/src/client/dist/spa/assets/index-CNin8jPU.js +0 -82
- package/src/client/dist/spa/assets/json.worker-CCzEOxDx.js +0 -58
- package/src/client/dist/spa/assets/kobo-commands-Ip0ObeCE.js +0 -9
- package/src/client/dist/spa/assets/notifications-C4MxuXC7.js +0 -1
- package/src/client/dist/spa/assets/render-chat-markdown-BJsZCnSw.js +0 -66
- package/src/client/dist/spa/assets/touch-yfnu5R3D.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-DcJRs0ay.js +0 -1
- package/src/client/dist/spa/assets/vue-i18n-C5Tx4bGk.js +0 -3
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-BzmG9fMN.js → _plugin-vue_export-helper-BDNMzG2s.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-DiwvWnMr.js → abap-08VXUWAP.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-CmtZjKlf.js → apex-BWPQTe0t.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-DL2My_i-.js → azcli-Bc_sGQ0U.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-B-nC98wG.js → bat-i0X4ZdIN.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-Ju5MwOgh.js → bicep-B5-_aFwp.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-8Eu1TyBr.js → cameligo-DMUM7wLl.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-u-RpMkH3.js → clojure-Cm7r79vr.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-CdA7bbTe.js → coffee-Ba7i2nA0.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-CzNFP8ks.js → cpp-C7h46wYY.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-j1LThmcE.js → csharp-BKxtCVv1.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CLRC61y6.js → csp-bTuwJoIa.js} +0 -0
- /package/src/client/dist/spa/assets/{css-r6rC_7P2.js → css-DIMkf-bt.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-CW08XVUh.js → cypher-CVaqCwHa.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-Cs9aL5T_.js → dart-onAF5SnQ.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-BWM0M184.js → dockerfile-DZFCIeNp.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-MJJuer5P.js → ecl-D05T4iGw.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-D2AIuXqn.js → elixir-6RTg0lbw.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-B2H24giC.js → flow9-C5_-GSwl.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-CFNadkg7.js → fsharp-C8Ef5oNN.js} +0 -0
- /package/src/client/dist/spa/assets/{go-dSur1iB2.js → go-C-y9NEjX.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-qyhAo11d.js → graphql-fmXr3nnJ.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-DFzjMyzm.js → hcl-CpzslTdj.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-TdzA8TIl.js → ini-sBoK_t0W.js} +0 -0
- /package/src/client/dist/spa/assets/{java-CSGA9pkE.js → java-BEtHBSE6.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-9izz5OsY.js → julia-Bri6UV-V.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-DIUPrqKg.js → kotlin-BOotOW0E.js} +0 -0
- /package/src/client/dist/spa/assets/{less-B8d93iCg.js → less-B9JPFI3C.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-DWtEIyu7.js → lexon-CfSJPG6W.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-Ciq0OGgt.js → lua-CsQS60Ue.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-Cki6JWj_.js → m3-D-oSqn_W.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-Cu47xwU0.js → markdown-Cimd5fb3.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-BM8ui995.js → mips-CIPQ_RoX.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-DqLio0_c.js → msdax-DauUninz.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-v1wbjJOq.js → mysql-SOo6toE5.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-CQl3PGSB.js → objective-c-FvmIjYaQ.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-D4iW0ZtD.js → pascal-DrH0SRf2.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-BdC9CZdj.js → pascaligo-D-ptJ9y-.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-BL10m4XD.js → perl-oz_6vUea.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-Be_oqVo3.js → pgsql-DTj74zXo.js} +0 -0
- /package/src/client/dist/spa/assets/{php-BtvXSFRI.js → php-nr791fC2.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-B2vUy15C.js → pla-CopQ2nXW.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-CbmTTfXr.js → postiats-43DmfD33.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-DszLhJGx.js → powerquery-D3hlyOfw.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-B0dYktF6.js → powershell-DmHpPYUd.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-CZvaj1VX.js → protobuf-C531GsRP.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-CPDx1B3S.js → pug-Z5eAx3Zn.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-CDP9TFLl.js → qsharp-DkqhCAOL.js} +0 -0
- /package/src/client/dist/spa/assets/{r-8DbbFX2l.js → r-BwWrilGY.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-DRWj9MtJ.js → redis-ClamHrr6.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-C6cElE_5.js → redshift-DT7zqm-g.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-W9pS9n3m.js → restructuredtext-BYgofb2h.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-BKnzWnk-.js → ruby-DezsRK8O.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-YPCclWwe.js → rust-DdL9SqIa.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-BgM4DTFb.js → sb-CcwsVR0C.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-fz1OPLMl.js → scala-DHpiXF5c.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-8Uz1RIbu.js → scheme-BeGwcela.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-Djo3IYXr.js → scss-gp-XZpBa.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-CINF5Tx_.js → shell-CC2rA5mh.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-GgiNEuUm.js → solidity-BEEn4gHE.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-Culj97P9.js → sophia-CRfGWb83.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-C2ZlpxOY.js → sparql-D_Lu-MrJ.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-BEf5Pg7Y.js → sql-NEE52Syq.js} +0 -0
- /package/src/client/dist/spa/assets/{st-CT6UUoeH.js → st-DbInun42.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-B5g0xTG3.js → swift-Bxkupp3x.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-CEgQz9DR.js → systemverilog-Bz4Y3fRF.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-D0qL2L0I.js → tcl-DISqw1ZD.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-BFUAVf1E.js → twig-De2hgUGE.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-CjVVcNKm.js → typespec-B8J7ngcE.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-CZJr-DQz.js → vb-DV3o63ZY.js} +0 -0
- /package/src/client/dist/spa/assets/{wgsl-ivoXUo2e.js → wgsl-DpFanUEy.js} +0 -0
package/AGENTS.md
CHANGED
|
@@ -4,21 +4,21 @@ Guidance for AI coding agents (Claude Code, Cursor, etc.) working on this reposi
|
|
|
4
4
|
|
|
5
5
|
## What this project is
|
|
6
6
|
|
|
7
|
-
**Kōbō** (
|
|
7
|
+
**Kōbō** (工房, Japanese for "workshop") orchestrates multiple Claude Code agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, Claude session, optional dev server, optional Notion source-of-truth, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, and manage the lifecycle.
|
|
8
8
|
|
|
9
|
-
Single-user,
|
|
9
|
+
Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback HTTP/WS requests behind a shared token (localhost always exempt). No multi-tenant concerns.
|
|
10
10
|
|
|
11
11
|
## Tech stack
|
|
12
12
|
|
|
13
|
-
**Backend
|
|
13
|
+
**Backend**: Node.js ≥ 20, Hono (HTTP), `ws` (WebSocket), better-sqlite3 (WAL mode), nanoid, `@modelcontextprotocol/sdk`. TypeScript throughout, `tsx` for dev, `tsc` for production build.
|
|
14
14
|
|
|
15
|
-
**Frontend
|
|
15
|
+
**Frontend**: Vue 3, Quasar 2, Pinia, vue-router, marked + dompurify for markdown rendering. Vite via `@quasar/app-vite`.
|
|
16
16
|
|
|
17
|
-
**Database
|
|
17
|
+
**Database**: a single SQLite file under the **Kōbō home directory** (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`). Fresh-install schema lives in `src/server/db/schema.ts` (`initSchema`); incremental migrations live in `src/server/db/migrations.ts`. **The project is in production**, so every schema change MUST ship as a migration that preserves data, never as a breaking change to `initSchema` alone. See [Database migrations](#database-migrations) below.
|
|
18
18
|
|
|
19
|
-
**Kōbō home directory
|
|
19
|
+
**Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
|
|
20
20
|
|
|
21
|
-
**Tests
|
|
21
|
+
**Tests**: vitest (24 backend test files, 544+ tests; 5 frontend test files, 45+ tests at time of writing). Frontend tests cover Pinia stores and pure utility modules; Vue components are not tested (type-check + manual smoke only).
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
@@ -100,16 +100,16 @@ src/
|
|
|
100
100
|
|
|
101
101
|
| Table | Purpose |
|
|
102
102
|
|---|---|
|
|
103
|
-
| `workspaces` | the unit of work
|
|
104
|
-
| `tasks` | workspace sub-items
|
|
105
|
-
| `agent_sessions` | Claude Code CLI invocations
|
|
106
|
-
| `ws_events` | persisted WebSocket events for replay on reconnect
|
|
107
|
-
| `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool
|
|
108
|
-
| `workspace_chat_history` | chat-input history per workspace
|
|
103
|
+
| `workspaces` | the unit of work: id, name, project_path, source_branch, working_branch, status, notion_url, model, dev_server_status, `archived_at`, `worktree_purged_at`, `worktree_purge_restore_data` (JSON), `auto_loop`, `auto_loop_ready`, `no_progress_streak`, timestamps |
|
|
104
|
+
| `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
|
|
105
|
+
| `agent_sessions` | Claude Code CLI invocations: pid, `claude_session_id`, status, started_at, ended_at, `name` |
|
|
106
|
+
| `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
|
|
107
|
+
| `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
|
|
108
|
+
| `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
|
|
109
109
|
|
|
110
110
|
`status` enum: `created | extracting | brainstorming | executing | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
|
|
111
111
|
|
|
112
|
-
`archived_at` is **orthogonal** to `status
|
|
112
|
+
`archived_at` is **orthogonal** to `status`. Archiving is a visibility flag, not a lifecycle state. Unarchive restores the exact pre-archive `status`.
|
|
113
113
|
|
|
114
114
|
`worktree_purged_at` + `worktree_purge_restore_data` drive the disk-space purge feature (see [Worktree purge](#worktree-purge) below): when set, the workspace's worktree folder has been removed from disk but the chat history is preserved. `worktree_purge_restore_data` is a JSON blob (`{ prNumber, prUrl, forge, mergeCommitSha, originalWorktreePath, originalSourceBranch, originalWorkingBranch }`) captured at purge time for future "Restore" UX. Both fields are cleared automatically by the pr-watcher when the worktree folder reappears on disk.
|
|
115
115
|
|
|
@@ -121,8 +121,8 @@ src/
|
|
|
121
121
|
|
|
122
122
|
### The two files and their roles
|
|
123
123
|
|
|
124
|
-
- **`src/server/db/schema.ts
|
|
125
|
-
- **`src/server/db/migrations.ts
|
|
124
|
+
- **`src/server/db/schema.ts`**: `initSchema(db)` is the source of truth for **fresh installs only**. It creates every table at its current shape. New installations (empty `data/` directory) run `initSchema` once and land at the latest `SCHEMA_VERSION`.
|
|
125
|
+
- **`src/server/db/migrations.ts`**: `runMigrations(db)` reads the current `version` from the `schema_version` table and sequentially applies every pending migration block up to `SCHEMA_VERSION`. Existing databases upgrade through this path.
|
|
126
126
|
|
|
127
127
|
Both files must be kept in sync: after adding a migration, update `initSchema` so fresh installs get the same final shape without replaying migrations.
|
|
128
128
|
|
|
@@ -138,13 +138,14 @@ Every feature that touches the schema:
|
|
|
138
138
|
- A database at the previous version can be upgraded without data loss
|
|
139
139
|
- The new version matches `SCHEMA_VERSION`
|
|
140
140
|
- Fresh installs and upgraded installs converge to the same schema
|
|
141
|
-
6. Never edit or reorder migration blocks that have already shipped
|
|
141
|
+
6. Never edit or reorder migration blocks that have already shipped; they are historical. If you need to fix a mistake, add a new migration.
|
|
142
142
|
|
|
143
143
|
### Rules
|
|
144
144
|
|
|
145
145
|
- **Migrations are append-only.** Shipped migration blocks are frozen. Fixes go in new migrations.
|
|
146
146
|
- **Always idempotent where possible.** Use `IF NOT EXISTS`, check for column existence before altering, etc. Prefer migrations that can be safely re-run.
|
|
147
147
|
- **`ALTER TABLE ADD COLUMN` is safe in SQLite** (even on large tables). For more invasive changes (rename, drop, change type), use the [12-step SQLite pattern](https://sqlite.org/lang_altertable.html#otheralter) within a transaction.
|
|
148
|
+
|
|
148
149
|
- **Run migrations on every backend start.** `runMigrations(db)` is called from `getDb()` via `src/server/db/index.ts`.
|
|
149
150
|
- **Test upgrades, not just fresh installs.** The `migrations.test.ts` suite must exercise "old DB → new DB" paths.
|
|
150
151
|
|
|
@@ -159,26 +160,26 @@ Clients subscribe to individual workspace ids. The server sends `WsEvent` object
|
|
|
159
160
|
Common types: `agent:output`, `agent:status`, `agent:error`, `user:message`, `task:updated`, `devserver:status`, `workspace:archived`, `workspace:unarchived`, `sync:response`.
|
|
160
161
|
|
|
161
162
|
Two emit flavors in `websocket-service.ts`:
|
|
162
|
-
- `emit(workspaceId, type, payload)`
|
|
163
|
-
- `emitEphemeral(workspaceId, type, payload)`
|
|
163
|
+
- `emit(workspaceId, type, payload)` persists to `ws_events` for later replay via `sync:request` on reconnect
|
|
164
|
+
- `emitEphemeral(workspaceId, type, payload)` delivers once and never persists. Use it for lifecycle events (archive, status changes) that shouldn't replay.
|
|
164
165
|
|
|
165
166
|
## External integrations
|
|
166
167
|
|
|
167
168
|
### Forge providers
|
|
168
169
|
|
|
169
|
-
`src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (no-op
|
|
170
|
+
`src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (a no-op that disables PR/MR features cleanly). The public surface is two functions: `getForgeProvider(name)` in `registry.ts` (returns the provider for a named forge) and `resolveForge(projectPath)` in `resolve.ts` (reads the per-project `forge` setting, then falls back to auto-detection from the `origin` remote URL: host contains `github.com` → GitHub, host contains `gitlab` → GitLab, otherwise `none`). The per-project `forge` setting (`'auto' | 'github' | 'gitlab' | 'none'`, default `'auto'`) is stored in `settings.json` and seeded by settings migration v32. PR routes (`open-pr`, `change-pr-base`) and the pr-watcher go through the resolved provider. **Kōbō ships no forge credentials**, so the user must install and authenticate `gh` or `glab` themselves; when the CLI is absent or unauthenticated, PR/MR actions are disabled with a tooltip rather than a raw error.
|
|
170
171
|
|
|
171
172
|
### Notion (opt-in, user-provided credentials)
|
|
172
173
|
|
|
173
|
-
`notion-service.ts` spawns the official [`@notionhq/notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) as a child process (`npx -y @notionhq/notion-mcp-server`) and talks to it over stdio using JSON-RPC / MCP. **Kōbō ships no Notion credentials
|
|
174
|
+
`notion-service.ts` spawns the official [`@notionhq/notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) as a child process (`npx -y @notionhq/notion-mcp-server`) and talks to it over stdio using JSON-RPC / MCP. **Kōbō ships no Notion credentials**, so the feature only works if the user has configured their own integration token. The token is resolved in this order:
|
|
174
175
|
|
|
175
176
|
1. `NOTION_API_TOKEN` env var
|
|
176
177
|
2. `NOTION_TOKEN` env var
|
|
177
|
-
3. `~/.claude.json` → `mcpServers.notion.env.NOTION_TOKEN` / `NOTION_API_TOKEN` (Claude Code's MCP config
|
|
178
|
+
3. `~/.claude.json` → `mcpServers.notion.env.NOTION_TOKEN` / `NOTION_API_TOKEN` (Claude Code's MCP config: the recommended path, the same token shared with Claude Code)
|
|
178
179
|
|
|
179
180
|
The MCP command and args can be overridden via `NOTION_MCP_COMMAND` (default `npx`) and `NOTION_MCP_ARGS` (default `-y @notionhq/notion-mcp-server`) for pinning a specific version or using a fork.
|
|
180
181
|
|
|
181
|
-
When adding features touching `notion-service.ts`, remember: **no token = no feature**. The rest of Kōbō must keep working if the Notion token is absent
|
|
182
|
+
When adding features touching `notion-service.ts`, remember: **no token = no feature**. The rest of Kōbō must keep working if the Notion token is absent; only the explicit Notion import endpoints should fail with a clear error. Do not throw at server startup.
|
|
182
183
|
|
|
183
184
|
See the "Notion integration" section of the README for the end-user setup guide.
|
|
184
185
|
|
|
@@ -186,16 +187,16 @@ See the "Notion integration" section of the README for the end-user setup guide.
|
|
|
186
187
|
|
|
187
188
|
Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
|
|
188
189
|
|
|
189
|
-
**Claude Code** (`claude-code/`)
|
|
190
|
+
**Claude Code** (`claude-code/`): uses `@anthropic-ai/claude-agent-sdk` (in-process async iterator). Spawns no subprocess. Auth via `~/.claude.json` or `ANTHROPIC_API_KEY` env var. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
|
|
190
191
|
|
|
191
|
-
**OpenAI Codex** (`codex/`)
|
|
192
|
-
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts
|
|
193
|
-
- `client.ts
|
|
194
|
-
- `protocol/types.ts
|
|
195
|
-
- `event-mapper.ts
|
|
196
|
-
- `server-requests.ts
|
|
197
|
-
- `engine.ts
|
|
198
|
-
- `spawn.ts
|
|
192
|
+
**OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
|
|
193
|
+
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
194
|
+
- `client.ts`: typed `AppServerClient` wrapping the peer (initialize / thread.start / thread.resume / turn.start / turn.interrupt)
|
|
195
|
+
- `protocol/types.ts`: hand-written subset of the Codex v2 protocol types (camelCase field names like `agentMessage`, `commandExecution`, etc.). The full canonical bindings are generated by `codex app-server generate-ts` if the protocol drifts.
|
|
196
|
+
- `event-mapper.ts`: translates app-server notifications (`item/started`, `item/completed`, `item/agentMessage/delta`, `turn/completed`, `thread/tokenUsage/updated`, `account/rateLimits/updated`, `error`) into Kōbō `AgentEvent` union
|
|
197
|
+
- `server-requests.ts`: handles server-initiated approval/elicitation requests (`item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/tool/requestUserInput`, `item/permissions/requestApproval`, plus v1 legacy aliases `execCommandApproval` / `applyPatchApproval`)
|
|
198
|
+
- `engine.ts`: `createCodexEngine()` factory wiring everything into `AgentEngine`
|
|
199
|
+
- `spawn.ts`: locates the `codex` binary via `@openai/codex` dependency and spawns `codex app-server`
|
|
199
200
|
|
|
200
201
|
Auth: delegated to the `codex` CLI which reads `OPENAI_API_KEY` from env or `~/.codex/auth.json`. Kōbō ships no Codex credentials. The `@openai/codex` package (binary) is a direct dependency.
|
|
201
202
|
|
|
@@ -203,12 +204,12 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
|
|
|
203
204
|
|
|
204
205
|
**Protocol gotchas worth remembering** (post-migration findings):
|
|
205
206
|
|
|
206
|
-
- **`experimentalApi: true` is mandatory in the `initialize` handshake.** Without it, any turn using experimental fields
|
|
207
|
-
- **`collaborationMode` is sticky server-side.** Once a turn ran in `mode: 'plan'`, every subsequent turn on the same thread stays in plan until we explicitly send `mode: 'default'` again. The engine therefore always emits the field on `turn/start
|
|
207
|
+
- **`experimentalApi: true` is mandatory in the `initialize` handshake.** Without it, any turn using experimental fields (most importantly `turn/start.collaborationMode`) is rejected with `-32600: requires experimentalApi capability`. See `client.ts:connect()`.
|
|
208
|
+
- **`collaborationMode` is sticky server-side.** Once a turn ran in `mode: 'plan'`, every subsequent turn on the same thread stays in plan until we explicitly send `mode: 'default'` again. The engine therefore always emits the field on `turn/start`, never omitting it, so a Plan → Bypass switch actually takes effect. Mapping: Kōbō `plan` → `plan`, every other Kōbō mode → `default`. Plan mode is the only one that unlocks Codex's internal `request_user_input` tool.
|
|
208
209
|
- **Permission mode vs collaboration mode are independent.** Sandbox + approvalPolicy control *what the agent may do at OS level* (read-only / workspace-write, never / on-request / unless-trusted). `collaborationMode` is a separate session-level flag that gates internal Codex behaviour (notably interactive Q&A). Kōbō hides both behind a single "permission mode" selector and maps them together.
|
|
209
|
-
- **Sub-agents map to `collabAgentToolCall`.** Codex's analogue of Claude's Task tool is `collabAgentToolCall` (`spawnAgent` / `sendInput` / `resumeAgent` / `wait` / `closeAgent`). The mapper emits **both** a `tool:call` named `Task` (chat card) and a `subagent:progress` event (right-hand panel) per call
|
|
210
|
+
- **Sub-agents map to `collabAgentToolCall`.** Codex's analogue of Claude's Task tool is `collabAgentToolCall` (`spawnAgent` / `sendInput` / `resumeAgent` / `wait` / `closeAgent`). The mapper emits **both** a `tool:call` named `Task` (chat card) and a `subagent:progress` event (right-hand panel) per call, the same dual-emission Claude does. See `event-mapper.ts` `handleItemStarted` / `handleItemCompleted` for the `collabAgentToolCall` branch.
|
|
210
211
|
- **`fileChange` items carry a unified-diff blob.** The protocol shape is `{ path, kind: PatchChangeKind, diff: string }` per change; `kind` is a discriminated union, not a string. The mapper flattens the first change into a Claude-style Edit input (`{ file_path, diff, change_kind, move_path? }`) so the existing `ToolCallItem` renderer picks it up. The client parses the unified diff into `DiffLine[]` via `parseUnifiedDiff` in `inline-diff.ts`.
|
|
211
|
-
- **Streaming bursts trip auto-scroll.** Codex emits one `message:text` event per token-delta (50-200 per message), versus Claude which emits ~1 per content block. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. The fix coalesces requests through `requestAnimationFrame` and only animates the *first* scroll after a quiet period
|
|
212
|
+
- **Streaming bursts trip auto-scroll.** Codex emits one `message:text` event per token-delta (50-200 per message), versus Claude which emits ~1 per content block. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. The fix coalesces requests through `requestAnimationFrame` and only animates the *first* scroll after a quiet period; subsequent scrolls during a burst snap instantly.
|
|
212
213
|
- **`MCP tools` need `default_tools_approval_mode: 'auto'` in `config.mcp_servers`.** Without it Codex flags every MCP tool call as needing user approval ("user cancelled MCP tool call"). Kōbō trusts every tool it spawns, so the options-builder pre-approves the namespace.
|
|
213
214
|
|
|
214
215
|
## Workspace operations
|
|
@@ -217,21 +218,21 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
|
|
|
217
218
|
|
|
218
219
|
`change-source-branch-service.ts` re-targets a workspace onto a new source branch. The default path is a cherry-pick of the branch-proper commits (commits in the working branch but in **neither** the old nor the new base), inspired by the sekur `deploy-preprod-rebase.yml` workflow. The route is `POST /api/workspaces/:id/change-source-branch` and returns a discriminated status: `done | aligned | conflict | too-many | dirty`.
|
|
219
220
|
|
|
220
|
-
- **Built-in cherry-pick
|
|
221
|
-
- **Custom bash override
|
|
222
|
-
- **Custom-script env vars
|
|
221
|
+
- **Built-in cherry-pick**: `fetchAllBranches` → `listProperCommits` → `stashPush` (if dirty + aligned) → backup branch (`kobo-backup/<branch>-<unix-ts>`) → `reset --hard origin/<new>` → cherry-pick replay → optional force-push prompt → forge PR-base update via `provider.changePrBase`. Conflicts leave the worktree in a cherry-pick state for the user/agent to resolve via `POST /:id/git/resolve-with-agent`. `GitConflictError` carries an `operation: 'rebase' | 'merge' | 'cherry-pick'` discriminator.
|
|
222
|
+
- **Custom bash override**: if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty; empty means the feature is disabled (opt-in).
|
|
223
|
+
- **Custom-script env vars**: `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
|
|
223
224
|
|
|
224
225
|
### Worktree purge
|
|
225
226
|
|
|
226
227
|
`src/server/services/worktree-purge-service.ts` removes a workspace's worktree from disk while preserving the chat history and PR metadata. Triggered manually via `POST /api/workspaces/:id/purge-worktree` from the workspace context menu, or automatically by the pr-watcher when a PR transitions to MERGED **and** `global.autoPurgeOnPrMerged` is enabled (Settings → Worktrees toggle, settings migration v36).
|
|
227
228
|
|
|
228
|
-
Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM
|
|
229
|
+
Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM, typically Docker-owned files in `node_modules` / `vendor`) are detected via regex on the error message and the warning toast carries a copy-pasteable `sudo rm -rf` + `git worktree prune` recovery command plus prevention tips (Docker `USER` directive, `setfacl` default ACL). See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
|
|
229
230
|
|
|
230
231
|
**Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`, checks `fs.existsSync(worktreePath)`, and on a hit calls `restoreWorktreeFromDisk(id)` which clears `worktree_purged_at` + `worktree_purge_restore_data` + `archived_at` in one transaction, then emits `workspace:worktree-restored`. The client websocket store reuses the same handler as `workspace:archived`/`unarchived` to refresh both the active and archived workspace lists. No UI action needed.
|
|
231
232
|
|
|
232
233
|
### Workspace attention indicators
|
|
233
234
|
|
|
234
|
-
`src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function
|
|
235
|
+
`src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function, easy to unit-test and free of IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
|
|
235
236
|
|
|
236
237
|
### Bulk workspace info refresh
|
|
237
238
|
|
|
@@ -245,15 +246,15 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
|
|
|
245
246
|
|
|
246
247
|
**Service layer** throws descriptive errors; the route layer catches and maps to HTTP status codes. Error messages follow the pattern `` `Workspace '${id}' not found` `` / `` `... is already archived` ``.
|
|
247
248
|
|
|
248
|
-
**Route layer** is thin
|
|
249
|
+
**Route layer** is thin: always wrap the handler body in `try / catch` and return `c.json({ error: message }, status)`. Match the existing shape in `src/server/routes/workspaces.ts`.
|
|
249
250
|
|
|
250
251
|
**Swallowed failures** are acceptable (and required) for best-effort side effects like `agentManager.stopAgent` and `devServerService.stopDevServer` during delete/archive. Log with `console.error` and continue. Never let these break the happy path.
|
|
251
252
|
|
|
252
253
|
**Route ordering matters** in Hono. Static paths (`GET /archived`) MUST be declared **before** dynamic segments (`GET /:id`) or the dynamic segment captures them. There's a regression test locking this invariant in `src/__tests__/routes-workspaces.test.ts`.
|
|
253
254
|
|
|
254
|
-
**File size
|
|
255
|
+
**File size**: prefer focused files. `WorkspaceList.vue` and `workspaces.ts` (routes) are the largest files; don't grow them further without a clear reason. If a file approaches unwieldy, surface it as a concern rather than silently splitting it.
|
|
255
256
|
|
|
256
|
-
**Dependencies
|
|
257
|
+
**Dependencies**: root `package.json` covers backend + tests. `src/client/package.json` is a separate npm tree. Install both.
|
|
257
258
|
|
|
258
259
|
## Internationalization (i18n)
|
|
259
260
|
|
|
@@ -270,9 +271,9 @@ The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), Frenc
|
|
|
270
271
|
|
|
271
272
|
## Testing discipline
|
|
272
273
|
|
|
273
|
-
- **TDD for backend
|
|
274
|
-
- **Route tests** use `vi.mock()` on service modules before imports (see `src/__tests__/routes-workspaces.test.ts`). Keep mocks complete
|
|
275
|
-
- **Frontend tests** cover Pinia stores (`workspace-store`, `settings-store`, `templates-store`) and pure utility modules (`expand-template`). Run with `cd src/client && npm test`. Vue components are **not** tested
|
|
274
|
+
- **TDD for backend**: write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See existing tests in `src/__tests__/workspace-service.test.ts` for the setup pattern (fresh in-memory DB per test via `resetDb()`).
|
|
275
|
+
- **Route tests** use `vi.mock()` on service modules before imports (see `src/__tests__/routes-workspaces.test.ts`). Keep mocks complete; missing exports cause obscure failures.
|
|
276
|
+
- **Frontend tests** cover Pinia stores (`workspace-store`, `settings-store`, `templates-store`) and pure utility modules (`expand-template`). Run with `cd src/client && npm test`. Vue components are **not** tested: type-check via `npx tsc --noEmit` + manual smoke testing covers UI behavior.
|
|
276
277
|
- **`beforeEach(() => vi.clearAllMocks())`** is the convention for all route test files.
|
|
277
278
|
|
|
278
279
|
## Git workflow
|
|
@@ -300,7 +301,7 @@ These rules are the source of truth and are also written to `.ai/.git-convention
|
|
|
300
301
|
- Never commit directly to `main`/`master`/`develop`
|
|
301
302
|
|
|
302
303
|
**Workflow**
|
|
303
|
-
- Rebase on the source branch before opening a PR
|
|
304
|
+
- Rebase on the source branch before opening a PR; do not merge it in
|
|
304
305
|
- Keep commits atomic and self-contained (each compiles and passes tests)
|
|
305
306
|
- Squash fixup commits before pushing
|
|
306
307
|
- Never force-push to shared branches
|
|
@@ -319,12 +320,12 @@ The human user of this repository prefers French for conversational exchanges. C
|
|
|
319
320
|
Always read `DESIGN.md` (repo root) before making any visual or UI decisions. Every
|
|
320
321
|
font choice, color, spacing value, and aesthetic decision is defined there. The CSS
|
|
321
322
|
variables in `src/client/src/css/design-tokens.scss` are the runtime source of truth
|
|
322
|
-
for the values documented in `DESIGN.md
|
|
323
|
+
for the values documented in `DESIGN.md`; never hardcode hex colors or spacing
|
|
323
324
|
literals in components.
|
|
324
325
|
|
|
325
326
|
Aesthetic direction: **Brutally Minimal × Industrial** (Linear / Anthropic Console
|
|
326
327
|
reference). Dark-native, monochrome, single indigo accent (`--kobo-accent` /
|
|
327
|
-
`#6c63ff`) used
|
|
328
|
+
`#6c63ff`) used sparingly. No purple gradients, no decorative illustrations, no
|
|
328
329
|
bubble-pill shapes, no spring physics. Geist + Geist Mono for technical values.
|
|
329
330
|
|
|
330
331
|
When reviewing or writing UI code, flag any deviation from `DESIGN.md`. Do not
|
|
@@ -332,10 +333,10 @@ deviate from the documented system without explicit user approval.
|
|
|
332
333
|
|
|
333
334
|
## What NOT to do
|
|
334
335
|
|
|
335
|
-
- Don't drop-and-recreate the database to apply schema changes. The project is in production
|
|
336
|
+
- Don't drop-and-recreate the database to apply schema changes. The project is in production, so every schema change ships as a migration that preserves data (see [Database migrations](#database-migrations)).
|
|
336
337
|
- Don't edit or reorder migration blocks that have already shipped. Migrations are append-only; fixes go in new migrations.
|
|
337
338
|
- Don't add confirmation dialogs for reversible actions (archive, unarchive). Only destructive actions (delete) get a dialog.
|
|
338
|
-
- Don't introduce ORMs, query builders, or schema validation libraries
|
|
339
|
+
- Don't introduce ORMs, query builders, or schema validation libraries; the project is small enough for raw prepared statements and hand-written mappers.
|
|
339
340
|
- Don't break the single-source-of-truth of `CLAUDE.md` → `AGENTS.md` symlink. Edit `AGENTS.md`; `CLAUDE.md` follows automatically.
|
|
340
341
|
- Don't skip `try/catch` swallowing on best-effort cleanup (agent stop, dev-server stop, worktree removal). These must never break the primary operation.
|
|
341
342
|
- Don't hardcode user-visible text in the frontend. Every string must go through `$t()` / `t()` with keys in all 5 locale files. See [Internationalization (i18n)](#internationalization-i18n).
|
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,18 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
-
## 1.
|
|
7
|
+
## 1.8.1
|
|
8
|
+
|
|
9
|
+
- feat: opt-in LAN network access plus UX and workspace fixes (#13)
|
|
10
|
+
- chore(docs): update CHANGELOG
|
|
11
|
+
|
|
12
|
+
## 1.8.0
|
|
13
|
+
|
|
14
|
+
- feat(schedule): manual wakeup & cron management per workspace
|
|
15
|
+
- feat(agent): enforce wakeup at turn-end + keep background work alive
|
|
16
|
+
- feat(workspace): warn when viewing a non-latest session outside auto-loop
|
|
17
|
+
|
|
18
|
+
## 1.7.34
|
|
8
19
|
|
|
9
20
|
- chore(npm): update claude sdk
|
|
10
21
|
|
package/README.md
CHANGED
|
@@ -13,19 +13,19 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **Isolated worktrees
|
|
17
|
-
- **Two agent engines
|
|
18
|
-
- **Live chat
|
|
19
|
-
- **Task tracking
|
|
20
|
-
- **Git panel
|
|
21
|
-
- **Attention indicators
|
|
22
|
-
- **Auto-loop
|
|
23
|
-
- **Quota-aware
|
|
24
|
-
- **Scheduled wakeups
|
|
25
|
-
- **Cron schedules
|
|
26
|
-
- **Lifecycle scripts
|
|
27
|
-
- **Disk-space purge
|
|
28
|
-
- **Optional integrations
|
|
16
|
+
- **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
|
|
17
|
+
- **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
|
|
18
|
+
- **Live chat**: streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and you can export any workspace's session events to CSV.
|
|
19
|
+
- **Task tracking**: a per-workspace MCP server (`kobo-tasks`) lets the agent manage its own tasks, acceptance criteria, and live status.
|
|
20
|
+
- **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch` (cherry-pick of the branch-proper commits, with an optional custom bash script). Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
|
|
21
|
+
- **Attention indicators**: workspace cards in the drawer surface CI failures and review-requested-changes inline, so failing PRs/MRs stand out at a glance.
|
|
22
|
+
- **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping on completion, stall, or error.
|
|
23
|
+
- **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets sit in the footer, and sessions auto-resume after a rate-limit reset.
|
|
24
|
+
- **Scheduled wakeups**: the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool. Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
|
|
25
|
+
- **Cron schedules**: recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`). Each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
|
|
26
|
+
- **Lifecycle scripts**: shell scripts run automatically at key moments. **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configure them globally or per project, with their output streamed into the chat.
|
|
27
|
+
- **Disk-space purge**: free a merged workspace's disk space without losing its chat history. The worktree folder is removed (PR metadata is captured for later restore), the workspace is archived, and the chat log stays queryable. Trigger it manually from the workspace menu or enable **auto-purge on PR merged** in Settings → Worktrees. Recreate the worktree later with `gh pr checkout <pr-number>` and Kōbō detects the folder reappearing within 30 seconds, then unarchives and reactivates the workspace with no UI action needed.
|
|
28
|
+
- **Optional integrations**: Notion (import missions), Sentry (fix from issue URL), local voice transcription (whisper.cpp).
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ Requires Node.js ≥ 20 and a logged-in Claude Code **or** Codex CLI.
|
|
|
35
35
|
npx @loicngr/kobo@latest
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Default port is `3000`. If you already run something on that port (or another Kōbō instance), pick your own
|
|
38
|
+
Default port is `3000`. If you already run something on that port (or another Kōbō instance), pick your own. `SERVER_PORT` is read first, `PORT` is the fallback:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
SERVER_PORT=9997 PORT=9998 npx @loicngr/kobo@latest
|
|
@@ -53,7 +53,7 @@ npm install
|
|
|
53
53
|
npm run dev:all # backend :3300 + client :8080
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
A production-installed Kōbō (`npx @loicngr/kobo`) and a dev server can run side by side
|
|
56
|
+
A production-installed Kōbō (`npx @loicngr/kobo`) and a dev server can run side by side, since they use separate data directories.
|
|
57
57
|
|
|
58
58
|
## Configuration
|
|
59
59
|
|
|
@@ -62,35 +62,35 @@ The most common knobs:
|
|
|
62
62
|
| Env var | Default | Purpose |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| `PORT` | `3000` | HTTP / WebSocket server port (overridden by `SERVER_PORT` if set) |
|
|
65
|
-
| `SERVER_PORT` |
|
|
65
|
+
| `SERVER_PORT` | none | Preferred override for the server port; takes precedence over `PORT` |
|
|
66
66
|
| `KOBO_HOME` | `~/.config/kobo` | Data directory (SQLite, settings, voice models) |
|
|
67
|
-
| `NOTION_API_TOKEN` |
|
|
68
|
-
| `OPENAI_API_KEY` |
|
|
67
|
+
| `NOTION_API_TOKEN` | none | Notion integration token |
|
|
68
|
+
| `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
|
|
69
69
|
|
|
70
|
-
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime
|
|
70
|
+
Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Per-project values inherit from the global ones when left empty.
|
|
71
71
|
|
|
72
|
-
The full reference
|
|
72
|
+
The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
|
|
73
73
|
|
|
74
74
|
## Agent runtimes
|
|
75
75
|
|
|
76
|
-
- **Claude Code.** Authenticate once with `claude /login`. Kōbō calls the embedded SDK directly
|
|
76
|
+
- **Claude Code.** Authenticate once with `claude /login`. Kōbō calls the embedded SDK directly, so no `claude` binary is required at runtime.
|
|
77
77
|
- **OpenAI Codex** (experimental). Run `codex login` or export `OPENAI_API_KEY`. Kōbō spawns a long-lived `codex app-server` subprocess per workspace and bridges its JSON-RPC stream to the same UI.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
You pick the engine at workspace creation. Both share the same task tracking, permission modes, sub-agent panel, and quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox + approval semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
|
|
80
80
|
|
|
81
81
|
## Disk-space purge
|
|
82
82
|
|
|
83
|
-
A merged workspace is automatically archived but its worktree folder usually carries a lot of weight (`node_modules`, `vendor`, build artefacts…). Kōbō
|
|
83
|
+
A merged workspace is automatically archived, but its worktree folder usually carries a lot of weight (`node_modules`, `vendor`, build artefacts…). Kōbō frees that space without losing anything queryable:
|
|
84
84
|
|
|
85
|
-
- **Manual
|
|
86
|
-
- **Automatic
|
|
87
|
-
- **Restore
|
|
85
|
+
- **Manual**: workspace context menu → *Free disk space (delete worktree)*. The worktree is removed; the chat history and PR metadata stay in the database.
|
|
86
|
+
- **Automatic**: **Settings → Worktrees → Auto-purge worktree on PR merged**. When the pr-watcher sees the OPEN → MERGED transition, it archives **and** purges.
|
|
87
|
+
- **Restore**: recreate the folder yourself (`gh pr checkout <pr>` or `git worktree add <path> <branch>`). The pr-watcher detects the directory reappearing within 30 seconds and re-activates the workspace automatically (clears purge flag, unarchives). No UI action needed.
|
|
88
88
|
|
|
89
89
|
### Avoiding permission errors during purge
|
|
90
90
|
|
|
91
91
|
Docker containers usually write as `root`, so files in `node_modules` / `vendor` end up root-owned on the host. Plain `rm -rf` (which Kōbō uses under the hood) then fails with `EACCES` / `EPERM`. Pick one of these strategies depending on your setup:
|
|
92
92
|
|
|
93
|
-
1. **Best
|
|
93
|
+
1. **Best: run your container as the host user.** Add a `USER` directive in your `Dockerfile`, or set `user: "${UID}:${GID}"` in `docker-compose.yml` with `UID`/`GID` exported in your shell. No more root-owned files, and nothing extra to do.
|
|
94
94
|
2. **Preventive ACL on the worktrees root.** On ext4 / btrfs / xfs with a regular Docker bind mount, a default ACL grants your user access to every file created later:
|
|
95
95
|
```bash
|
|
96
96
|
setfacl -d -m u:$(whoami):rwX <worktrees-root> # e.g. ~/.worktrees
|
|
@@ -112,28 +112,50 @@ When a purge does fail, Kōbō surfaces a toast with a copy-pasteable recovery c
|
|
|
112
112
|
|
|
113
113
|
Kōbō ships first-class support for three external systems. All are opt-in and reuse credentials you may already have configured for Claude Code.
|
|
114
114
|
|
|
115
|
-
- **Notion
|
|
116
|
-
- **Sentry
|
|
117
|
-
- **Voice transcription
|
|
115
|
+
- **Notion**: import missions, tasks, and acceptance criteria from a Notion page.
|
|
116
|
+
- **Sentry**: paste an issue URL to spawn a fix workspace with the stacktrace, tags, and a TDD workflow.
|
|
117
|
+
- **Voice transcription**: local push-to-talk via [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp).
|
|
118
118
|
|
|
119
119
|
See [`CONFIGURATION.md`](./CONFIGURATION.md) for the setup of each.
|
|
120
120
|
|
|
121
|
+
## Network access
|
|
122
|
+
|
|
123
|
+
By default, Kōbō binds to `127.0.0.1` only (localhost). To control Kōbō from
|
|
124
|
+
another device on the same Wi-Fi or LAN:
|
|
125
|
+
|
|
126
|
+
1. Open **Settings → Global → Network access** and enable it.
|
|
127
|
+
2. Restart Kōbō when prompted, since the server must re-bind to apply the change.
|
|
128
|
+
3. Scan the QR code shown in the Settings panel from your phone, or copy a LAN
|
|
129
|
+
URL and paste the token in the login dialog on the remote device.
|
|
130
|
+
|
|
131
|
+
> **Trusted networks only.** Kōbō uses plain HTTP, so the token travels in cleartext.
|
|
132
|
+
> Do not expose the port to the internet. For remote access over the internet,
|
|
133
|
+
> put a terminating HTTPS proxy or a VPN (e.g. Tailscale) in front of Kōbō.
|
|
134
|
+
>
|
|
135
|
+
> **Production only.** This protection applies when running a built Kōbō
|
|
136
|
+
> (`npm start` / `npx @loicngr/kobo`). In development (`npm run dev:all`) the Vite
|
|
137
|
+
> dev server is always exposed on the LAN and bypasses the token. See
|
|
138
|
+
> [`CONFIGURATION.md`](./CONFIGURATION.md#production-vs-development-mode-important).
|
|
139
|
+
|
|
140
|
+
See [`CONFIGURATION.md`](./CONFIGURATION.md#network-access) for token management,
|
|
141
|
+
QR code details, and all security caveats.
|
|
142
|
+
|
|
121
143
|
## Skill suites
|
|
122
144
|
|
|
123
145
|
Kōbō's auto-generated prompts (review, auto-loop grooming, QA, brainstorming) can target four different skill ecosystems, selectable in **Settings → Skills**:
|
|
124
146
|
|
|
125
|
-
- **[superpowers](https://github.com/obra/superpowers)** (default)
|
|
126
|
-
- **[gstack](https://github.com/garrytan/gstack)
|
|
127
|
-
- **superpowers + gstack
|
|
128
|
-
- **custom
|
|
147
|
+
- **[superpowers](https://github.com/obra/superpowers)** (default): a plugin for Claude Code with the brainstorm → spec → plan → execute discipline, TDD, debugging, code review.
|
|
148
|
+
- **[gstack](https://github.com/garrytan/gstack)**: CLI slash commands for navigation, QA, design review, ship pipeline, second-opinion via Codex.
|
|
149
|
+
- **superpowers + gstack**: both, with each used for what it does best.
|
|
150
|
+
- **custom**: write your own prompts.
|
|
129
151
|
|
|
130
|
-
Optionally pair with **[gbrain](https://github.com/garrytan/gbrain)
|
|
152
|
+
Optionally pair with **[gbrain](https://github.com/garrytan/gbrain)**, a per-project knowledge graph + semantic search exposed as an MCP server. It is inherited automatically from your `~/.claude.json` config.
|
|
131
153
|
|
|
132
154
|
Full install instructions and the prompt-suite differences are in [`CONFIGURATION.md`](./CONFIGURATION.md#skill-suites).
|
|
133
155
|
|
|
134
156
|
## Architecture
|
|
135
157
|
|
|
136
|
-
Hono backend, Vue 3 + Quasar SPA, SQLite (WAL) for persistence, WebSocket for live updates. Each workspace spawns its own agent engine and a dedicated MCP server (`kobo-tasks`) the agent uses to query and mutate workspace state.
|
|
158
|
+
Hono backend, Vue 3 + Quasar SPA, SQLite (WAL) for persistence, WebSocket for live updates. Each workspace spawns its own agent engine and a dedicated MCP server (`kobo-tasks`) that the agent uses to query and mutate workspace state.
|
|
137
159
|
|
|
138
160
|
```
|
|
139
161
|
src/
|
|
@@ -166,7 +188,7 @@ PRs welcome. Branch off `develop`, follow Conventional Commits, run `make ci` be
|
|
|
166
188
|
|
|
167
189
|
## Release
|
|
168
190
|
|
|
169
|
-
Releases are cut from `main`. Bump `package.json` on `develop`, merge into `main`, push. The release workflow builds, tests, publishes to npm, tags `v<version>`, and creates the GitHub Release
|
|
191
|
+
Releases are cut from `main`. Bump `package.json` on `develop`, merge into `main`, push. The release workflow builds, tests, publishes to npm, tags `v<version>`, and creates the GitHub Release. It fails early if the version or tag already exists.
|
|
170
192
|
|
|
171
193
|
## License
|
|
172
194
|
|
|
@@ -676,6 +676,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
676
676
|
delaySeconds,
|
|
677
677
|
prompt,
|
|
678
678
|
reason,
|
|
679
|
+
// Pin the calling session so the wakeup resumes THIS conversation (keeps
|
|
680
|
+
// context). The route now defaults to 'fresh' for manual UI scheduling;
|
|
681
|
+
// the agent must opt into 'resume' explicitly.
|
|
682
|
+
mode: 'resume',
|
|
679
683
|
});
|
|
680
684
|
return ok(result);
|
|
681
685
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Hono } from 'hono';
|
|
|
6
6
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
7
7
|
import { closeDb, getDb } from './db/index.js';
|
|
8
8
|
import { getPendingMigrations, runMigrations } from './db/migrations.js';
|
|
9
|
+
import { networkAuthMiddleware } from './middleware/network-auth-middleware.js';
|
|
9
10
|
import changelogRouter from './routes/changelog.js';
|
|
10
11
|
import devServerRouter from './routes/dev-server.js';
|
|
11
12
|
import documentsRouter from './routes/documents.js';
|
|
@@ -30,8 +31,11 @@ import { runContentMigrationIfNeeded } from './services/content-migration-servic
|
|
|
30
31
|
import * as cronService from './services/cron-service.js';
|
|
31
32
|
import { createDailyDbBackupIfNeeded, createPreMigrationBackup } from './services/db-backup-service.js';
|
|
32
33
|
import { startDevServer, stopDevServer } from './services/dev-server-service.js';
|
|
34
|
+
import { authorizeWsUpgrade, getLanUrls, resolveBindHost } from './services/network-access-service.js';
|
|
33
35
|
import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
|
|
34
36
|
import * as quotaBackoffService from './services/quota-backoff-service.js';
|
|
37
|
+
import { getGlobalSettings } from './services/settings-service.js';
|
|
38
|
+
import { reloadDefaultTemplates } from './services/templates-service.js';
|
|
35
39
|
import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
|
|
36
40
|
import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
|
|
37
41
|
import * as wakeupService from './services/wakeup-service.js';
|
|
@@ -79,10 +83,23 @@ autoLoopService.rehydrate();
|
|
|
79
83
|
restoreRetryCountsFromDb();
|
|
80
84
|
quotaBackoffService.restoreOnBoot((workspaceId) => autoLoopService.onQuotaBackoffExpired(workspaceId));
|
|
81
85
|
cronService.restoreOnBoot();
|
|
86
|
+
// Deliver any new default prompt templates to existing installs (seed-once via the
|
|
87
|
+
// seededDefaultSlugs watermark; never overwrites or re-adds deleted defaults).
|
|
88
|
+
try {
|
|
89
|
+
const { added } = reloadDefaultTemplates();
|
|
90
|
+
if (added.length > 0) {
|
|
91
|
+
console.log(`[templates] Added ${added.length} new default template(s): ${added.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error('[templates] reloadDefaultTemplates on boot failed:', err);
|
|
96
|
+
}
|
|
82
97
|
startPrWatcher();
|
|
83
98
|
startUsagePoller();
|
|
84
99
|
// Create Hono app
|
|
85
100
|
const app = new Hono();
|
|
101
|
+
// Gate non-loopback requests behind the network-access token (loopback exempt).
|
|
102
|
+
app.use('/api/*', networkAuthMiddleware);
|
|
86
103
|
// Health check (root / is handled by the SPA catch-all below)
|
|
87
104
|
app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion() }));
|
|
88
105
|
// Mount route sub-routers
|
|
@@ -148,12 +165,21 @@ if (clientDistPath) {
|
|
|
148
165
|
});
|
|
149
166
|
}
|
|
150
167
|
// Create HTTP server via @hono/node-server
|
|
168
|
+
const bindHost = resolveBindHost(getGlobalSettings().networkAccessEnabled);
|
|
151
169
|
const server = serve({
|
|
152
170
|
fetch: app.fetch,
|
|
153
171
|
port: PORT,
|
|
172
|
+
hostname: bindHost,
|
|
154
173
|
}, (info) => {
|
|
155
174
|
setBackendPort(info.port);
|
|
156
|
-
|
|
175
|
+
if (getGlobalSettings().networkAccessEnabled) {
|
|
176
|
+
console.log(`Server running — network access ON (port ${info.port})`);
|
|
177
|
+
for (const url of getLanUrls(info.port))
|
|
178
|
+
console.log(` LAN: ${url}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(`Server running at http://localhost:${info.port} (localhost only)`);
|
|
182
|
+
}
|
|
157
183
|
// Content migration runs AFTER the HTTP listener is up so the frontend
|
|
158
184
|
// can observe progress via WS broadcasts + GET /api/migration/status.
|
|
159
185
|
// Not awaited — the callback returns quickly, the migration runs in the
|
|
@@ -360,6 +386,18 @@ setMessageHandler((type, payload) => {
|
|
|
360
386
|
// Handle WebSocket upgrade requests on /ws path
|
|
361
387
|
server.on('upgrade', (request, socket, head) => {
|
|
362
388
|
const { pathname } = new URL(request.url ?? '/', `http://localhost:${PORT}`);
|
|
389
|
+
const wsGlobal = getGlobalSettings();
|
|
390
|
+
if (!authorizeWsUpgrade({
|
|
391
|
+
address: request.socket.remoteAddress,
|
|
392
|
+
rawUrl: request.url,
|
|
393
|
+
enabled: wsGlobal.networkAccessEnabled,
|
|
394
|
+
expectedToken: wsGlobal.networkAccessToken,
|
|
395
|
+
})) {
|
|
396
|
+
console.warn(`[network-auth] WS 401 (missing/invalid token) from ${request.socket.remoteAddress ?? 'unknown'} ${pathname}`);
|
|
397
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
398
|
+
socket.destroy();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
363
401
|
if (pathname === '/ws') {
|
|
364
402
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
365
403
|
wss.emit('connection', ws, request);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getConnInfo } from '@hono/node-server/conninfo';
|
|
2
|
+
import { evaluateNetworkAccess } from '../services/network-access-service.js';
|
|
3
|
+
import { getGlobalSettings } from '../services/settings-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* Gates non-loopback requests behind the network-access token.
|
|
6
|
+
*
|
|
7
|
+
* Loopback requests always pass (the host machine's own usage is frictionless).
|
|
8
|
+
* The client IP comes only from the OS socket via getConnInfo, never from
|
|
9
|
+
* X-Forwarded-For, so a remote client cannot spoof a loopback address.
|
|
10
|
+
*/
|
|
11
|
+
export const networkAuthMiddleware = async (c, next) => {
|
|
12
|
+
const address = getConnInfo(c).remote.address;
|
|
13
|
+
const global = getGlobalSettings();
|
|
14
|
+
const decision = evaluateNetworkAccess({
|
|
15
|
+
address,
|
|
16
|
+
enabled: global.networkAccessEnabled,
|
|
17
|
+
expectedToken: global.networkAccessToken,
|
|
18
|
+
providedToken: c.req.header('X-Kobo-Token'),
|
|
19
|
+
});
|
|
20
|
+
if (decision.allow)
|
|
21
|
+
return next();
|
|
22
|
+
// Surface denied requests so "my device can't connect" is debuggable.
|
|
23
|
+
// Never log the token itself, only the reason and the remote address.
|
|
24
|
+
const reason = decision.status === 403 ? 'network access disabled' : 'missing/invalid token';
|
|
25
|
+
console.warn(`[network-auth] HTTP ${decision.status} (${reason}) from ${address ?? 'unknown'} ${c.req.method} ${c.req.path}`);
|
|
26
|
+
return c.json({ error: 'unauthorized' }, decision.status);
|
|
27
|
+
};
|