@loicngr/kobo 1.6.1 → 1.6.3

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.
Files changed (152) hide show
  1. package/README.md +17 -16
  2. package/dist/mcp-server/kobo-tasks-handlers.js +145 -0
  3. package/dist/mcp-server/kobo-tasks-server.js +144 -54
  4. package/dist/server/index.js +2 -2
  5. package/dist/server/routes/documents.js +113 -0
  6. package/dist/server/services/agent/engines/claude-code/args-builder.js +21 -0
  7. package/package.json +1 -1
  8. package/src/client/dist/spa/assets/ActivityFeed-BCPHgr3p.css +1 -0
  9. package/src/client/dist/spa/assets/ActivityFeed-DPg8lZZP.js +7 -0
  10. package/src/client/dist/spa/assets/{ClosePopup-DqhgFbQo.js → ClosePopup-DhM1C4Zw.js} +1 -1
  11. package/src/client/dist/spa/assets/{CreatePage-DENfwzPL.js → CreatePage-C2mUAcwE.js} +1 -1
  12. package/src/client/dist/spa/assets/DiffViewer-BVU58ujc.css +1 -0
  13. package/src/client/dist/spa/assets/DiffViewer-DdLwXeB3.js +2 -0
  14. package/src/client/dist/spa/assets/{HealthPage-Cjc79NaA.js → HealthPage-CJDF2_4D.js} +1 -1
  15. package/src/client/dist/spa/assets/MainLayout-BiBJtDTk.css +1 -0
  16. package/src/client/dist/spa/assets/{MainLayout-CFbMw65L.js → MainLayout-K2u9uIOb.js} +17 -17
  17. package/src/client/dist/spa/assets/QBtn-CyzfM9-_.js +1 -0
  18. package/src/client/dist/spa/assets/QChip-KJoHYE6F.js +1 -0
  19. package/src/client/dist/spa/assets/{QDialog-D42GLa1i.js → QDialog-DQeAxY3-.js} +1 -1
  20. package/src/client/dist/spa/assets/QExpansionItem-DCRks-Ra.js +1 -0
  21. package/src/client/dist/spa/assets/{QItemSection-GlMrLmz3.js → QItemSection-CGpX7GcL.js} +1 -1
  22. package/src/client/dist/spa/assets/{QScrollArea-L6wUiA20.js → QScrollArea-e5qTqwcb.js} +1 -1
  23. package/src/client/dist/spa/assets/QSlideTransition-BQxI8l5r.js +1 -0
  24. package/src/client/dist/spa/assets/{QTabPanels-Crs-ujNO.js → QTabPanels--6cYe2US.js} +1 -1
  25. package/src/client/dist/spa/assets/{QTooltip-Cg9E3Dvw.js → QTooltip-C4CPesBX.js} +1 -1
  26. package/src/client/dist/spa/assets/{SearchPage-Bf-iZnyE.js → SearchPage-Drjd-r_Y.js} +1 -1
  27. package/src/client/dist/spa/assets/{SettingsPage-BdcH3BSs.js → SettingsPage-DvqT-ViK.js} +1 -1
  28. package/src/client/dist/spa/assets/{TouchPan-DFx22dM3.js → TouchPan-BT6phK1f.js} +1 -1
  29. package/src/client/dist/spa/assets/{WorkspacePage-UUE0pPCR.js → WorkspacePage-Bk_SslTv.js} +3 -3
  30. package/src/client/dist/spa/assets/build-path-tree-BE2FoPWg.js +1 -0
  31. package/src/client/dist/spa/assets/{cssMode-BYtqFZtm.js → cssMode-0GRgcPCL.js} +1 -1
  32. package/src/client/dist/spa/assets/documents-BJW_8dHZ.js +60 -0
  33. package/src/client/dist/spa/assets/{editor.api-D6ZaO4A_.js → editor.api-C1-xuJKd.js} +1 -1
  34. package/src/client/dist/spa/assets/{editor.main-Cc_RDKsq.js → editor.main-C2QDgca3.js} +3 -3
  35. package/src/client/dist/spa/assets/{freemarker2-CBm--bBd.js → freemarker2-Dgc_0Q23.js} +1 -1
  36. package/src/client/dist/spa/assets/{handlebars-whX2mkV5.js → handlebars-DkfsYZsi.js} +1 -1
  37. package/src/client/dist/spa/assets/{html-D7ga_o6c.js → html-CbSyHFvE.js} +1 -1
  38. package/src/client/dist/spa/assets/{htmlMode-BXImjcsv.js → htmlMode-BKR6h-2d.js} +1 -1
  39. package/src/client/dist/spa/assets/i18n-C03q300x.js +1 -0
  40. package/src/client/dist/spa/assets/index-9vZWx9Bu.js +2 -0
  41. package/src/client/dist/spa/assets/{javascript-BwmzNMn5.js → javascript-QJqA2E1u.js} +1 -1
  42. package/src/client/dist/spa/assets/{jsonMode-CN5Z5bK_.js → jsonMode-D8CXYQiy.js} +1 -1
  43. package/src/client/dist/spa/assets/{liquid-CzMNAPor.js → liquid-DLssQpNB.js} +1 -1
  44. package/src/client/dist/spa/assets/{mdx-DC_P05Da.js → mdx-4u6l2_x4.js} +1 -1
  45. package/src/client/dist/spa/assets/{models-DMQoi09X.js → models-BtywKe_m.js} +1 -1
  46. package/src/client/dist/spa/assets/{monaco.contribution-BsBaFOOD.js → monaco.contribution-DFCuvXsm.js} +2 -2
  47. package/src/client/dist/spa/assets/{python-9DTZ8C3K.js → python-BFIl5AC4.js} +1 -1
  48. package/src/client/dist/spa/assets/{razor-B1LfM20o.js → razor-BrKtxN6e.js} +1 -1
  49. package/src/client/dist/spa/assets/{scroll-Dh2g7BwR.js → scroll-JVVkg2Ng.js} +1 -1
  50. package/src/client/dist/spa/assets/{tsMode-DI2bWo8r.js → tsMode-B7FS-eG8.js} +1 -1
  51. package/src/client/dist/spa/assets/{typescript-BZ9QJ2_N.js → typescript-CpUDqcsF.js} +1 -1
  52. package/src/client/dist/spa/assets/use-checkbox-DYiZQsbF.js +1 -0
  53. package/src/client/dist/spa/assets/{use-portal-mhLq4Rqk.js → use-portal-DBe4lcC2.js} +1 -1
  54. package/src/client/dist/spa/assets/{xml-D6qm6rp0.js → xml-_L0I3U6m.js} +1 -1
  55. package/src/client/dist/spa/assets/{yaml-D2dUr_wY.js → yaml-CY3U_Y-0.js} +1 -1
  56. package/src/client/dist/spa/index.html +6 -5
  57. package/src/mcp-server/kobo-tasks-handlers.ts +191 -0
  58. package/src/mcp-server/kobo-tasks-server.ts +158 -53
  59. package/dist/server/routes/plans.js +0 -89
  60. package/src/client/dist/spa/assets/ActivityFeed-DYtAK49y.js +0 -7
  61. package/src/client/dist/spa/assets/ActivityFeed-DiwnrdKX.css +0 -1
  62. package/src/client/dist/spa/assets/DiffViewer-C6q11kmw.js +0 -2
  63. package/src/client/dist/spa/assets/DiffViewer-DiHFLSk4.css +0 -1
  64. package/src/client/dist/spa/assets/MainLayout-B5poKEy_.css +0 -1
  65. package/src/client/dist/spa/assets/QBtn-p1aZtrJH.js +0 -1
  66. package/src/client/dist/spa/assets/QExpansionItem-5ekmpO-2.js +0 -1
  67. package/src/client/dist/spa/assets/QMenu-Q69oVX7b.js +0 -1
  68. package/src/client/dist/spa/assets/i18n-BxLBrD1J.js +0 -1
  69. package/src/client/dist/spa/assets/index-D997aY4Y.js +0 -2
  70. package/src/client/dist/spa/assets/marked.esm-DW0ulF0a.js +0 -60
  71. /package/src/client/dist/spa/assets/{QBadge-BUkmTO0P.js → QBadge-DqtcDv8D.js} +0 -0
  72. /package/src/client/dist/spa/assets/{QItemLabel-Czw5g0px.js → QItemLabel-Codqjisk.js} +0 -0
  73. /package/src/client/dist/spa/assets/{QList-DNzlynsS.js → QList-B-MkPF7n.js} +0 -0
  74. /package/src/client/dist/spa/assets/{QPage-B09NY4Nf.js → QPage-yqdKDG7-.js} +0 -0
  75. /package/src/client/dist/spa/assets/{QSpace-PlDK6Fg3.js → QSpace-BNr0AftG.js} +0 -0
  76. /package/src/client/dist/spa/assets/{QSpinnerDots-By20ptst.js → QSpinnerDots-DEiRooBD.js} +0 -0
  77. /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-CEhRWsKN.js → _plugin-vue_export-helper-r4mAJOHR.js} +0 -0
  78. /package/src/client/dist/spa/assets/{abap-DiwvWnMr.js → abap-Bgec7Keq.js} +0 -0
  79. /package/src/client/dist/spa/assets/{apex-CmtZjKlf.js → apex-VBlPwEoQ.js} +0 -0
  80. /package/src/client/dist/spa/assets/{azcli-DL2My_i-.js → azcli-DKqrEFBx.js} +0 -0
  81. /package/src/client/dist/spa/assets/{bat-B-nC98wG.js → bat-DdgQWy_0.js} +0 -0
  82. /package/src/client/dist/spa/assets/{bicep-Ju5MwOgh.js → bicep-CRMM43EB.js} +0 -0
  83. /package/src/client/dist/spa/assets/{cameligo-8Eu1TyBr.js → cameligo-UatALtML.js} +0 -0
  84. /package/src/client/dist/spa/assets/{clojure-u-RpMkH3.js → clojure-D8JU08RA.js} +0 -0
  85. /package/src/client/dist/spa/assets/{coffee-CdA7bbTe.js → coffee-C56wu358.js} +0 -0
  86. /package/src/client/dist/spa/assets/{cpp-CzNFP8ks.js → cpp-CyZLvhJG.js} +0 -0
  87. /package/src/client/dist/spa/assets/{csharp-j1LThmcE.js → csharp-BJl3ixva.js} +0 -0
  88. /package/src/client/dist/spa/assets/{csp-CLRC61y6.js → csp-CxEKxmO-.js} +0 -0
  89. /package/src/client/dist/spa/assets/{css-r6rC_7P2.js → css-B0t_muXd.js} +0 -0
  90. /package/src/client/dist/spa/assets/{cypher-CW08XVUh.js → cypher-D1hqiMFD.js} +0 -0
  91. /package/src/client/dist/spa/assets/{dart-Cs9aL5T_.js → dart-Bz550Pyv.js} +0 -0
  92. /package/src/client/dist/spa/assets/{dockerfile-BWM0M184.js → dockerfile-CIXgVAuA.js} +0 -0
  93. /package/src/client/dist/spa/assets/{ecl-MJJuer5P.js → ecl-D9qbvZoA.js} +0 -0
  94. /package/src/client/dist/spa/assets/{elixir-D2AIuXqn.js → elixir-b2M38fAy.js} +0 -0
  95. /package/src/client/dist/spa/assets/{flow9-B2H24giC.js → flow9-Dq1UYMkt.js} +0 -0
  96. /package/src/client/dist/spa/assets/{format-uvONOeL4.js → format-Bttc9ToS.js} +0 -0
  97. /package/src/client/dist/spa/assets/{formatters-DiJ12fKd.js → formatters-BDadphwz.js} +0 -0
  98. /package/src/client/dist/spa/assets/{fsharp-CMk2OIJN.js → fsharp-CFNadkg7.js} +0 -0
  99. /package/src/client/dist/spa/assets/{go-BrMkuJg0.js → go-dSur1iB2.js} +0 -0
  100. /package/src/client/dist/spa/assets/{graphql-PSR1UKGv.js → graphql-qyhAo11d.js} +0 -0
  101. /package/src/client/dist/spa/assets/{hcl-DAQrbDOW.js → hcl-DFzjMyzm.js} +0 -0
  102. /package/src/client/dist/spa/assets/{ini-0TG5BxW0.js → ini-TdzA8TIl.js} +0 -0
  103. /package/src/client/dist/spa/assets/{java-rgorz17v.js → java-CSGA9pkE.js} +0 -0
  104. /package/src/client/dist/spa/assets/{julia-C8VMdHm8.js → julia-9izz5OsY.js} +0 -0
  105. /package/src/client/dist/spa/assets/{kotlin-CllWo3gX.js → kotlin-DuPK7AtF.js} +0 -0
  106. /package/src/client/dist/spa/assets/{less-Cgca25AP.js → less-B8d93iCg.js} +0 -0
  107. /package/src/client/dist/spa/assets/{lexon-D0GHdBaw.js → lexon-DWtEIyu7.js} +0 -0
  108. /package/src/client/dist/spa/assets/{lua-DmRsNG-P.js → lua-Ciq0OGgt.js} +0 -0
  109. /package/src/client/dist/spa/assets/{m3-BgL5dNKT.js → m3-Cki6JWj_.js} +0 -0
  110. /package/src/client/dist/spa/assets/{markdown-BuJfycGS.js → markdown-Cu47xwU0.js} +0 -0
  111. /package/src/client/dist/spa/assets/{mips-C9m_93PR.js → mips-BM8ui995.js} +0 -0
  112. /package/src/client/dist/spa/assets/{msdax-CpFHC9OI.js → msdax-DqLio0_c.js} +0 -0
  113. /package/src/client/dist/spa/assets/{mysql-qFvltsqN.js → mysql-v1wbjJOq.js} +0 -0
  114. /package/src/client/dist/spa/assets/{objective-c-Bnmr858J.js → objective-c-CQl3PGSB.js} +0 -0
  115. /package/src/client/dist/spa/assets/{pascal-WP0_D5AO.js → pascal-D4iW0ZtD.js} +0 -0
  116. /package/src/client/dist/spa/assets/{pascaligo-Blom4Rij.js → pascaligo-BdC9CZdj.js} +0 -0
  117. /package/src/client/dist/spa/assets/{perl-B-vk8g64.js → perl-BL10m4XD.js} +0 -0
  118. /package/src/client/dist/spa/assets/{pgsql-Cgvz6v67.js → pgsql-Be_oqVo3.js} +0 -0
  119. /package/src/client/dist/spa/assets/{php-8a3Lrw9m.js → php-BtvXSFRI.js} +0 -0
  120. /package/src/client/dist/spa/assets/{pla-DuFqEZ8V.js → pla-B2vUy15C.js} +0 -0
  121. /package/src/client/dist/spa/assets/{postiats-DkLtSgkp.js → postiats-CbmTTfXr.js} +0 -0
  122. /package/src/client/dist/spa/assets/{powerquery-BJ1aNepW.js → powerquery-DszLhJGx.js} +0 -0
  123. /package/src/client/dist/spa/assets/{powershell-rE98k687.js → powershell-B0dYktF6.js} +0 -0
  124. /package/src/client/dist/spa/assets/{protobuf-CUheFacr.js → protobuf-CZvaj1VX.js} +0 -0
  125. /package/src/client/dist/spa/assets/{pug-LDcAMD8w.js → pug-CPDx1B3S.js} +0 -0
  126. /package/src/client/dist/spa/assets/{qsharp-DUKSQoR1.js → qsharp-CDP9TFLl.js} +0 -0
  127. /package/src/client/dist/spa/assets/{r-D-QApv87.js → r-8DbbFX2l.js} +0 -0
  128. /package/src/client/dist/spa/assets/{rate-limit-labels-BvYERsho.js → rate-limit-labels-BoDORKFj.js} +0 -0
  129. /package/src/client/dist/spa/assets/{redis-SXdDyWR9.js → redis-DRWj9MtJ.js} +0 -0
  130. /package/src/client/dist/spa/assets/{redshift-Y6lsCryn.js → redshift-C6cElE_5.js} +0 -0
  131. /package/src/client/dist/spa/assets/{restructuredtext-edObr9a8.js → restructuredtext-W9pS9n3m.js} +0 -0
  132. /package/src/client/dist/spa/assets/{ruby-CNnUfF-8.js → ruby-BKnzWnk-.js} +0 -0
  133. /package/src/client/dist/spa/assets/{rust-IHUZWzBr.js → rust-YPCclWwe.js} +0 -0
  134. /package/src/client/dist/spa/assets/{sb-DrUvY44N.js → sb-BgM4DTFb.js} +0 -0
  135. /package/src/client/dist/spa/assets/{scala-B4hbXGLM.js → scala-fz1OPLMl.js} +0 -0
  136. /package/src/client/dist/spa/assets/{scheme-BGrd12j3.js → scheme-8Uz1RIbu.js} +0 -0
  137. /package/src/client/dist/spa/assets/{scss-x5G1ES4U.js → scss-Djo3IYXr.js} +0 -0
  138. /package/src/client/dist/spa/assets/{shell-DOehe2Y8.js → shell-CINF5Tx_.js} +0 -0
  139. /package/src/client/dist/spa/assets/{solidity-BeRvcwWV.js → solidity-GgiNEuUm.js} +0 -0
  140. /package/src/client/dist/spa/assets/{sophia-DZbkUNjy.js → sophia-Culj97P9.js} +0 -0
  141. /package/src/client/dist/spa/assets/{sparql-B7_oi5-h.js → sparql-C2ZlpxOY.js} +0 -0
  142. /package/src/client/dist/spa/assets/{sql-CTlsFWVE.js → sql-BEf5Pg7Y.js} +0 -0
  143. /package/src/client/dist/spa/assets/{st-DJVEJdPE.js → st-CT6UUoeH.js} +0 -0
  144. /package/src/client/dist/spa/assets/{swift-CwhT3fYa.js → swift-B5g0xTG3.js} +0 -0
  145. /package/src/client/dist/spa/assets/{systemverilog-BQN63pkN.js → systemverilog-CEgQz9DR.js} +0 -0
  146. /package/src/client/dist/spa/assets/{tcl-DqwfpskA.js → tcl-D0qL2L0I.js} +0 -0
  147. /package/src/client/dist/spa/assets/{touch-D_A29lik.js → touch-CBLrR6_z.js} +0 -0
  148. /package/src/client/dist/spa/assets/{twig-BiyenUgc.js → twig-BFUAVf1E.js} +0 -0
  149. /package/src/client/dist/spa/assets/{typespec-CWOJribt.js → typespec-CjVVcNKm.js} +0 -0
  150. /package/src/client/dist/spa/assets/{use-quasar-BBrzedjR.js → use-quasar-Ch82z8H5.js} +0 -0
  151. /package/src/client/dist/spa/assets/{vb-Cq5F87m3.js → vb-CZJr-DQz.js} +0 -0
  152. /package/src/client/dist/spa/assets/{wgsl-BAvW2lVr.js → wgsl-ivoXUo2e.js} +0 -0
