@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
|
@@ -12,7 +12,7 @@ export function parseChangelog(markdown) {
|
|
|
12
12
|
const entries = [];
|
|
13
13
|
let current = null;
|
|
14
14
|
for (const line of markdown.split('\n')) {
|
|
15
|
-
const heading = line.match(/^##\s+v?(\d+\.\d+\.\d+[\w
|
|
15
|
+
const heading = line.match(/^##\s+v?(\d+\.\d+\.\d+[\w./-]*)\s*$/);
|
|
16
16
|
if (heading) {
|
|
17
17
|
if (current)
|
|
18
18
|
entries.push({ version: current.version, notes: current.lines.join('\n').trim() });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { getBackendPort } from '../services/agent/orchestrator.js';
|
|
2
3
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
|
|
4
|
+
import { generateToken, getLanUrls } from '../services/network-access-service.js';
|
|
3
5
|
import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from '../services/review-template-service.js';
|
|
4
6
|
import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaults.js';
|
|
5
7
|
import * as settingsService from '../services/settings-service.js';
|
|
@@ -45,6 +47,53 @@ app.get('/defaults', (c) => {
|
|
|
45
47
|
changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
|
|
46
48
|
});
|
|
47
49
|
});
|
|
50
|
+
// GET /api/settings/network — network access state + LAN URLs
|
|
51
|
+
app.get('/network', (c) => {
|
|
52
|
+
try {
|
|
53
|
+
const global = settingsService.getGlobalSettings();
|
|
54
|
+
return c.json({
|
|
55
|
+
enabled: global.networkAccessEnabled,
|
|
56
|
+
token: global.networkAccessToken,
|
|
57
|
+
urls: getLanUrls(getBackendPort()),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return c.json({ error: message }, 500);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// GET /api/settings/network/ping — token validation probe (behind the gate)
|
|
66
|
+
app.get('/network/ping', (c) => c.json({ ok: true }));
|
|
67
|
+
// POST /api/settings/network — toggle enabled / regenerate token
|
|
68
|
+
app.post('/network', async (c) => {
|
|
69
|
+
try {
|
|
70
|
+
const body = await c.req.json();
|
|
71
|
+
const current = settingsService.getGlobalSettings();
|
|
72
|
+
const patch = {};
|
|
73
|
+
let restartRequired = false;
|
|
74
|
+
if (typeof body.enabled === 'boolean' && body.enabled !== current.networkAccessEnabled) {
|
|
75
|
+
patch.networkAccessEnabled = body.enabled;
|
|
76
|
+
restartRequired = true;
|
|
77
|
+
if (body.enabled && !current.networkAccessToken) {
|
|
78
|
+
patch.networkAccessToken = generateToken();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (body.regenerate) {
|
|
82
|
+
patch.networkAccessToken = generateToken();
|
|
83
|
+
}
|
|
84
|
+
const updated = settingsService.updateNetworkAccessSettings(patch);
|
|
85
|
+
return c.json({
|
|
86
|
+
enabled: updated.networkAccessEnabled,
|
|
87
|
+
token: updated.networkAccessToken,
|
|
88
|
+
urls: getLanUrls(getBackendPort()),
|
|
89
|
+
restartRequired,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return c.json({ error: message }, 500);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
48
97
|
// GET /api/settings/mcp-servers — list active MCP servers from Claude config
|
|
49
98
|
app.get('/mcp-servers', (c) => {
|
|
50
99
|
try {
|
|
@@ -1124,8 +1124,9 @@ app.delete('/:id/quota-backoff', (c) => {
|
|
|
1124
1124
|
return c.json({ error: message }, 500);
|
|
1125
1125
|
}
|
|
1126
1126
|
});
|
|
1127
|
-
// POST /api/workspaces/:id/pending-wakeup —
|
|
1128
|
-
// `kobo__schedule_wakeup` MCP tool
|
|
1127
|
+
// POST /api/workspaces/:id/pending-wakeup — schedule a wakeup, either from
|
|
1128
|
+
// the `kobo__schedule_wakeup` MCP tool (agent, mode='resume') or from the UI
|
|
1129
|
+
// (manual, mode='fresh', default). Replaces any existing pending wakeup.
|
|
1129
1130
|
app.post('/:id/pending-wakeup', async (c) => {
|
|
1130
1131
|
try {
|
|
1131
1132
|
const id = c.req.param('id');
|
|
@@ -1133,6 +1134,7 @@ app.post('/:id/pending-wakeup', async (c) => {
|
|
|
1133
1134
|
const delaySeconds = body.delaySeconds;
|
|
1134
1135
|
const prompt = body.prompt;
|
|
1135
1136
|
const reason = body.reason;
|
|
1137
|
+
const rawMode = typeof body.mode === 'string' ? body.mode : 'fresh';
|
|
1136
1138
|
if (typeof delaySeconds !== 'number' || !Number.isFinite(delaySeconds) || delaySeconds <= 0) {
|
|
1137
1139
|
return c.json({ error: 'delaySeconds must be a positive number' }, 400);
|
|
1138
1140
|
}
|
|
@@ -1142,14 +1144,14 @@ app.post('/:id/pending-wakeup', async (c) => {
|
|
|
1142
1144
|
if (reason !== undefined && typeof reason !== 'string') {
|
|
1143
1145
|
return c.json({ error: 'reason must be a string when provided' }, 400);
|
|
1144
1146
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
// at fire time. The MCP tool is invoked from inside an active session, so
|
|
1148
|
-
// a missing controller signals misuse — reject explicitly.
|
|
1149
|
-
const agentSessionId = agentManager.getActiveSessionId(id);
|
|
1150
|
-
if (!agentSessionId) {
|
|
1151
|
-
return c.json({ error: 'no active agent session for this workspace' }, 409);
|
|
1147
|
+
if (rawMode !== 'fresh' && rawMode !== 'resume') {
|
|
1148
|
+
return c.json({ error: "mode must be 'fresh' or 'resume'" }, 400);
|
|
1152
1149
|
}
|
|
1150
|
+
// 'resume' pins the active session so the wakeup resumes that conversation;
|
|
1151
|
+
// 'fresh' (default) — or 'resume' with no active session — leaves it unpinned
|
|
1152
|
+
// so the wakeup fires a brand-new session running `prompt`. This is what makes
|
|
1153
|
+
// manual scheduling on an idle workspace possible (no more hard 409).
|
|
1154
|
+
const agentSessionId = rawMode === 'resume' ? (agentManager.getActiveSessionId(id) ?? undefined) : undefined;
|
|
1153
1155
|
wakeupService.schedule(id, delaySeconds, prompt, reason, agentSessionId);
|
|
1154
1156
|
const pending = wakeupService.getPending(id);
|
|
1155
1157
|
return c.json({ ok: true, pending });
|
|
@@ -2658,6 +2660,23 @@ app.post('/:id/push', async (c) => {
|
|
|
2658
2660
|
return c.json({ error: message }, 500);
|
|
2659
2661
|
}
|
|
2660
2662
|
});
|
|
2663
|
+
// POST /api/workspaces/:id/fetch — git fetch the workspace repo (all branches of origin).
|
|
2664
|
+
// Read-only: updates remote-tracking refs, never touches the working tree.
|
|
2665
|
+
app.post('/:id/fetch', (c) => {
|
|
2666
|
+
try {
|
|
2667
|
+
const id = c.req.param('id');
|
|
2668
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
2669
|
+
if (!workspace) {
|
|
2670
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2671
|
+
}
|
|
2672
|
+
gitOps.fetchAllBranches(workspace.worktreePath);
|
|
2673
|
+
return c.json({ ok: true });
|
|
2674
|
+
}
|
|
2675
|
+
catch (err) {
|
|
2676
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2677
|
+
return c.json({ error: message }, 500);
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2661
2680
|
// POST /api/workspaces/:id/pull — pull working branch from origin (fast-forward only)
|
|
2662
2681
|
app.post('/:id/pull', (c) => {
|
|
2663
2682
|
try {
|
|
@@ -5,6 +5,7 @@ import { createMapperState, mapSdkMessage, QUOTA_PATTERN, tryEmitQuota } from '.
|
|
|
5
5
|
import { buildClaudeOptions } from './options-builder.js';
|
|
6
6
|
import { buildCompactionSessionStartOutput } from './precompact-hook.js';
|
|
7
7
|
import { resolveClaudeBinaryPath } from './resolve-binary.js';
|
|
8
|
+
import { buildStopHookOutput } from './stop-hook.js';
|
|
8
9
|
/**
|
|
9
10
|
* Grace window between the SDK's terminal `result` message and the generator
|
|
10
11
|
* reaching `done`. A healthy run closes within milliseconds; if the generator
|
|
@@ -89,6 +90,19 @@ export function createClaudeCodeEngine() {
|
|
|
89
90
|
],
|
|
90
91
|
},
|
|
91
92
|
],
|
|
93
|
+
// Decision-point enforcement of the "schedule a wakeup or the session
|
|
94
|
+
// stalls" invariant: when the agent tries to end its turn with
|
|
95
|
+
// background work still in flight and nothing scheduled to resume the
|
|
96
|
+
// session, inject a reminder so it calls `kobo__schedule_wakeup` instead
|
|
97
|
+
// of going idle. A passive system-prompt rule isn't enough — see
|
|
98
|
+
// stop-hook.ts. `additionalContext` continues the turn so the model acts.
|
|
99
|
+
Stop: [
|
|
100
|
+
{
|
|
101
|
+
hooks: [
|
|
102
|
+
async (input) => buildStopHookOutput(options.workspaceId, input),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
92
106
|
};
|
|
93
107
|
const { options: sdkOptions, effectivePrompt } = buildClaudeOptions({
|
|
94
108
|
prompt: options.prompt,
|
|
@@ -167,10 +181,29 @@ export function createClaudeCodeEngine() {
|
|
|
167
181
|
console.warn(`[claude-engine] SDK generator still open ${RESULT_DRAIN_TIMEOUT_MS}ms after 'result' — forcing session:ended`);
|
|
168
182
|
const reason = userInterrupted ? 'killed' : mapperState.sawErrorResult ? 'error' : 'completed';
|
|
169
183
|
emitSessionEnded(reason, reason === 'completed' ? 0 : null);
|
|
170
|
-
//
|
|
171
|
-
// tear down.
|
|
172
|
-
// the
|
|
173
|
-
|
|
184
|
+
// Normally we abort here to unstick the SDK so its subprocesses / MCP
|
|
185
|
+
// children tear down. BUT if a wakeup is scheduled for this workspace,
|
|
186
|
+
// the agent intentionally left background work running for the wakeup
|
|
187
|
+
// to resume and check — aborting would kill that work. Skip the abort
|
|
188
|
+
// in that case so the background tasks survive. `session:ended` was
|
|
189
|
+
// already emitted (so the orchestrator/auto-loop proceed and the
|
|
190
|
+
// controller is removed, letting the wakeup fire later); the parked
|
|
191
|
+
// generator drains on its own once the background work finishes.
|
|
192
|
+
//
|
|
193
|
+
// The wakeup check is a dynamic import: a static `import` of
|
|
194
|
+
// wakeup-service here would form an engine → wakeup-service →
|
|
195
|
+
// orchestrator → engine cycle that breaks module init under the test
|
|
196
|
+
// SSR transform. Resolving it at call-time (once, 15s after result)
|
|
197
|
+
// sidesteps the cycle with no hot-path cost.
|
|
198
|
+
void (async () => {
|
|
199
|
+
const { isWakeupScheduled } = await import('../../../wakeup-service.js');
|
|
200
|
+
if (isWakeupScheduled(options.workspaceId)) {
|
|
201
|
+
console.warn(`[claude-engine] generator still open ${RESULT_DRAIN_TIMEOUT_MS}ms after 'result' but a wakeup is scheduled for ${options.workspaceId} — leaving background work alive (no abort)`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
abortController.abort();
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
174
207
|
}, RESULT_DRAIN_TIMEOUT_MS);
|
|
175
208
|
resultDrainTimer.unref?.();
|
|
176
209
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isWakeupScheduled } from '../../../wakeup-service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decide whether to nudge the agent to schedule a wakeup before it ends its
|
|
4
|
+
* turn. The agent should be reminded ONLY when it leaves background work
|
|
5
|
+
* in-flight with nothing scheduled to resume the session — otherwise the Kōbō
|
|
6
|
+
* session goes idle and the work stalls.
|
|
7
|
+
*
|
|
8
|
+
* Returns false (clean stop, no nudge) when:
|
|
9
|
+
* - the stop hook is already active (anti-loop — we nudged on the prior stop);
|
|
10
|
+
* - there is no in-flight background work (a genuine end of turn);
|
|
11
|
+
* - an SDK-level cron/wakeup is scheduled (`session_crons`); or
|
|
12
|
+
* - a Kōbō-level wakeup is scheduled (`pending_wakeups`).
|
|
13
|
+
*/
|
|
14
|
+
export function shouldNudgeWakeup(input) {
|
|
15
|
+
if (input.stopHookActive)
|
|
16
|
+
return false;
|
|
17
|
+
if (input.backgroundTaskCount <= 0)
|
|
18
|
+
return false;
|
|
19
|
+
if (input.sdkScheduledWakeupCount > 0)
|
|
20
|
+
return false;
|
|
21
|
+
if (input.koboWakeupScheduled)
|
|
22
|
+
return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/** The decision-point reminder injected when {@link shouldNudgeWakeup} is true. */
|
|
26
|
+
export function buildNudgeText(backgroundTaskCount) {
|
|
27
|
+
return [
|
|
28
|
+
`⚠️ [Kōbō] You are about to end your turn with ${backgroundTaskCount} background task(s) still running, but NO wakeup is scheduled.`,
|
|
29
|
+
'This Kōbō session will go idle and will NOT resume itself — the background work will stall and never be checked.',
|
|
30
|
+
'',
|
|
31
|
+
'Before ending your turn, do ONE of:',
|
|
32
|
+
'• If you need that work to finish: call `kobo__schedule_wakeup` now with `delaySeconds` ≈ the expected remaining duration and a `prompt` telling future-you exactly what to check (log path + next step). Then end the turn.',
|
|
33
|
+
'• If the background work is no longer needed: stop it (kill the process), then end the turn.',
|
|
34
|
+
'',
|
|
35
|
+
'Do not end the turn passively waiting — nothing will wake the session.',
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the Stop hook output for a workspace: cross-checks the SDK signals with
|
|
40
|
+
* Kōbō's own `pending_wakeups` table (since `kobo__schedule_wakeup` is an MCP
|
|
41
|
+
* tool that never appears in the SDK's `session_crons`) and returns the wakeup
|
|
42
|
+
* nudge only when the agent is genuinely about to stall.
|
|
43
|
+
*/
|
|
44
|
+
export function buildStopHookOutput(workspaceId, input) {
|
|
45
|
+
const backgroundTaskCount = input.background_tasks?.length ?? 0;
|
|
46
|
+
const koboWakeupScheduled = isWakeupScheduled(workspaceId);
|
|
47
|
+
const nudge = shouldNudgeWakeup({
|
|
48
|
+
stopHookActive: input.stop_hook_active ?? false,
|
|
49
|
+
backgroundTaskCount,
|
|
50
|
+
sdkScheduledWakeupCount: input.session_crons?.length ?? 0,
|
|
51
|
+
koboWakeupScheduled,
|
|
52
|
+
});
|
|
53
|
+
if (!nudge)
|
|
54
|
+
return {};
|
|
55
|
+
return { hookSpecificOutput: { hookEventName: 'Stop', additionalContext: buildNudgeText(backgroundTaskCount) } };
|
|
56
|
+
}
|
|
@@ -22,6 +22,10 @@ let backendPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 300
|
|
|
22
22
|
export function setBackendPort(port) {
|
|
23
23
|
backendPort = port;
|
|
24
24
|
}
|
|
25
|
+
/** Current bound port of the running backend. */
|
|
26
|
+
export function getBackendPort() {
|
|
27
|
+
return backendPort;
|
|
28
|
+
}
|
|
25
29
|
/** workspaceId -> SessionController */
|
|
26
30
|
const controllers = new Map();
|
|
27
31
|
/** workspaceId -> last engine session ID (for resume) */
|
|
@@ -72,15 +72,21 @@ function nextAfter(expression, from) {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Validate the expression, persist the row, arm a setTimeout for the next
|
|
75
|
-
* fire, emit `cron:created`. Throws on invalid expression
|
|
76
|
-
*
|
|
75
|
+
* fire, emit `cron:created`. Throws on an invalid expression. If the first
|
|
76
|
+
* occurrence falls within MIN_DELAY_BETWEEN_FIRES_SECONDS of now, that imminent
|
|
77
|
+
* fire is SKIPPED (scheduled to the following occurrence) rather than rejected
|
|
78
|
+
* — so a valid recurring cron created near a boundary (e.g. a 15-min cron made
|
|
79
|
+
* 30s before the tick, or a daily cron made shortly before its time) still
|
|
80
|
+
* arms, and never fires within a minute of creation.
|
|
77
81
|
*/
|
|
78
82
|
export function arm(workspaceId, args) {
|
|
79
83
|
const now = new Date();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
let next = nextAfter(args.expression, now);
|
|
85
|
+
if (next.getTime() - now.getTime() < MIN_DELAY_BETWEEN_FIRES_SECONDS * 1000) {
|
|
86
|
+
// Skip the imminent occurrence. The one after is guaranteed ≥60s out for
|
|
87
|
+
// any standard 5-field cron (min granularity is 1 minute), so a single skip
|
|
88
|
+
// always suffices.
|
|
89
|
+
next = nextAfter(args.expression, next);
|
|
84
90
|
}
|
|
85
91
|
const id = nanoid();
|
|
86
92
|
const db = getDb();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
4
|
+
/** True for loopback remote addresses. Undefined → false (deny-safe). */
|
|
5
|
+
export function isLoopbackAddress(address) {
|
|
6
|
+
if (!address)
|
|
7
|
+
return false;
|
|
8
|
+
return LOOPBACK_ADDRESSES.has(address);
|
|
9
|
+
}
|
|
10
|
+
/** Bind host for `serve()`: localhost-only when disabled, all interfaces when enabled. */
|
|
11
|
+
export function resolveBindHost(enabled) {
|
|
12
|
+
return enabled ? undefined : '127.0.0.1';
|
|
13
|
+
}
|
|
14
|
+
/** Non-internal IPv4 URLs for the running server, for display + QR. */
|
|
15
|
+
export function getLanUrls(port) {
|
|
16
|
+
const urls = [];
|
|
17
|
+
for (const infos of Object.values(os.networkInterfaces())) {
|
|
18
|
+
if (!infos)
|
|
19
|
+
continue;
|
|
20
|
+
for (const info of infos) {
|
|
21
|
+
if (info.family === 'IPv4' && !info.internal) {
|
|
22
|
+
urls.push(`http://${info.address}:${port}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return urls;
|
|
27
|
+
}
|
|
28
|
+
/** ~32-char url-safe random token. */
|
|
29
|
+
export function generateToken() {
|
|
30
|
+
return crypto.randomBytes(24).toString('base64url');
|
|
31
|
+
}
|
|
32
|
+
/** Constant-time token comparison; false on empty/length mismatch (never throws). */
|
|
33
|
+
export function tokenMatches(provided, expected) {
|
|
34
|
+
if (!provided || !expected)
|
|
35
|
+
return false;
|
|
36
|
+
const a = Buffer.from(provided);
|
|
37
|
+
const b = Buffer.from(expected);
|
|
38
|
+
if (a.length !== b.length)
|
|
39
|
+
return false;
|
|
40
|
+
return crypto.timingSafeEqual(a, b);
|
|
41
|
+
}
|
|
42
|
+
/** Core gate decision shared by the HTTP middleware and the WS upgrade guard. */
|
|
43
|
+
export function evaluateNetworkAccess(params) {
|
|
44
|
+
if (isLoopbackAddress(params.address))
|
|
45
|
+
return { allow: true, status: 200 };
|
|
46
|
+
if (!params.enabled)
|
|
47
|
+
return { allow: false, status: 403 };
|
|
48
|
+
if (tokenMatches(params.providedToken, params.expectedToken))
|
|
49
|
+
return { allow: true, status: 200 };
|
|
50
|
+
return { allow: false, status: 401 };
|
|
51
|
+
}
|
|
52
|
+
/** WS upgrade authorization: parses `?token=` from the raw URL. */
|
|
53
|
+
export function authorizeWsUpgrade(params) {
|
|
54
|
+
let providedToken;
|
|
55
|
+
try {
|
|
56
|
+
providedToken = new URL(params.rawUrl ?? '/', 'http://localhost').searchParams.get('token') ?? undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
providedToken = undefined;
|
|
60
|
+
}
|
|
61
|
+
return evaluateNetworkAccess({
|
|
62
|
+
address: params.address,
|
|
63
|
+
enabled: params.enabled,
|
|
64
|
+
expectedToken: params.expectedToken,
|
|
65
|
+
providedToken,
|
|
66
|
+
}).allow;
|
|
67
|
+
}
|
|
@@ -629,6 +629,31 @@ const settingsMigrations = [
|
|
|
629
629
|
}
|
|
630
630
|
},
|
|
631
631
|
},
|
|
632
|
+
{
|
|
633
|
+
version: 39,
|
|
634
|
+
name: 'add-network-access',
|
|
635
|
+
migrate: ({ global }) => {
|
|
636
|
+
// Opt-in LAN access with token auth. Disabled by default (localhost-only
|
|
637
|
+
// bind). Token is generated lazily on first enable via POST /network.
|
|
638
|
+
if (typeof global.networkAccessEnabled !== 'boolean') {
|
|
639
|
+
global.networkAccessEnabled = false;
|
|
640
|
+
}
|
|
641
|
+
if (typeof global.networkAccessToken !== 'string') {
|
|
642
|
+
global.networkAccessToken = '';
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
version: 40,
|
|
648
|
+
name: 'add-question-notification-sound',
|
|
649
|
+
migrate: ({ global }) => {
|
|
650
|
+
// Distinct sound played when the agent asks a question. Defaults to 'hey.mp3'
|
|
651
|
+
// so questions are audibly distinct from a customised task-done sound.
|
|
652
|
+
if (typeof global.audioQuestionSound !== 'string' || global.audioQuestionSound.length === 0) {
|
|
653
|
+
global.audioQuestionSound = 'hey.mp3';
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
},
|
|
632
657
|
];
|
|
633
658
|
/** Current settings schema version — always equals the highest migration version. */
|
|
634
659
|
export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
|
|
@@ -675,9 +700,12 @@ function defaultSettings() {
|
|
|
675
700
|
fileManagerCommand: '',
|
|
676
701
|
terminalCommand: '',
|
|
677
702
|
autoPurgeOnPrMerged: false,
|
|
703
|
+
networkAccessEnabled: false,
|
|
704
|
+
networkAccessToken: '',
|
|
678
705
|
browserNotifications: true,
|
|
679
706
|
audioNotifications: true,
|
|
680
707
|
audioNotificationSound: 'hey.mp3',
|
|
708
|
+
audioQuestionSound: 'hey.mp3',
|
|
681
709
|
audioNotificationVolume: 1,
|
|
682
710
|
notionStatusProperty: '',
|
|
683
711
|
notionInProgressStatus: '',
|
|
@@ -867,7 +895,7 @@ export function getSettings() {
|
|
|
867
895
|
return readSettings();
|
|
868
896
|
}
|
|
869
897
|
/** Keys stripped from exports — secrets that should stay on the machine. */
|
|
870
|
-
const SECRET_GLOBAL_KEYS = ['notionMcpKey', 'sentryMcpKey'];
|
|
898
|
+
const SECRET_GLOBAL_KEYS = ['notionMcpKey', 'sentryMcpKey', 'networkAccessToken'];
|
|
871
899
|
/** Build an export bundle with settings + templates. MCP keys are stripped. */
|
|
872
900
|
export function exportConfigBundle(templates) {
|
|
873
901
|
const settings = readSettings();
|
|
@@ -1033,6 +1061,7 @@ export function updateGlobalSettings(data) {
|
|
|
1033
1061
|
'browserNotifications',
|
|
1034
1062
|
'audioNotifications',
|
|
1035
1063
|
'audioNotificationSound',
|
|
1064
|
+
'audioQuestionSound',
|
|
1036
1065
|
'audioNotificationVolume',
|
|
1037
1066
|
'notionStatusProperty',
|
|
1038
1067
|
'notionInProgressStatus',
|
|
@@ -1093,6 +1122,26 @@ export function updateGlobalSettings(data) {
|
|
|
1093
1122
|
writeSettings(settings, { backup: true });
|
|
1094
1123
|
return settings.global;
|
|
1095
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Persist network-access settings (enabled flag and/or token) directly.
|
|
1127
|
+
*
|
|
1128
|
+
* Network access is managed exclusively through this path (POST /api/settings/network),
|
|
1129
|
+
* never the generic `updateGlobalSettings` allowlist: `networkAccessToken` is a secret
|
|
1130
|
+
* (kept out of the allowlist so a config import can't inject one) and `networkAccessEnabled`
|
|
1131
|
+
* is kept out too so it can't be flipped on via PUT /global without an accompanying token
|
|
1132
|
+
* (which would leave the server bound wide but unauthenticatable after a restart).
|
|
1133
|
+
*/
|
|
1134
|
+
export function updateNetworkAccessSettings(patch) {
|
|
1135
|
+
const settings = readSettings();
|
|
1136
|
+
if (typeof patch.networkAccessEnabled === 'boolean') {
|
|
1137
|
+
settings.global.networkAccessEnabled = patch.networkAccessEnabled;
|
|
1138
|
+
}
|
|
1139
|
+
if (typeof patch.networkAccessToken === 'string') {
|
|
1140
|
+
settings.global.networkAccessToken = patch.networkAccessToken;
|
|
1141
|
+
}
|
|
1142
|
+
writeSettings(settings, { backup: true });
|
|
1143
|
+
return settings.global;
|
|
1144
|
+
}
|
|
1096
1145
|
function ensureGlobalWorktreesRootExists(worktreesPath) {
|
|
1097
1146
|
const root = resolveGlobalWorktreesRoot(worktreesPath);
|
|
1098
1147
|
if (!root || isNonNativeWindowsPath(root))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getTemplatesPath } from '../utils/paths.js';
|
|
4
|
-
const CURRENT_FILE_VERSION =
|
|
4
|
+
const CURRENT_FILE_VERSION = 2;
|
|
5
5
|
const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6
6
|
const MAX_CONTENT_LENGTH = 4096;
|
|
7
7
|
const MAX_DESCRIPTION_LENGTH = 120;
|
|
@@ -117,10 +117,23 @@ function validateTemplateInput(input) {
|
|
|
117
117
|
throw new Error(`Invalid content: must be 1..${MAX_CONTENT_LENGTH} chars`);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
function
|
|
120
|
+
function readSeededSlugs() {
|
|
121
|
+
const filePath = getTemplatesPath();
|
|
122
|
+
if (!existsSync(filePath))
|
|
123
|
+
return undefined;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
126
|
+
return Array.isArray(parsed.seededDefaultSlugs) ? parsed.seededDefaultSlugs : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function writeTemplates(templates, seededDefaultSlugs) {
|
|
121
133
|
const filePath = getTemplatesPath();
|
|
122
134
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
123
|
-
const
|
|
135
|
+
const seeded = seededDefaultSlugs ?? readSeededSlugs() ?? [...LEGACY_DEFAULT_SLUGS];
|
|
136
|
+
const file = { version: CURRENT_FILE_VERSION, templates, seededDefaultSlugs: seeded };
|
|
124
137
|
writeFileSync(filePath, JSON.stringify(file, null, 2), 'utf-8');
|
|
125
138
|
}
|
|
126
139
|
/**
|
|
@@ -154,6 +167,24 @@ export function replaceAllTemplates(templates) {
|
|
|
154
167
|
}
|
|
155
168
|
writeTemplates(validated);
|
|
156
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The default slugs that existed BEFORE the seededDefaultSlugs watermark shipped.
|
|
172
|
+
* Bootstraps the watermark for pre-watermark installs so a default the user deleted
|
|
173
|
+
* earlier is not re-added on the first boot. Frozen historical snapshot; never change it.
|
|
174
|
+
*/
|
|
175
|
+
const LEGACY_DEFAULT_SLUGS = [
|
|
176
|
+
'kobo-context',
|
|
177
|
+
'review-quality',
|
|
178
|
+
'add-tests',
|
|
179
|
+
'explain',
|
|
180
|
+
'refactor',
|
|
181
|
+
'plan-tasks',
|
|
182
|
+
'show-tasks',
|
|
183
|
+
'mark-done',
|
|
184
|
+
'sync-tasks',
|
|
185
|
+
'pr-review-comments',
|
|
186
|
+
'ci-status',
|
|
187
|
+
];
|
|
157
188
|
export const DEFAULT_TEMPLATES = [
|
|
158
189
|
{
|
|
159
190
|
slug: 'kobo-context',
|
|
@@ -192,7 +223,9 @@ export const DEFAULT_TEMPLATES = [
|
|
|
192
223
|
`# Boundaries\n` +
|
|
193
224
|
`- The user owns the \`description\` field of the workspace — never write it; you only own \`agent_description\`\n` +
|
|
194
225
|
`- The user can interrupt you at any time via the chat; treat their messages as authoritative redirections\n` +
|
|
195
|
-
`- Auto-loop is automatically disabled if the user sends a chat message during a loop — they'll re-enable it manually after\n
|
|
226
|
+
`- Auto-loop is automatically disabled if the user sends a chat message during a loop — they'll re-enable it manually after\n` +
|
|
227
|
+
`\n# Decision support\n` +
|
|
228
|
+
`For a high-stakes engineering decision with no obvious answer (architecture choice, risky refactor, a real tradeoff), run a "council": the \`/council\` template spawns 5 sub-agent advisors with distinct lenses, peer-reviews them, and synthesises a verdict into \`.ai/thoughts/\`. It costs ~11 sub-agent calls, so use it sparingly.\n`,
|
|
196
229
|
},
|
|
197
230
|
{
|
|
198
231
|
slug: 'review-quality',
|
|
@@ -244,28 +277,64 @@ export const DEFAULT_TEMPLATES = [
|
|
|
244
277
|
description: 'Check GitHub Actions status on PR',
|
|
245
278
|
content: 'Check the CI/CD status for the pull request on branch {working_branch}.\n\nIf a PR exists (PR {pr_url}):\n1. Use the GitHub MCP tools to list the check runs / status checks on the latest commit of the PR\n2. For each check, report:\n - Check name\n - Status (queued, in_progress, completed)\n - Conclusion (success, failure, neutral, skipped, etc.)\n - Duration if available\n3. If any checks failed, fetch the logs or annotations and summarize what went wrong\n4. Give an overall summary: all green, some failing, or still running\n\nIf no PR exists, say so and suggest creating one first.',
|
|
246
279
|
},
|
|
280
|
+
{
|
|
281
|
+
slug: 'council',
|
|
282
|
+
description: 'Pressure-test a high-stakes decision with 5 sub-agent advisors',
|
|
283
|
+
content: `Run a "council" to pressure-test a high-stakes decision for this workspace ("{workspace_name}").\n\n` +
|
|
284
|
+
`The decision: take what the user described in this message. If they gave none, ask for it in one line and stop.\n\n` +
|
|
285
|
+
`You orchestrate this with your own sub-agents:\n\n` +
|
|
286
|
+
`1. Spawn 5 sub-agents IN PARALLEL, one per lens. Each answers the decision independently in 150-300 words, fully in its lens, no hedging, no false balance:\n` +
|
|
287
|
+
` - Contrarian: hunt the fatal flaw; what fails, what's missing.\n` +
|
|
288
|
+
` - First Principles: ignore the surface question; what are we really solving? Is this even the right question?\n` +
|
|
289
|
+
` - Expansionist: the upside everyone misses; what if it works better than expected?\n` +
|
|
290
|
+
` - Outsider: zero context; react only to what's stated; catch the curse of knowledge.\n` +
|
|
291
|
+
` - Executor: can it be done, and what's the fastest first step Monday morning?\n\n` +
|
|
292
|
+
`2. Anonymise the 5 answers as A-E (randomised). Spawn 5 reviewer sub-agents; each names the strongest answer, the biggest blind spot, and what ALL of them missed. Under 200 words each.\n\n` +
|
|
293
|
+
`3. Synthesise the verdict yourself:\n` +
|
|
294
|
+
` - Where the council agrees (high-confidence signals).\n` +
|
|
295
|
+
` - Where it clashes (present both sides honestly).\n` +
|
|
296
|
+
` - Blind spots the review caught.\n` +
|
|
297
|
+
` - One clear recommendation, not "it depends".\n` +
|
|
298
|
+
` - The one thing to do first.\n\n` +
|
|
299
|
+
`4. Post the verdict in chat, and save the full transcript (decision, 5 answers, 5 reviews, verdict) to \`.ai/thoughts/council-<short-topic-slug>.md\`.\n\n` +
|
|
300
|
+
`Cost: this runs ~11 sub-agent calls. Use it only for decisions that are expensive to get wrong, not routine choices.\n`,
|
|
301
|
+
},
|
|
247
302
|
];
|
|
248
303
|
function seedTemplates() {
|
|
249
304
|
const now = new Date().toISOString();
|
|
250
305
|
const seed = DEFAULT_TEMPLATES.map((t) => ({ ...t, createdAt: now, updatedAt: now }));
|
|
251
|
-
writeTemplates(seed);
|
|
306
|
+
writeTemplates(seed, DEFAULT_TEMPLATES.map((t) => t.slug));
|
|
252
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Add any default whose slug was never seeded into this install AND is not already
|
|
310
|
+
* present. Each default seeds at most once (tracked in `seededDefaultSlugs`), so a
|
|
311
|
+
* default the user deleted does not come back. Never overwrites an existing template.
|
|
312
|
+
*/
|
|
253
313
|
export function reloadDefaultTemplates() {
|
|
254
314
|
const existing = listTemplates();
|
|
255
|
-
const existingBySlug = new
|
|
315
|
+
const existingBySlug = new Set(existing.map((t) => t.slug));
|
|
316
|
+
const rawSeeded = readSeededSlugs();
|
|
317
|
+
const seeded = new Set(rawSeeded ?? LEGACY_DEFAULT_SLUGS);
|
|
256
318
|
const now = new Date().toISOString();
|
|
257
319
|
const added = [];
|
|
258
320
|
const kept = [];
|
|
259
321
|
const next = [...existing];
|
|
260
322
|
for (const def of DEFAULT_TEMPLATES) {
|
|
261
|
-
if (
|
|
323
|
+
if (seeded.has(def.slug)) {
|
|
262
324
|
kept.push(def.slug);
|
|
263
325
|
continue;
|
|
264
326
|
}
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
if (existingBySlug.has(def.slug)) {
|
|
328
|
+
kept.push(def.slug);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
next.push({ ...def, createdAt: now, updatedAt: now });
|
|
332
|
+
added.push(def.slug);
|
|
333
|
+
}
|
|
334
|
+
seeded.add(def.slug);
|
|
335
|
+
}
|
|
336
|
+
if (added.length > 0 || rawSeeded === undefined) {
|
|
337
|
+
writeTemplates(next, [...seeded]);
|
|
267
338
|
}
|
|
268
|
-
if (added.length > 0)
|
|
269
|
-
writeTemplates(next);
|
|
270
339
|
return { added, kept };
|
|
271
340
|
}
|
|
@@ -71,6 +71,15 @@ export function getPending(workspaceId) {
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a wakeup is currently scheduled for the workspace. Used to decide
|
|
76
|
+
* that background work the agent left running is intentional (the wakeup will
|
|
77
|
+
* resume the session to check it) and must NOT be torn down — e.g. the engine's
|
|
78
|
+
* result-drain watchdog skips its abort when this is true.
|
|
79
|
+
*/
|
|
80
|
+
export function isWakeupScheduled(workspaceId) {
|
|
81
|
+
return getPending(workspaceId) !== null;
|
|
82
|
+
}
|
|
74
83
|
/** Re-register timers for rows persisted across restart. Skips stale entries. */
|
|
75
84
|
export function rehydrate() {
|
|
76
85
|
try {
|
|
@@ -6,7 +6,7 @@ import { resolveForge } from './forge/resolve.js';
|
|
|
6
6
|
import { destroyTerminal } from './terminal-service.js';
|
|
7
7
|
import { emitEphemeral } from './websocket-service.js';
|
|
8
8
|
import { archiveWorkspace, getWorkspace, markWorktreePurged, } from './workspace-service.js';
|
|
9
|
-
import { removeWorktree } from './worktree-service.js';
|
|
9
|
+
import { isPermissionError, removeWorktree } from './worktree-service.js';
|
|
10
10
|
export async function purgeWorktree(workspaceId) {
|
|
11
11
|
const workspace = getWorkspace(workspaceId);
|
|
12
12
|
if (!workspace)
|
|
@@ -71,7 +71,7 @@ export async function purgeWorktree(workspaceId) {
|
|
|
71
71
|
return { outcome: 'purged', warnings };
|
|
72
72
|
}
|
|
73
73
|
function buildRemovalFailureMessage(worktreePath, projectPath, errMsg) {
|
|
74
|
-
const isPermission =
|
|
74
|
+
const isPermission = isPermissionError(errMsg);
|
|
75
75
|
const baseLine = `Failed to remove worktree '${worktreePath}'.`;
|
|
76
76
|
const recovery = ['Recovery:', ` sudo rm -rf '${worktreePath}'`, ` cd '${projectPath}' && git worktree prune`].join('\n');
|
|
77
77
|
if (isPermission) {
|