@loicngr/kobo 1.6.2 → 1.6.4
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/dist/mcp-server/kobo-tasks-handlers.js +145 -0
- package/dist/mcp-server/kobo-tasks-server.js +144 -54
- package/dist/server/routes/workspaces.js +63 -5
- package/dist/server/services/agent/engines/claude-code/args-builder.js +30 -3
- package/dist/server/services/settings-service.js +25 -1
- package/dist/server/utils/git-ops.js +96 -0
- package/package.json +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-Bsm5QUA9.js +7 -0
- package/src/client/dist/spa/assets/ActivityFeed-GAoo1WOT.css +1 -0
- package/src/client/dist/spa/assets/ClosePopup-DTgXzcoa.js +1 -0
- package/src/client/dist/spa/assets/{CreatePage-sGrkfyOm.js → CreatePage-CyHlqcZv.js} +1 -1
- package/src/client/dist/spa/assets/DiffViewer-BC81-2me.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-BDOGx54T.js +2 -0
- package/src/client/dist/spa/assets/{HealthPage-BO-bMpEu.js → HealthPage-CwaLsB_m.js} +1 -1
- package/src/client/dist/spa/assets/MainLayout-CKGnqXNp.css +1 -0
- package/src/client/dist/spa/assets/{MainLayout-BpqOChIX.js → MainLayout-CYMzO0e8.js} +17 -17
- package/src/client/dist/spa/assets/QChip-1nQ_KMFF.js +1 -0
- package/src/client/dist/spa/assets/QDialog-G448EJG4.js +1 -0
- package/src/client/dist/spa/assets/{QExpansionItem-DCRks-Ra.js → QExpansionItem-HLBjHx-0.js} +1 -1
- package/src/client/dist/spa/assets/QScrollArea-CBW6shMb.js +1 -0
- package/src/client/dist/spa/assets/{QSeparator-rkjCbX2M.js → QSeparator-DNSiXYrN.js} +1 -1
- package/src/client/dist/spa/assets/QTabPanels-Cw4nnIbR.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-DbEBexRN.js +1 -0
- package/src/client/dist/spa/assets/{SearchPage-BrUbbtgI.js → SearchPage-D5C4I5SC.js} +1 -1
- package/src/client/dist/spa/assets/{SettingsPage-B3elO1PX.js → SettingsPage-DIDwr-uq.js} +1 -1
- package/src/client/dist/spa/assets/TouchPan-Y_Bxzun2.js +1 -0
- package/src/client/dist/spa/assets/{WorkspacePage-BHl17_tY.js → WorkspacePage-Bx_w4QNi.js} +3 -3
- package/src/client/dist/spa/assets/{build-path-tree-Bgl2q74t.js → build-path-tree-TBMtG2me.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-B1wQ-79R.js → cssMode-noDDoaIu.js} +1 -1
- package/src/client/dist/spa/assets/{documents-CHc8t22V.js → documents-C8qHepC2.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-CRb_5Zw6.js → editor.api-CtzJLjGf.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-C5sdCvGW.js → editor.main-DPd-buqm.js} +3 -3
- package/src/client/dist/spa/assets/{formatters-BDadphwz.js → formatters-D7eTm7uK.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-CVSnsZk-.js → freemarker2-BbnROwW1.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-uL_pucGI.js → handlebars-Ksvxefhe.js} +1 -1
- package/src/client/dist/spa/assets/{html-CatZVwWp.js → html-C-hmwqXO.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-DTDzEngo.js → htmlMode-DrAVxVod.js} +1 -1
- package/src/client/dist/spa/assets/i18n-Dah5k8f2.js +1 -0
- package/src/client/dist/spa/assets/index-Y9eYNsHp.js +2 -0
- package/src/client/dist/spa/assets/{javascript-DeHBpolA.js → javascript-CPLPvA_S.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-Bma_YGGm.js → jsonMode-DOTbDLvn.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-CW7xQEG_.js → liquid-CdSz2rzX.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BsYUhMzF.js → mdx-D9jK81Jo.js} +1 -1
- package/src/client/dist/spa/assets/models-DVnzXKOh.js +1 -0
- package/src/client/dist/spa/assets/{monaco.contribution-Du0atePv.js → monaco.contribution-BuL3dvHW.js} +2 -2
- package/src/client/dist/spa/assets/private.use-form-C5G_3nU5.js +1 -0
- package/src/client/dist/spa/assets/{python-D7DQWXZm.js → python-Dge5WJA8.js} +1 -1
- package/src/client/dist/spa/assets/{razor-B2ZxF301.js → razor-CuTUK6Ln.js} +1 -1
- package/src/client/dist/spa/assets/scroll-C-Vz5BD9.js +1 -0
- package/src/client/dist/spa/assets/touch-B2uuAH_y.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-B4Xul5xA.js → tsMode-CV1esWeE.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-CdsKQuLT.js → typescript-CG_BbDRI.js} +1 -1
- package/src/client/dist/spa/assets/{use-checkbox-DYiZQsbF.js → use-checkbox-BduGd8xg.js} +1 -1
- package/src/client/dist/spa/assets/{use-id-CeduaJbU.js → use-id-BmXMngYX.js} +1 -1
- package/src/client/dist/spa/assets/{xml-Wap00dMv.js → xml-GLWDavVQ.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-BxRDHC24.js → yaml-BFBo2noB.js} +1 -1
- package/src/client/dist/spa/index.html +8 -9
- package/src/mcp-server/kobo-tasks-handlers.ts +191 -0
- package/src/mcp-server/kobo-tasks-server.ts +158 -53
- package/src/client/dist/spa/assets/ActivityFeed-CFuT6H5u.js +0 -7
- package/src/client/dist/spa/assets/ActivityFeed-DXYafbn4.css +0 -1
- package/src/client/dist/spa/assets/ClosePopup-DhM1C4Zw.js +0 -1
- package/src/client/dist/spa/assets/DiffViewer-BVU58ujc.css +0 -1
- package/src/client/dist/spa/assets/DiffViewer-BwSRtVRI.js +0 -2
- package/src/client/dist/spa/assets/MainLayout-BiBJtDTk.css +0 -1
- package/src/client/dist/spa/assets/QChip-KJoHYE6F.js +0 -1
- package/src/client/dist/spa/assets/QDialog-DQeAxY3-.js +0 -1
- package/src/client/dist/spa/assets/QScrollArea-e5qTqwcb.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels--6cYe2US.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-C4CPesBX.js +0 -1
- package/src/client/dist/spa/assets/TouchPan-BT6phK1f.js +0 -1
- package/src/client/dist/spa/assets/format-Bttc9ToS.js +0 -1
- package/src/client/dist/spa/assets/i18n-D-VdPLEh.js +0 -1
- package/src/client/dist/spa/assets/index-CUI-zN26.js +0 -2
- package/src/client/dist/spa/assets/models-BbSRHL9b.js +0 -1
- package/src/client/dist/spa/assets/private.use-form-D1RuEt2P.js +0 -1
- package/src/client/dist/spa/assets/scroll-JVVkg2Ng.js +0 -1
- package/src/client/dist/spa/assets/touch-CBLrR6_z.js +0 -1
- package/src/client/dist/spa/assets/use-portal-DBe4lcC2.js +0 -1
- /package/src/client/dist/spa/assets/{QBadge-DqtcDv8D.js → QBadge-Di02fu2H.js} +0 -0
- /package/src/client/dist/spa/assets/{QItemLabel-Codqjisk.js → QItemLabel-Czw5g0px.js} +0 -0
- /package/src/client/dist/spa/assets/{QItemSection-CGpX7GcL.js → QItemSection-BzWLL-V-.js} +0 -0
- /package/src/client/dist/spa/assets/{QList-B-MkPF7n.js → QList-D2GuTeLl.js} +0 -0
- /package/src/client/dist/spa/assets/{QPage-yqdKDG7-.js → QPage-BTzNQlb1.js} +0 -0
- /package/src/client/dist/spa/assets/{QSlideTransition-BQxI8l5r.js → QSlideTransition-s6ZkYsLs.js} +0 -0
- /package/src/client/dist/spa/assets/{QSpace-BNr0AftG.js → QSpace-0zdF1m5x.js} +0 -0
- /package/src/client/dist/spa/assets/{QSpinnerDots-DEiRooBD.js → QSpinnerDots-By20ptst.js} +0 -0
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-r4mAJOHR.js → _plugin-vue_export-helper-Cj6tcsj6.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-Bgec7Keq.js → abap-DiwvWnMr.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-VBlPwEoQ.js → apex-CmtZjKlf.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-DKqrEFBx.js → azcli-DL2My_i-.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-DdgQWy_0.js → bat-B-nC98wG.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-CRMM43EB.js → bicep-Ju5MwOgh.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-UatALtML.js → cameligo-8Eu1TyBr.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-D8JU08RA.js → clojure-u-RpMkH3.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-C56wu358.js → coffee-CdA7bbTe.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-CyZLvhJG.js → cpp-CzNFP8ks.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-BJl3ixva.js → csharp-j1LThmcE.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CxEKxmO-.js → csp-CLRC61y6.js} +0 -0
- /package/src/client/dist/spa/assets/{css-B0t_muXd.js → css-r6rC_7P2.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-D1hqiMFD.js → cypher-CW08XVUh.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-Bz550Pyv.js → dart-Cs9aL5T_.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-CIXgVAuA.js → dockerfile-BWM0M184.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-D9qbvZoA.js → ecl-MJJuer5P.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-b2M38fAy.js → elixir-D2AIuXqn.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-Dq1UYMkt.js → flow9-B2H24giC.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-CFNadkg7.js → fsharp-CMk2OIJN.js} +0 -0
- /package/src/client/dist/spa/assets/{go-dSur1iB2.js → go-BrMkuJg0.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-qyhAo11d.js → graphql-PSR1UKGv.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-DFzjMyzm.js → hcl-DAQrbDOW.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-TdzA8TIl.js → ini-0TG5BxW0.js} +0 -0
- /package/src/client/dist/spa/assets/{java-CSGA9pkE.js → java-rgorz17v.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-9izz5OsY.js → julia-C8VMdHm8.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-DuPK7AtF.js → kotlin-CllWo3gX.js} +0 -0
- /package/src/client/dist/spa/assets/{less-B8d93iCg.js → less-Cgca25AP.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-DWtEIyu7.js → lexon-D0GHdBaw.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-Ciq0OGgt.js → lua-DmRsNG-P.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-Cki6JWj_.js → m3-BgL5dNKT.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-Cu47xwU0.js → markdown-BuJfycGS.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-BM8ui995.js → mips-C9m_93PR.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-DqLio0_c.js → msdax-CpFHC9OI.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-v1wbjJOq.js → mysql-qFvltsqN.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-CQl3PGSB.js → objective-c-Bnmr858J.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-D4iW0ZtD.js → pascal-WP0_D5AO.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-BdC9CZdj.js → pascaligo-Blom4Rij.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-BL10m4XD.js → perl-B-vk8g64.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-Be_oqVo3.js → pgsql-Cgvz6v67.js} +0 -0
- /package/src/client/dist/spa/assets/{php-BtvXSFRI.js → php-8a3Lrw9m.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-B2vUy15C.js → pla-DuFqEZ8V.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-CbmTTfXr.js → postiats-DkLtSgkp.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-DszLhJGx.js → powerquery-BJ1aNepW.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-B0dYktF6.js → powershell-rE98k687.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-CZvaj1VX.js → protobuf-CUheFacr.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-CPDx1B3S.js → pug-LDcAMD8w.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-CDP9TFLl.js → qsharp-DUKSQoR1.js} +0 -0
- /package/src/client/dist/spa/assets/{r-8DbbFX2l.js → r-D-QApv87.js} +0 -0
- /package/src/client/dist/spa/assets/{rate-limit-labels-BoDORKFj.js → rate-limit-labels-Su-L56A2.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-DRWj9MtJ.js → redis-SXdDyWR9.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-C6cElE_5.js → redshift-Y6lsCryn.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-W9pS9n3m.js → restructuredtext-edObr9a8.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-BKnzWnk-.js → ruby-CNnUfF-8.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-YPCclWwe.js → rust-IHUZWzBr.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-BgM4DTFb.js → sb-DrUvY44N.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-fz1OPLMl.js → scala-B4hbXGLM.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-8Uz1RIbu.js → scheme-BGrd12j3.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-Djo3IYXr.js → scss-x5G1ES4U.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-CINF5Tx_.js → shell-DOehe2Y8.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-GgiNEuUm.js → solidity-BeRvcwWV.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-Culj97P9.js → sophia-DZbkUNjy.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-C2ZlpxOY.js → sparql-B7_oi5-h.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-BEf5Pg7Y.js → sql-CTlsFWVE.js} +0 -0
- /package/src/client/dist/spa/assets/{st-CT6UUoeH.js → st-DJVEJdPE.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-B5g0xTG3.js → swift-CwhT3fYa.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-CEgQz9DR.js → systemverilog-BQN63pkN.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-D0qL2L0I.js → tcl-DqwfpskA.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-BFUAVf1E.js → twig-BiyenUgc.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-CjVVcNKm.js → typespec-CWOJribt.js} +0 -0
- /package/src/client/dist/spa/assets/{use-quasar-Ch82z8H5.js → use-quasar-BBrzedjR.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-CZJr-DQz.js → vb-Cq5F87m3.js} +0 -0
- /package/src/client/dist/spa/assets/{vue-i18n-CeG0hR0Z.js → vue-i18n-eUDnMrPl.js} +0 -0
- /package/src/client/dist/spa/assets/{wgsl-ivoXUo2e.js → wgsl-BAvW2lVr.js} +0 -0
|
@@ -183,3 +183,148 @@ export function listWorkspaceImagesHandler(worktreePath) {
|
|
|
183
183
|
};
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
+
// ── Documents ────────────────────────────────────────────────────────────────
|
|
187
|
+
/** Directories (relative to the worktree root) scanned for AI-generated docs. */
|
|
188
|
+
export const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
|
|
189
|
+
/** Depth cap to keep recursion bounded even on pathological symlink loops. */
|
|
190
|
+
const DOC_MAX_DEPTH = 8;
|
|
191
|
+
function walkMarkdownFiles(rootAbs, rootRel, out, depth = 0) {
|
|
192
|
+
if (depth > DOC_MAX_DEPTH)
|
|
193
|
+
return;
|
|
194
|
+
let entries;
|
|
195
|
+
try {
|
|
196
|
+
entries = fs.readdirSync(rootAbs);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (entry.startsWith('.') && entry !== '.ai')
|
|
203
|
+
continue;
|
|
204
|
+
const absEntry = path.join(rootAbs, entry);
|
|
205
|
+
const relEntry = `${rootRel}/${entry}`;
|
|
206
|
+
let stat;
|
|
207
|
+
try {
|
|
208
|
+
stat = fs.statSync(absEntry);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (stat.isDirectory()) {
|
|
214
|
+
walkMarkdownFiles(absEntry, relEntry, out, depth + 1);
|
|
215
|
+
}
|
|
216
|
+
else if (stat.isFile() && entry.endsWith('.md')) {
|
|
217
|
+
out.push({ path: relEntry, name: entry, modifiedAt: stat.mtime.toISOString() });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Recursively list every `.md` file under `docs/plans/`, `docs/superpowers/`,
|
|
223
|
+
* and `.ai/thoughts/` inside the given worktree. Sorted by modifiedAt desc.
|
|
224
|
+
*/
|
|
225
|
+
export function listDocumentsHandler(worktreePath) {
|
|
226
|
+
const documents = [];
|
|
227
|
+
for (const dir of DOCUMENT_DIRS) {
|
|
228
|
+
const absDir = path.join(worktreePath, dir);
|
|
229
|
+
if (!fs.existsSync(absDir))
|
|
230
|
+
continue;
|
|
231
|
+
walkMarkdownFiles(absDir, dir, documents);
|
|
232
|
+
}
|
|
233
|
+
documents.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
|
|
234
|
+
return documents;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Read a single document. The caller-supplied path must be relative to the
|
|
238
|
+
* worktree root and live under one of the allowed DOCUMENT_DIRS; `.md` only;
|
|
239
|
+
* traversal (`..`) is rejected.
|
|
240
|
+
*/
|
|
241
|
+
export function readDocumentHandler(worktreePath, relPath) {
|
|
242
|
+
if (!relPath)
|
|
243
|
+
throw new Error('path is required');
|
|
244
|
+
const normalized = path.normalize(relPath);
|
|
245
|
+
if (normalized.includes('..') ||
|
|
246
|
+
!DOCUMENT_DIRS.some((dir) => normalized.startsWith(`${dir}/`) || normalized === dir)) {
|
|
247
|
+
throw new Error(`Invalid path: must be under ${DOCUMENT_DIRS.map((d) => `${d}/`).join(', ')}`);
|
|
248
|
+
}
|
|
249
|
+
if (!normalized.endsWith('.md')) {
|
|
250
|
+
throw new Error('Only .md files can be read');
|
|
251
|
+
}
|
|
252
|
+
const abs = path.join(worktreePath, normalized);
|
|
253
|
+
if (!fs.existsSync(abs)) {
|
|
254
|
+
throw new Error(`Document not found: ${normalized}`);
|
|
255
|
+
}
|
|
256
|
+
return { path: normalized, content: fs.readFileSync(abs, 'utf-8') };
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Append a thought / decision / note to `.ai/thoughts/<YYYY-MM-DD>-<slug>.md`.
|
|
260
|
+
* Creates the directory if missing. Returns the path (worktree-relative) of
|
|
261
|
+
* the file actually written — useful for the agent to reference it in chat.
|
|
262
|
+
*/
|
|
263
|
+
export function logThoughtHandler(worktreePath, data) {
|
|
264
|
+
const title = data.title?.trim();
|
|
265
|
+
if (!title)
|
|
266
|
+
throw new Error('title is required');
|
|
267
|
+
const content = data.content?.trim();
|
|
268
|
+
if (!content)
|
|
269
|
+
throw new Error('content is required');
|
|
270
|
+
const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts');
|
|
271
|
+
fs.mkdirSync(thoughtsDir, { recursive: true });
|
|
272
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
273
|
+
const slug = title
|
|
274
|
+
.toLowerCase()
|
|
275
|
+
.normalize('NFKD')
|
|
276
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
277
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
278
|
+
.replace(/^-+|-+$/g, '')
|
|
279
|
+
.slice(0, 60) || 'note';
|
|
280
|
+
const tagSuffix = data.tag ? `-${data.tag.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}` : '';
|
|
281
|
+
const filename = `${date}-${slug}${tagSuffix}.md`;
|
|
282
|
+
const abs = path.join(thoughtsDir, filename);
|
|
283
|
+
const relPath = `.ai/thoughts/${filename}`;
|
|
284
|
+
const header = `# ${title}\n\n_${new Date().toISOString()}_${data.tag ? ` · tag: \`${data.tag}\`` : ''}\n\n`;
|
|
285
|
+
fs.writeFileSync(abs, header + content + (content.endsWith('\n') ? '' : '\n'), 'utf-8');
|
|
286
|
+
return { path: relPath };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Aggregate `usage` events from `ws_events` to report how many tokens and
|
|
290
|
+
* dollars the workspace has consumed — both in total and for the currently
|
|
291
|
+
* running agent_session (if any). Silently skips rows whose payload is not
|
|
292
|
+
* valid JSON or not a usage event.
|
|
293
|
+
*/
|
|
294
|
+
export function getSessionUsageHandler(db, workspaceId) {
|
|
295
|
+
const runningSession = db
|
|
296
|
+
.prepare("SELECT id FROM agent_sessions WHERE workspace_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1")
|
|
297
|
+
.get(workspaceId);
|
|
298
|
+
const currentSessionId = runningSession?.id ?? null;
|
|
299
|
+
const rows = db
|
|
300
|
+
.prepare("SELECT payload, session_id FROM ws_events WHERE workspace_id = ? AND type = 'agent:event'")
|
|
301
|
+
.all(workspaceId);
|
|
302
|
+
const totals = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
303
|
+
const current = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
let parsed;
|
|
306
|
+
try {
|
|
307
|
+
parsed = JSON.parse(row.payload);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (parsed.kind !== 'usage')
|
|
313
|
+
continue;
|
|
314
|
+
const input = typeof parsed.inputTokens === 'number' ? parsed.inputTokens : 0;
|
|
315
|
+
const output = typeof parsed.outputTokens === 'number' ? parsed.outputTokens : 0;
|
|
316
|
+
const cost = typeof parsed.costUsd === 'number' ? parsed.costUsd : 0;
|
|
317
|
+
totals.inputTokens += input;
|
|
318
|
+
totals.outputTokens += output;
|
|
319
|
+
totals.costUsd += cost;
|
|
320
|
+
if (currentSessionId && row.session_id === currentSessionId) {
|
|
321
|
+
current.inputTokens += input;
|
|
322
|
+
current.outputTokens += output;
|
|
323
|
+
current.costUsd += cost;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
workspaceTotals: totals,
|
|
328
|
+
currentSession: { sessionId: currentSessionId, ...current },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -5,7 +5,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import Database from 'better-sqlite3';
|
|
8
|
-
import { createTaskHandler, deleteTaskHandler, getDevServerStatusHandler, getSettingsHandler, getWorkspaceInfoHandler, listTasksHandler, listWorkspaceImagesHandler, markTaskDoneHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
8
|
+
import { createTaskHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, logThoughtHandler, markTaskDoneHandler, readDocumentHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
9
9
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
10
10
|
const dbPath = process.env.KOBO_DB_PATH;
|
|
11
11
|
const settingsPath = process.env.KOBO_SETTINGS_PATH;
|
|
@@ -73,37 +73,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
73
73
|
tools: [
|
|
74
74
|
{
|
|
75
75
|
name: 'list_tasks',
|
|
76
|
-
description: '
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {},
|
|
80
|
-
required: [],
|
|
81
|
-
},
|
|
76
|
+
description: 'CALL FIRST on any non-trivial turn to know what the user wants done and what is already completed. Returns every task and acceptance criterion for the current workspace with its id and status. Re-call periodically (before marking something done, or after the user asks for a status) to stay in sync with user-added or external updates.',
|
|
77
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
82
78
|
},
|
|
83
79
|
{
|
|
84
80
|
name: 'mark_task_done',
|
|
85
|
-
description: '
|
|
81
|
+
description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified (tests pass, feature works, diff committed). Do not wait for the end of the turn — the user watches progress live and marking each item as it completes is the primary signal Kōbō uses to track you.',
|
|
86
82
|
inputSchema: {
|
|
87
83
|
type: 'object',
|
|
88
84
|
properties: {
|
|
89
|
-
task_id: {
|
|
90
|
-
type: 'string',
|
|
91
|
-
description: 'The ID of the task to mark as done (obtained from list_tasks)',
|
|
92
|
-
},
|
|
85
|
+
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
93
86
|
},
|
|
94
87
|
required: ['task_id'],
|
|
95
88
|
},
|
|
96
89
|
},
|
|
97
90
|
{
|
|
98
91
|
name: 'create_task',
|
|
99
|
-
description: '
|
|
92
|
+
description: 'CALL WHEN you discover follow-up work that was not in the original list and needs to stick around (e.g. "refactor this helper later", "add a test for edge case"). Appends at the end of the list. Do not use it for ephemeral internal notes — prefer log_thought for those.',
|
|
100
93
|
inputSchema: {
|
|
101
94
|
type: 'object',
|
|
102
95
|
properties: {
|
|
103
|
-
title: { type: 'string', description: '
|
|
96
|
+
title: { type: 'string', description: 'Short, imperative title (e.g. "Add retry to fetchUser").' },
|
|
104
97
|
is_acceptance_criterion: {
|
|
105
98
|
type: 'boolean',
|
|
106
|
-
description: '
|
|
99
|
+
description: 'Mark as acceptance criterion rather than a task (default: false).',
|
|
107
100
|
},
|
|
108
101
|
},
|
|
109
102
|
required: ['title'],
|
|
@@ -111,20 +104,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
111
104
|
},
|
|
112
105
|
{
|
|
113
106
|
name: 'update_task',
|
|
114
|
-
description: '
|
|
107
|
+
description: 'CALL WHEN you need to refine a task — rewording for clarity, flipping status to `in_progress` as you start it, or promoting a task to acceptance criterion. At least one mutable field is required.',
|
|
115
108
|
inputSchema: {
|
|
116
109
|
type: 'object',
|
|
117
110
|
properties: {
|
|
118
|
-
task_id: { type: 'string', description: '
|
|
119
|
-
title: { type: 'string', description: 'New title (optional)' },
|
|
111
|
+
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
112
|
+
title: { type: 'string', description: 'New title (optional).' },
|
|
120
113
|
status: {
|
|
121
114
|
type: 'string',
|
|
122
115
|
enum: ['pending', 'in_progress', 'done'],
|
|
123
|
-
description: 'New status (optional)',
|
|
116
|
+
description: 'New status (optional).',
|
|
124
117
|
},
|
|
125
118
|
is_acceptance_criterion: {
|
|
126
119
|
type: 'boolean',
|
|
127
|
-
description: 'Toggle acceptance criterion flag (optional)',
|
|
120
|
+
description: 'Toggle acceptance criterion flag (optional).',
|
|
128
121
|
},
|
|
129
122
|
},
|
|
130
123
|
required: ['task_id'],
|
|
@@ -132,95 +125,150 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
132
125
|
},
|
|
133
126
|
{
|
|
134
127
|
name: 'delete_task',
|
|
135
|
-
description: '
|
|
128
|
+
description: 'CALL ONLY when a task was created in error or became truly irrelevant (scope change validated by user). Prefer marking done or in_progress over deleting.',
|
|
136
129
|
inputSchema: {
|
|
137
130
|
type: 'object',
|
|
138
131
|
properties: {
|
|
139
|
-
task_id: { type: 'string', description: '
|
|
132
|
+
task_id: { type: 'string', description: 'Task id from list_tasks.' },
|
|
140
133
|
},
|
|
141
134
|
required: ['task_id'],
|
|
142
135
|
},
|
|
143
136
|
},
|
|
144
137
|
{
|
|
145
|
-
name: '
|
|
146
|
-
description: '
|
|
138
|
+
name: 'get_workspace_info',
|
|
139
|
+
description: 'CALL EARLY in a session to confirm project path, working/source branch, worktree path, model, and notion link. Cheap read — useful when the user refers to "this workspace" or when you need the worktree path to locate files.',
|
|
140
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'get_git_info',
|
|
144
|
+
description: 'CALL BEFORE creating a PR, committing in batches, or reporting progress to the user. Returns commit count ahead of source, files changed, insertions/deletions, and existing PR URL if any.',
|
|
145
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'set_workspace_status',
|
|
149
|
+
description: 'CALL WHEN you believe the mission is done (`completed`), blocked beyond recovery (`error`), or explicitly idle awaiting user input (`idle`). Transitions are validated by the backend — invalid ones are rejected.',
|
|
147
150
|
inputSchema: {
|
|
148
151
|
type: 'object',
|
|
149
152
|
properties: {
|
|
150
|
-
|
|
153
|
+
status: {
|
|
151
154
|
type: 'string',
|
|
152
|
-
|
|
155
|
+
enum: ['idle', 'completed', 'error'],
|
|
156
|
+
description: 'Target status.',
|
|
153
157
|
},
|
|
154
158
|
},
|
|
155
|
-
required: [],
|
|
159
|
+
required: ['status'],
|
|
156
160
|
},
|
|
157
161
|
},
|
|
158
162
|
{
|
|
159
|
-
name: '
|
|
160
|
-
description: '
|
|
161
|
-
inputSchema: {
|
|
162
|
-
type: 'object',
|
|
163
|
-
properties: {},
|
|
164
|
-
required: [],
|
|
165
|
-
},
|
|
163
|
+
name: 'get_notion_ticket',
|
|
164
|
+
description: 'CALL when the user references "the ticket", "the Notion page", or when you need the source-of-truth text for the mission. Returns the Notion URL + locally-extracted ticket content from .ai/thoughts/.',
|
|
165
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
|
-
name: '
|
|
169
|
-
description: '
|
|
168
|
+
name: 'get_dev_server_status',
|
|
169
|
+
description: 'CALL BEFORE asking the user whether the app is running, or when your change is dev-server-sensitive. Returns running/stopped/starting/error + URL, port, container names.',
|
|
170
170
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
name: 'start_dev_server',
|
|
174
|
-
description: '
|
|
174
|
+
description: 'CALL WHEN the user asks you to test the running app and the dev server is stopped.',
|
|
175
175
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
name: 'stop_dev_server',
|
|
179
|
-
description: '
|
|
179
|
+
description: 'CALL WHEN the user explicitly asks to stop the dev server, or before destructive operations that require a clean boot.',
|
|
180
180
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
name: 'get_dev_server_logs',
|
|
184
|
-
description: '
|
|
184
|
+
description: 'CALL WHEN debugging a runtime issue the user describes as happening in the running app. Returns the last N lines of logs (default 200). Cheaper than asking the user to paste them.',
|
|
185
185
|
inputSchema: {
|
|
186
186
|
type: 'object',
|
|
187
187
|
properties: {
|
|
188
|
-
tail: {
|
|
189
|
-
type: 'number',
|
|
190
|
-
description: 'Number of lines to fetch from the end (default: 200)',
|
|
191
|
-
},
|
|
188
|
+
tail: { type: 'number', description: 'Number of lines from the end (default: 200).' },
|
|
192
189
|
},
|
|
193
190
|
required: [],
|
|
194
191
|
},
|
|
195
192
|
},
|
|
196
193
|
{
|
|
197
194
|
name: 'list_workspace_images',
|
|
198
|
-
description: '
|
|
195
|
+
description: 'CALL WHEN the user mentions "the screenshot", "the attached image", or when you need to reference a previously-uploaded image. Returns uid, originalName, relativePath, createdAt for every image in .ai/images/.',
|
|
199
196
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
200
197
|
},
|
|
201
198
|
{
|
|
202
|
-
name: '
|
|
203
|
-
description: '
|
|
199
|
+
name: 'get_settings',
|
|
200
|
+
description: 'CALL WHEN you need to confirm configured models, PR prompt templates, git conventions, or dev-server commands before acting on them. Pass project_path to merge global + project-specific entries.',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
project_path: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
description: 'Project path to resolve a specific project entry (optional).',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
required: [],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
// ── Knowledge / context tools ─────────────────────────────────────────────
|
|
213
|
+
{
|
|
214
|
+
name: 'list_documents',
|
|
215
|
+
description: 'CALL EARLY on a new session to discover plans, specs, and thoughts previously written for this workspace. Recursively lists every .md under docs/plans/, docs/superpowers/, and .ai/thoughts/. Before writing a new plan, check if one already exists.',
|
|
204
216
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
205
217
|
},
|
|
206
218
|
{
|
|
207
|
-
name: '
|
|
208
|
-
description: '
|
|
219
|
+
name: 'read_document',
|
|
220
|
+
description: 'CALL AFTER list_documents when a file title looks relevant to the current task. Returns the full markdown content. Scoped to docs/plans/, docs/superpowers/, .ai/thoughts/ — reject anything else.',
|
|
209
221
|
inputSchema: {
|
|
210
222
|
type: 'object',
|
|
211
223
|
properties: {
|
|
212
|
-
|
|
224
|
+
path: {
|
|
213
225
|
type: 'string',
|
|
214
|
-
|
|
215
|
-
description: 'New status (e.g. idle, completed)',
|
|
226
|
+
description: 'Worktree-relative path from list_documents (e.g. "docs/superpowers/plans/2026-04-17-foo.md").',
|
|
216
227
|
},
|
|
217
228
|
},
|
|
218
|
-
required: ['
|
|
229
|
+
required: ['path'],
|
|
219
230
|
},
|
|
220
231
|
},
|
|
221
232
|
{
|
|
222
|
-
name: '
|
|
223
|
-
description: '
|
|
233
|
+
name: 'log_thought',
|
|
234
|
+
description: 'CALL WHEN you make a decision worth remembering — architecture choice, trade-off taken, dead-end avoided, pattern discovered. Appends a dated markdown file to .ai/thoughts/. Keep entries short and focused; one decision per call. Use create_task for actionable follow-ups instead.',
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
title: { type: 'string', description: 'Short, descriptive title (becomes the filename slug and the # H1).' },
|
|
239
|
+
content: { type: 'string', description: 'Markdown body explaining the decision and its reasoning.' },
|
|
240
|
+
tag: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'Optional short tag appended to filename (e.g. "arch", "bug", "perf").',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ['title', 'content'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'search_codebase',
|
|
250
|
+
description: 'CALL WHEN you need to recall prior chat history across workspaces — past decisions, prior user requests, an agent message you remember but can’t locate. Full-text search over user messages + agent outputs persisted in Kōbō. Use the local Grep tool for searching source code; this tool searches CONVERSATIONS.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
query: { type: 'string', description: 'Search phrase. Plain text; no regex.' },
|
|
255
|
+
include_archived: {
|
|
256
|
+
type: 'boolean',
|
|
257
|
+
description: 'Include archived workspaces in the search (default: false).',
|
|
258
|
+
},
|
|
259
|
+
scope: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
enum: ['workspace', 'all'],
|
|
262
|
+
description: 'Restrict to this workspace only (default) or search across every workspace.',
|
|
263
|
+
},
|
|
264
|
+
limit: { type: 'number', description: 'Max results to return (default 30, max 100).' },
|
|
265
|
+
},
|
|
266
|
+
required: ['query'],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'get_session_usage',
|
|
271
|
+
description: 'CALL when you need to self-regulate on long missions — returns token/cost totals for the workspace lifetime and for the currently running agent_session. Useful before spawning heavy subagents or deep reasoning on already-expensive sessions.',
|
|
224
272
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
225
273
|
},
|
|
226
274
|
],
|
|
@@ -339,6 +387,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
339
387
|
const result = await backendRequest('PATCH', `/api/workspaces/${workspaceId}`, { status });
|
|
340
388
|
return ok(result);
|
|
341
389
|
}
|
|
390
|
+
if (name === 'list_documents') {
|
|
391
|
+
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
392
|
+
return ok(listDocumentsHandler(info.worktreePath));
|
|
393
|
+
}
|
|
394
|
+
if (name === 'read_document') {
|
|
395
|
+
const docPath = a.path;
|
|
396
|
+
if (!docPath)
|
|
397
|
+
return fail('path parameter is required');
|
|
398
|
+
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
399
|
+
return ok(readDocumentHandler(info.worktreePath, docPath));
|
|
400
|
+
}
|
|
401
|
+
if (name === 'log_thought') {
|
|
402
|
+
const title = a.title;
|
|
403
|
+
const content = a.content;
|
|
404
|
+
if (!title)
|
|
405
|
+
return fail('title parameter is required');
|
|
406
|
+
if (!content)
|
|
407
|
+
return fail('content parameter is required');
|
|
408
|
+
const info = getWorkspaceInfoHandler(db, workspaceId);
|
|
409
|
+
return ok(logThoughtHandler(info.worktreePath, {
|
|
410
|
+
title,
|
|
411
|
+
content,
|
|
412
|
+
tag: a.tag,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
if (name === 'get_session_usage') {
|
|
416
|
+
return ok(getSessionUsageHandler(db, workspaceId));
|
|
417
|
+
}
|
|
418
|
+
if (name === 'search_codebase') {
|
|
419
|
+
const query = a.query;
|
|
420
|
+
if (!query)
|
|
421
|
+
return fail('query parameter is required');
|
|
422
|
+
const scope = a.scope ?? 'workspace';
|
|
423
|
+
const includeArchived = a.include_archived === true;
|
|
424
|
+
const limit = Math.min(Math.max(1, a.limit ?? 30), 100);
|
|
425
|
+
const qs = new URLSearchParams({ q: query, limit: String(limit) });
|
|
426
|
+
if (includeArchived)
|
|
427
|
+
qs.set('includeArchived', 'true');
|
|
428
|
+
const raw = (await backendRequest('GET', `/api/search?${qs.toString()}`));
|
|
429
|
+
const results = scope === 'all' ? raw : raw.filter((r) => r.workspaceId === workspaceId);
|
|
430
|
+
return ok({ query, scope, total: results.length, results });
|
|
431
|
+
}
|
|
342
432
|
return fail(`Unknown tool: ${name}`);
|
|
343
433
|
}
|
|
344
434
|
catch (err) {
|
|
@@ -76,6 +76,27 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
76
76
|
});
|
|
77
77
|
let notionContent = null;
|
|
78
78
|
let sentryContent = null;
|
|
79
|
+
// Auto-tag the workspace based on its creation source — `notion` when
|
|
80
|
+
// imported from a Notion page, `sentry` when bootstrapped from a Sentry
|
|
81
|
+
// issue URL. Pre-seeded in the global tag catalogue via migration v9.
|
|
82
|
+
// Skip any tag the user has removed from the catalogue so we respect
|
|
83
|
+
// their choice (they may have pruned "notion"/"sentry" on purpose).
|
|
84
|
+
const catalogTags = new Set(globalSettings.tags ?? []);
|
|
85
|
+
const autoTags = [];
|
|
86
|
+
if (body.notionUrl && catalogTags.has('notion'))
|
|
87
|
+
autoTags.push('notion');
|
|
88
|
+
if (body.sentryUrl && catalogTags.has('sentry'))
|
|
89
|
+
autoTags.push('sentry');
|
|
90
|
+
if (autoTags.length > 0) {
|
|
91
|
+
try {
|
|
92
|
+
const tagged = workspaceService.setWorkspaceTags(workspace.id, autoTags);
|
|
93
|
+
if (tagged)
|
|
94
|
+
workspace = tagged;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error('[workspaces] Failed to apply auto tags:', err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
79
100
|
// Extract Notion page content if a URL was provided
|
|
80
101
|
if (body.notionUrl) {
|
|
81
102
|
workspaceService.updateWorkspaceStatus(workspace.id, 'extracting');
|
|
@@ -1143,18 +1164,27 @@ app.get('/:id/git-stats', async (c) => {
|
|
|
1143
1164
|
return c.json({ error: message }, 500);
|
|
1144
1165
|
}
|
|
1145
1166
|
});
|
|
1146
|
-
// GET /api/workspaces/:id/diff — list changed files
|
|
1167
|
+
// GET /api/workspaces/:id/diff?mode=branch|unpushed — list changed files
|
|
1168
|
+
// - `branch` (default): committed + working tree changes vs sourceBranch,
|
|
1169
|
+
// i.e. what the PR will contain.
|
|
1170
|
+
// - `unpushed`: committed-only changes vs `origin/<workingBranch>`,
|
|
1171
|
+
// i.e. what the next `git push` will send.
|
|
1147
1172
|
app.get('/:id/diff', (c) => {
|
|
1148
1173
|
try {
|
|
1149
1174
|
const id = c.req.param('id');
|
|
1175
|
+
const mode = c.req.query('mode') === 'unpushed' ? 'unpushed' : 'branch';
|
|
1150
1176
|
const workspace = workspaceService.getWorkspace(id);
|
|
1151
1177
|
if (!workspace) {
|
|
1152
1178
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1153
1179
|
}
|
|
1154
1180
|
const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
|
|
1155
|
-
const files =
|
|
1181
|
+
const files = mode === 'unpushed'
|
|
1182
|
+
? gitOps.getUnpushedChangedFiles(worktreePath, workspace.workingBranch)
|
|
1183
|
+
: gitOps.getChangedFiles(worktreePath, workspace.sourceBranch);
|
|
1184
|
+
c.header('Cache-Control', 'no-store');
|
|
1156
1185
|
return c.json({
|
|
1157
1186
|
files,
|
|
1187
|
+
mode,
|
|
1158
1188
|
sourceBranch: workspace.sourceBranch,
|
|
1159
1189
|
workingBranch: workspace.workingBranch,
|
|
1160
1190
|
});
|
|
@@ -1164,11 +1194,16 @@ app.get('/:id/diff', (c) => {
|
|
|
1164
1194
|
return c.json({ error: message }, 500);
|
|
1165
1195
|
}
|
|
1166
1196
|
});
|
|
1167
|
-
// GET /api/workspaces/:id/diff
|
|
1197
|
+
// GET /api/workspaces/:id/diff-file?path=...&mode=branch|unpushed
|
|
1198
|
+
// Resolves `original` at the appropriate base ref:
|
|
1199
|
+
// - `branch` → sourceBranch
|
|
1200
|
+
// - `unpushed` → origin/<workingBranch>
|
|
1201
|
+
// `modified` is always the current worktree content.
|
|
1168
1202
|
app.get('/:id/diff-file', (c) => {
|
|
1169
1203
|
try {
|
|
1170
1204
|
const id = c.req.param('id');
|
|
1171
1205
|
const filePath = c.req.query('path');
|
|
1206
|
+
const mode = c.req.query('mode') === 'unpushed' ? 'unpushed' : 'branch';
|
|
1172
1207
|
if (!filePath) {
|
|
1173
1208
|
return c.json({ error: 'Missing path query parameter' }, 400);
|
|
1174
1209
|
}
|
|
@@ -1177,9 +1212,32 @@ app.get('/:id/diff-file', (c) => {
|
|
|
1177
1212
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1178
1213
|
}
|
|
1179
1214
|
const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
|
|
1180
|
-
const
|
|
1215
|
+
const baseRef = mode === 'unpushed' ? `origin/${workspace.workingBranch}` : workspace.sourceBranch;
|
|
1216
|
+
const original = gitOps.getFileAtRef(worktreePath, baseRef, filePath);
|
|
1181
1217
|
const modified = gitOps.getFileContent(worktreePath, filePath);
|
|
1182
|
-
|
|
1218
|
+
c.header('Cache-Control', 'no-store');
|
|
1219
|
+
return c.json({ original: original ?? '', modified: modified ?? '', filePath, mode });
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1223
|
+
return c.json({ error: message }, 500);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
// GET /api/workspaces/:id/commits?limit=50 — list commits between sourceBranch
|
|
1227
|
+
// and HEAD, each tagged with whether it's already pushed to origin/<branch>.
|
|
1228
|
+
app.get('/:id/commits', (c) => {
|
|
1229
|
+
try {
|
|
1230
|
+
const id = c.req.param('id');
|
|
1231
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1232
|
+
if (!workspace) {
|
|
1233
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1234
|
+
}
|
|
1235
|
+
const limitRaw = c.req.query('limit');
|
|
1236
|
+
const limit = Math.min(Math.max(1, parseInt(limitRaw ?? '50', 10) || 50), 200);
|
|
1237
|
+
const worktreePath = path.join(workspace.projectPath, '.worktrees', workspace.workingBranch);
|
|
1238
|
+
const commits = gitOps.listBranchCommits(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
|
|
1239
|
+
c.header('Cache-Control', 'no-store');
|
|
1240
|
+
return c.json({ commits, sourceBranch: workspace.sourceBranch, workingBranch: workspace.workingBranch });
|
|
1183
1241
|
}
|
|
1184
1242
|
catch (err) {
|
|
1185
1243
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1,10 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Short brief injected at the top of the first prompt of a *new* session so
|
|
3
|
+
* the agent discovers the Kōbō MCP toolbox without having to be asked.
|
|
4
|
+
* Skipped on `--resume` because the brief is already in conversation history.
|
|
5
|
+
*/
|
|
6
|
+
const KOBO_MCP_BRIEF = [
|
|
7
|
+
'[Kōbō MCP] This workspace exposes a dedicated MCP server with tools prefixed `kobo__`.',
|
|
8
|
+
'Conventions — read these BEFORE starting work, not as a fallback:',
|
|
9
|
+
'• `kobo__list_tasks` first on any non-trivial turn, then `kobo__mark_task_done` as each item completes.',
|
|
10
|
+
'• `kobo__list_documents` / `kobo__read_document` to discover existing plans and specs under docs/ and .ai/thoughts/ before writing new ones.',
|
|
11
|
+
'• `kobo__log_thought` to persist notable decisions to `.ai/thoughts/<date>-<slug>.md`.',
|
|
12
|
+
'• `kobo__search_codebase` to recall prior chat history (conversations, not source — use Grep for source).',
|
|
13
|
+
'• `kobo__get_workspace_info` / `kobo__get_git_info` / `kobo__get_notion_ticket` for context.',
|
|
14
|
+
'• `kobo__set_workspace_status` when the mission is done / blocked / idle.',
|
|
15
|
+
'Each tool carries its own "WHEN to use" guidance in its description — follow it.',
|
|
16
|
+
].join('\n');
|
|
1
17
|
export function buildClaudeArgs(input) {
|
|
2
18
|
const args = ['--output-format', 'stream-json', '--verbose'];
|
|
3
|
-
|
|
19
|
+
// `plan` mode is handled by Claude Code natively via `--permission-mode plan`.
|
|
20
|
+
// Under plan mode, Claude restricts itself to read-only tools and surfaces an
|
|
21
|
+
// `ExitPlanMode` tool call when the plan is ready for the user to approve.
|
|
22
|
+
// `--dangerously-skip-permissions` is incompatible with plan mode (it would
|
|
23
|
+
// bypass the very restriction plan mode enforces), so we skip it here.
|
|
24
|
+
if (input.permissionMode === 'plan') {
|
|
25
|
+
args.push('--permission-mode', 'plan');
|
|
26
|
+
}
|
|
27
|
+
else if (input.skipPermissions) {
|
|
4
28
|
args.push('--dangerously-skip-permissions');
|
|
29
|
+
}
|
|
5
30
|
let prompt = input.prompt;
|
|
6
|
-
|
|
7
|
-
|
|
31
|
+
// Only prepend the MCP brief on a fresh session — on --resume, the previous
|
|
32
|
+
// turn's context already contains it, and re-prepending would spam.
|
|
33
|
+
if (!input.resumeFromEngineSessionId) {
|
|
34
|
+
prompt = `${KOBO_MCP_BRIEF}\n\n${prompt}`;
|
|
8
35
|
}
|
|
9
36
|
if (input.model && input.model !== 'auto')
|
|
10
37
|
args.push('--model', input.model);
|