package/README.md CHANGED
@@ -2,12 +2,8 @@
2
2
 
3
3
  > **Kōbō** (工房) — Japanese for *workshop*. A multi-workspace agent manager for [Claude Code](https://claude.com/claude-code).
4
4
 
5
- > [!WARNING]
6
- > 🚧 **Work in progress** — This project is under active development. Breaking changes may occur at any time.
7
- > ⚠️ **Planned refactor with potential data loss** — A major refactor is planned and may require database/schema changes that can cause data loss.
8
- > ❌ **Do not use in production** until this refactor is complete and a stable migration path is documented.
9
- >
10
- > **Engine refactor in progress.** The legacy `agent-manager.ts` has been split into an agent engine abstraction (`src/server/services/agent/`) with a pluggable `AgentEngine` contract, a shared `Orchestrator`, and a normalised `AgentEvent` stream. The Claude Code CLI is now one engine among potentially several (`src/server/services/agent/engines/claude-code/`). A runtime migration (`content-migration-service.ts`) converts legacy `ws_events` into the new normalised form on first boot after upgrade; **a timestamped copy of `kobo.db` is written alongside it before the migration runs**, so you can roll back by restoring the backup if anything goes sideways. Expect continued churn until the WebSocket event surface and UI stream reducers stabilise.
5
+ > [!NOTE]
6
+ > 🚧 **Active development** — breaking changes may still land on `develop`. The database layer ships with forward-only migrations and a timestamped pre-migration backup of `kobo.db` before any schema change, so upgrades preserve your data even across invasive refactors.
11
7
 
12
8
  Kōbō lets you delegate multiple coding missions to Claude Code agents in parallel. Each workspace lives in its own isolated git worktree with its own branch, its own Claude session, optionally its own dev server, and a custom MCP tools server the agent uses to track progress. A Vue 3 dashboard shows live agent output, tasks, acceptance criteria, and git state across every workspace.
13
9
 
@@ -16,25 +12,31 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
16
12
  ## Features
17
13
 
18
14
  - **Isolated git worktrees** — every workspace runs on its own branch in its own directory, so concurrent Claude sessions never step on each other
19
- - **Live agent output** — stream `stdout`/`stderr` from Claude Code to the browser via WebSocket, with persisted event replay on reconnect
15
+ - **Pluggable agent engine** — Kōbō talks to agents through an `AgentEngine` contract with a normalised `AgentEvent` stream (`src/server/services/agent/engines/`). Claude Code is the first engine; dropping in another runtime (e.g. the Claude Agent SDK) only requires a new adapter, not a rewrite of the UI or orchestration layer
16
+ - **Rich chat feed** — live streaming text, thinking blocks, inline tool calls with expandable diffs for Edit/Write, per-turn session cards, markdown rendering, jump-to-previous-user-message button, and infinite scroll-up over persisted history
20
17
  - **Task & acceptance criteria tracking** — the agent reports progress through a dedicated MCP server (`kobo-tasks`) that reads and updates tasks directly from the SQLite database
18
+ - **Documents panel** — tree view in the right drawer that surfaces every AI-generated markdown file under `docs/plans/`, `docs/superpowers/`, and `.ai/thoughts/`. Paths mentioned in chat messages are auto-detected against the catalogue and become one-click deep-links into the panel
19
+ - **Git panel with inline diff viewer** — Monaco-powered side-by-side / inline diff of the working branch against its source, with file tree (same q-tree as Documents), inline rebase/merge conflict resolution, and a clean action bar: `Sync` split-button (pull / rebase / merge), `Push`, `Diff`, `Create PR`
21
20
  - **Notion integration** — pull workspace missions straight from Notion pages, extract markdown, and use it as the source of truth for acceptance criteria
22
21
  - **Sentry integration** — paste a Sentry issue URL to spin up a dedicated "fix workspace" with the stacktrace, tags, and offending spans written to `.ai/thoughts/SENTRY-<id>.md`; the agent is primed with a TDD fix workflow and has access to the Sentry MCP tools for deeper digging
23
22
  - **Per-workspace dev servers** — start/stop Docker or Node dev servers scoped to each branch, with log streaming
24
23
  - **Conventional-commit enforcement** — project-level git conventions are written to `.ai/.git-conventions.md` inside every workspace so Claude follows them during commits
25
- - **Pull request automation** — one-click `push`, `pull`, and `open-pr` endpoints integrate with the GitHub CLI, using a configurable prompt template
24
+ - **Pull request automation** — one-click `push`, `pull`, `open-pr`, and "change PR base" endpoints integrate with the GitHub CLI, using a configurable prompt template
26
25
  - **Multi-session support** — create multiple Claude agent sessions per workspace, each with its own chat history; resume completed sessions via `--resume`; sessions are named and persisted in localStorage
27
26
  - **Prompt templates** — personal library of reusable prompts with variable substitution (`{working_branch}`, `{commit_count}`, etc.), insertable from the chat input via `/` autocomplete; editable in Settings > Templates
28
- - **Plan browser** — read-only viewer for markdown plan files produced by agents, rendered directly in the right-side panel
27
+ - **Favorites and tags** — pin workspaces to the top via right-click favourite, organise with per-workspace tags filterable from the sidebar; a global tag catalogue keeps colours consistent across workspaces
28
+ - **Health panel + config export/import** — inspect backend health (agent sessions, migration state, dev servers, DB size) and roundtrip your Kōbō config (settings, templates, skills) between machines via JSON
29
+ - **Usage tracking** — rolling input/output token counts and cost estimates per workspace, aggregated across sessions and live-updated from `usage` events
30
+ - **Resizable right drawer** — drag-to-resize horizontally and vertically, with tab state and split ratio persisted to localStorage
29
31
  - **Soft interrupt** — pause an agent mid-execution (SIGINT, like pressing Escape in Claude Code) without killing the process; the agent stops the current tool and waits for the next message
30
32
  - **Archive instead of delete** — soft-remove workspaces without losing the worktree, branches, or history; unarchive restores the exact pre-archive state
31
33
 
32
34
  ## Tech stack
33
35
 
34
36
  - **Backend** — Node.js ≥ 20, [Hono](https://hono.dev/), [better-sqlite3](https://github.com/WiseLibs/better-sqlite3), [ws](https://github.com/websockets/ws), [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk)
35
- - **Frontend** — [Vue 3](https://vuejs.org/), [Quasar 2](https://quasar.dev/), [Pinia](https://pinia.vuejs.org/), `vue-router`
37
+ - **Frontend** — [Vue 3](https://vuejs.org/), [Quasar 2](https://quasar.dev/), [Pinia](https://pinia.vuejs.org/), `vue-router`, [Monaco Editor](https://microsoft.github.io/monaco-editor/) (git diff viewer), `marked` + `dompurify` (markdown rendering)
36
38
  - **Tooling** — TypeScript, [Vitest](https://vitest.dev/), [Biome](https://biomejs.dev/) (lint + format), `tsx` for dev
37
- - **Storage** — single SQLite file (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`) with WAL mode
39
+ - **Storage** — single SQLite file (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`) with WAL mode and forward-only migrations
38
40
 
39
41
  ## Quick start
40
42
 
@@ -51,7 +53,7 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
51
53
  ### Run via `npx` (recommended)
52
54
 
53
55
  ```bash
54
- PORT=9999 npx @loicngr/kobo@latest
56
+ SERVER_PORT=9998 PORT=9999 npx @loicngr/kobo@latest
55
57
  ```
56
58
 
57
59
  That's it. npm downloads the package, installs dependencies, starts the Kōbō server on the port you specified, and serves the web UI at `http://localhost:9999`. Data is persisted to `~/.config/kobo/` (overridable via `KOBO_HOME`).
@@ -244,10 +246,10 @@ See [`AGENTS.md`](./AGENTS.md) for a deeper dive into conventions, data model, W
244
246
 
245
247
  | Table | Purpose |
246
248
  |---|---|
247
- | `workspaces` | the unit of work — branch, status, Notion link, model, `archived_at`, … |
249
+ | `workspaces` | the unit of work — branch, status, model, engine, `archived_at`, `favorited_at`, `tags`, Notion link, … |
248
250
  | `tasks` | workspace sub-items — tasks and acceptance criteria |
249
- | `agent_sessions` | Claude Code CLI invocations `claude_session_id`, pid, lifecycle |
250
- | `ws_events` | persisted WebSocket events for replay on reconnect |
251
+ | `agent_sessions` | agent runs pid, `engine_session_id`, lifecycle |
252
+ | `ws_events` | persisted WebSocket events (chat history, `agent:event` stream, user messages) for replay on reconnect |
251
253
 
252
254
  ## MCP server
253
255
 
@@ -274,7 +276,6 @@ This is a personal tool, but PRs and issues are welcome. Before submitting:
274
276
  1. Read [`AGENTS.md`](./AGENTS.md) — it covers the commit rules, branching model, and code conventions
275
277
  2. Run `npm run lint`, `npx tsc --noEmit`, and `npm test` locally
276
278
  3. Base your branch on `develop` (not `main`); PRs target `develop`
277
- 4. **Do not add `Co-Authored-By` trailers** in commits, even for AI-assisted work
278
279
 
279
280
  CI runs lint + type check + tests on every PR to `develop`.
280
281
 
@@ -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: 'List all tasks and acceptance criteria for the current workspace with their IDs and current status. Call this first to discover task IDs before calling mark_task_done / update_task / delete_task.',
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: 'Mark a task or acceptance criterion as done. Use this when you have completed the work for a criterion and validated it.',
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: 'Create a new task or acceptance criterion for the current workspace. Appended at the end of the list.',
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: 'Task title' },
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: 'Whether this is an acceptance criterion (default: false)',
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: 'Update an existing task — change title, status, or is_acceptance_criterion flag. At least one field is required.',
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: 'The ID of the task to update' },
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: 'Delete a task from the current workspace permanently.',
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: 'The ID of the task to delete' },
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: 'get_settings',
146
- description: 'Read Kōbō settings (global + projects). Optionally filter to a specific project by path to get both global and that project override.',
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
- project_path: {
153
+ status: {
151
154
  type: 'string',
152
- description: 'Optional project path to resolve a specific project entry',
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: 'get_dev_server_status',
160
- description: 'Get the live dev server status for the current workspace. Returns status (running/stopped/starting/error/unknown), URL, HTTP port, instance name, project name, and running container names.',
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: 'get_workspace_info',
169
- description: 'Get all metadata about the current workspace (name, project path, branches, model, notion URL, worktree path, status).',
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: 'Start the dev server configured for the current workspace.',
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: 'Stop the dev server of the current workspace.',
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: 'Fetch the last N lines of the dev server logs for the current workspace.',
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: 'List all images uploaded to the current workspace (from .ai/images/index.json). Returns uid, originalName, relativePath and createdAt for each image.',
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: 'get_git_info',
203
- description: 'Get git stats for the current workspace (commit count, files changed, insertions, deletions, PR URL if any).',
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: 'set_workspace_status',
208
- description: 'Update the current workspace status. Valid values: idle, completed, error. Transitions are validated by the backend.',
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
- status: {
224
+ path: {
213
225
  type: 'string',
214
- enum: ['idle', 'completed', 'error'],
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: ['status'],
229
+ required: ['path'],
219
230
  },
220
231
  },
221
232
  {
222
- name: 'get_notion_ticket',
223
- description: 'Get the Notion ticket info for the current workspace. Returns the Notion URL and the extracted ticket content (from .ai/thoughts/) if available.',
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) {
@@ -8,13 +8,13 @@ import WebSocket, { WebSocketServer } from 'ws';
8
8
  import { closeDb, getDb } from './db/index.js';
9
9
  import { runMigrations } from './db/migrations.js';
10
10
  import devServerRouter from './routes/dev-server.js';
11
+ import documentsRouter from './routes/documents.js';
11
12
  import { enginesRouter } from './routes/engines.js';
12
13
  import gitRouter from './routes/git.js';
13
14
  import healthRouter from './routes/health.js';
14
15
  import imagesRouter from './routes/images.js';
15
16
  import { migrationRouter } from './routes/migration.js';
16
17
  import notionRouter from './routes/notion.js';
17
- import plansRouter from './routes/plans.js';
18
18
  import searchRouter from './routes/search.js';
19
19
  import sentryRouter from './routes/sentry.js';
20
20
  import settingsRouter from './routes/settings.js';
@@ -71,7 +71,7 @@ app.route('/api/git', gitRouter);
71
71
  app.route('/api/settings', settingsRouter);
72
72
  app.route('/api/dev-server', devServerRouter);
73
73
  app.route('/api/templates', templatesRouter);
74
- app.route('/api/workspaces', plansRouter);
74
+ app.route('/api/workspaces', documentsRouter);
75
75
  app.route('/api/search', searchRouter);
76
76
  app.route('/api/health', healthRouter);
77
77
  app.route('/api/engines', enginesRouter);