@loicngr/kobo 1.4.5 → 1.4.7

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 (179) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +2 -2
  3. package/dist/mcp-server/kobo-tasks-handlers.js +12 -1
  4. package/dist/mcp-server/kobo-tasks-server.js +37 -2
  5. package/dist/server/db/index.js +5 -0
  6. package/dist/server/db/migrations.js +9 -0
  7. package/dist/server/db/schema.js +2 -0
  8. package/dist/server/index.js +17 -18
  9. package/dist/server/routes/dev-server.js +1 -0
  10. package/dist/server/routes/git.js +1 -0
  11. package/dist/server/routes/images.js +3 -0
  12. package/dist/server/routes/notion.js +1 -0
  13. package/dist/server/routes/settings.js +1 -0
  14. package/dist/server/routes/workspaces.js +166 -51
  15. package/dist/server/services/agent-manager.js +35 -9
  16. package/dist/server/services/image-service.js +2 -1
  17. package/dist/server/services/notion-service.js +14 -18
  18. package/dist/server/services/pr-watcher-service.js +2 -0
  19. package/dist/server/services/settings-service.js +33 -6
  20. package/dist/server/services/setup-script-service.js +1 -0
  21. package/dist/server/services/websocket-service.js +8 -9
  22. package/dist/server/services/workspace-service.js +33 -2
  23. package/dist/server/services/worktree-service.js +4 -2
  24. package/dist/server/utils/git-ops.js +19 -5
  25. package/dist/server/utils/process-tracker.js +7 -0
  26. package/package.json +1 -1
  27. package/src/client/dist/spa/assets/ActivityFeed-BXrsWU-N.css +1 -0
  28. package/src/client/dist/spa/assets/ActivityFeed-Zg4aFWHr.js +68 -0
  29. package/src/client/dist/spa/assets/CreatePage-B4NEzk8h.js +2 -0
  30. package/src/client/dist/spa/assets/CreatePage-Cb9tgQ57.css +1 -0
  31. package/src/client/dist/spa/assets/DiffViewer-CSwamnmH.js +2 -0
  32. package/src/client/dist/spa/assets/DiffViewer-DiHFLSk4.css +1 -0
  33. package/src/client/dist/spa/assets/MainLayout-DYu8fNdb.css +1 -0
  34. package/src/client/dist/spa/assets/MainLayout-To97f8bb.js +2 -0
  35. package/src/client/dist/spa/assets/QBadge-BBHYbjmH.js +1 -0
  36. package/src/client/dist/spa/assets/QCheckbox-C3OW_sBe.js +1 -0
  37. package/src/client/dist/spa/assets/QExpansionItem--CUnMqsJ.js +1 -0
  38. package/src/client/dist/spa/assets/QPage-DfVYLit9.js +1 -0
  39. package/src/client/dist/spa/assets/QSpinnerDots-DxFX2A_Y.js +1 -0
  40. package/src/client/dist/spa/assets/QTabPanels-CQiX55Zs.js +1 -0
  41. package/src/client/dist/spa/assets/QTooltip-DDQMcKoX.js +1 -0
  42. package/src/client/dist/spa/assets/SettingsPage-CEJA3OTN.css +1 -0
  43. package/src/client/dist/spa/assets/SettingsPage-DHVfZP9g.js +1 -0
  44. package/src/client/dist/spa/assets/TouchPan-DmgViFM9.js +1 -0
  45. package/src/client/dist/spa/assets/WorkspacePage-BV77y-1V.css +1 -0
  46. package/src/client/dist/spa/assets/WorkspacePage-BfUdWLME.js +2 -0
  47. package/src/client/dist/spa/assets/_plugin-vue_export-helper-D5gezOWD.js +1 -0
  48. package/src/client/dist/spa/assets/{cssMode-Dsa4Lydc.js → cssMode-4AF4Bwjq.js} +1 -1
  49. package/src/client/dist/spa/assets/{editor.api-qmVdKoc0.js → editor.api-Ctxw4rqS.js} +1 -1
  50. package/src/client/dist/spa/assets/{editor.main-D2fZAQLs.js → editor.main-BZM2ZVF-.js} +3 -3
  51. package/src/client/dist/spa/assets/format-DhM1gNfW.js +1 -0
  52. package/src/client/dist/spa/assets/formatters-BpjOVbqs.js +6 -0
  53. package/src/client/dist/spa/assets/{freemarker2-e-FYsZTq.js → freemarker2-nlLLg2wF.js} +1 -1
  54. package/src/client/dist/spa/assets/{handlebars-CAwfoT2m.js → handlebars-D1p1nhG6.js} +1 -1
  55. package/src/client/dist/spa/assets/{html-BTRUpMfA.js → html-B6WFBXSs.js} +1 -1
  56. package/src/client/dist/spa/assets/{htmlMode-mzgQeoHf.js → htmlMode-DLQXzxD7.js} +1 -1
  57. package/src/client/dist/spa/assets/i18n-CrbCJQHz.js +1 -0
  58. package/src/client/dist/spa/assets/i18n-uUJIn7l1.js +1 -0
  59. package/src/client/dist/spa/assets/index-CkkvRhkB.js +5 -0
  60. package/src/client/dist/spa/assets/{javascript-DDeQuxhB.js → javascript-Cuk4WgU2.js} +1 -1
  61. package/src/client/dist/spa/assets/{jsonMode-Ch5vu2Iw.js → jsonMode-CvIzW7YR.js} +1 -1
  62. package/src/client/dist/spa/assets/{liquid-qVj3a9kh.js → liquid-BESgd7r_.js} +1 -1
  63. package/src/client/dist/spa/assets/{mdx-Cb_a7RXe.js → mdx-CjeTpIfQ.js} +1 -1
  64. package/src/client/dist/spa/assets/{monaco.contribution-Rz70-mfd.js → monaco.contribution-BdsaXuzN.js} +2 -2
  65. package/src/client/dist/spa/assets/nodes-Bj1I9JfN.js +1 -0
  66. package/src/client/dist/spa/assets/{python-Cz0peIkX.js → python-DjonvvLh.js} +1 -1
  67. package/src/client/dist/spa/assets/{razor-CU8Xe-4p.js → razor-CYj_TCQ0.js} +1 -1
  68. package/src/client/dist/spa/assets/settings-DQXlzOR-.js +1 -0
  69. package/src/client/dist/spa/assets/touch-Dm5n4n0E.js +1 -0
  70. package/src/client/dist/spa/assets/{tsMode-CP1svEaN.js → tsMode-C8aV_UsX.js} +1 -1
  71. package/src/client/dist/spa/assets/{typescript-aPJGGIsI.js → typescript-Clp9vIWN.js} +1 -1
  72. package/src/client/dist/spa/assets/use-checkbox-Cfet_Yar.js +1 -0
  73. package/src/client/dist/spa/assets/use-quasar-Da_Py0Ib.js +1 -0
  74. package/src/client/dist/spa/assets/vue-i18n-nv59vAyH.js +3 -0
  75. package/src/client/dist/spa/assets/{xml-CZM1zQhV.js → xml-6gY0RxjJ.js} +1 -1
  76. package/src/client/dist/spa/assets/{yaml-B8qPirw0.js → yaml-IQGDhYaJ.js} +1 -1
  77. package/src/client/dist/spa/index.html +5 -4
  78. package/src/client/dist/spa/notification.mp3 +0 -0
  79. package/src/mcp-server/kobo-tasks-handlers.ts +21 -5
  80. package/src/mcp-server/kobo-tasks-server.ts +39 -2
  81. package/src/client/dist/spa/assets/ActivityFeed-Bx7maW4r.css +0 -1
  82. package/src/client/dist/spa/assets/ActivityFeed-DoQIjq5C.js +0 -60
  83. package/src/client/dist/spa/assets/CreatePage-6J0aDFtf.js +0 -2
  84. package/src/client/dist/spa/assets/CreatePage-DOr3puTt.css +0 -1
  85. package/src/client/dist/spa/assets/DiffViewer-7dck6mJc.css +0 -1
  86. package/src/client/dist/spa/assets/DiffViewer-dvGJaxu-.js +0 -2
  87. package/src/client/dist/spa/assets/MainLayout-DGzPKBi9.js +0 -2
  88. package/src/client/dist/spa/assets/MainLayout-gjAr74zR.css +0 -1
  89. package/src/client/dist/spa/assets/QBadge-Y5QfSDtm.js +0 -1
  90. package/src/client/dist/spa/assets/QBtn-D_bkYnrl.js +0 -1
  91. package/src/client/dist/spa/assets/QExpansionItem-D5gm2xc8.js +0 -1
  92. package/src/client/dist/spa/assets/QPage-Bg3Rohl6.js +0 -1
  93. package/src/client/dist/spa/assets/QSeparator-MRAsTeNf.js +0 -1
  94. package/src/client/dist/spa/assets/QSpinnerDots-Bu0GloxK.js +0 -1
  95. package/src/client/dist/spa/assets/QTooltip-Cuj49WIu.js +0 -1
  96. package/src/client/dist/spa/assets/SettingsPage-BNA4jJYf.css +0 -1
  97. package/src/client/dist/spa/assets/SettingsPage-BOkWnRl2.js +0 -1
  98. package/src/client/dist/spa/assets/WorkspacePage-9gRnhdjv.js +0 -2
  99. package/src/client/dist/spa/assets/WorkspacePage-CFC48jKO.css +0 -1
  100. package/src/client/dist/spa/assets/_plugin-vue_export-helper-Bo392ayB.js +0 -1
  101. package/src/client/dist/spa/assets/formatters-CXx5Gzsp.js +0 -1
  102. package/src/client/dist/spa/assets/i18n-B1eQvEGk.js +0 -1
  103. package/src/client/dist/spa/assets/index-C0u2YcnZ.js +0 -5
  104. package/src/client/dist/spa/assets/nodes-Bo-5xQjA.js +0 -1
  105. package/src/client/dist/spa/assets/position-engine-CDz__T_5.js +0 -1
  106. package/src/client/dist/spa/assets/runtime-core.esm-bundler-BLPLlWMG.js +0 -1
  107. package/src/client/dist/spa/assets/use-checkbox-B8W131xl.js +0 -1
  108. package/src/client/dist/spa/assets/use-quasar-sc8fDqi0.js +0 -1
  109. package/src/client/dist/spa/assets/vue-i18n-CoZsbeQK.js +0 -3
  110. /package/src/client/dist/spa/assets/{abap-Co3wj02O.js → abap-DVDKJwpW.js} +0 -0
  111. /package/src/client/dist/spa/assets/{apex-CUKwGs62.js → apex-DjF5nFAs.js} +0 -0
  112. /package/src/client/dist/spa/assets/{azcli-DMImymmY.js → azcli-HEPMaiDV.js} +0 -0
  113. /package/src/client/dist/spa/assets/{bat--P_y70-E.js → bat-D6epFECU.js} +0 -0
  114. /package/src/client/dist/spa/assets/{bicep-C3w6oSfK.js → bicep-BspG10fo.js} +0 -0
  115. /package/src/client/dist/spa/assets/{cameligo-D9NSR4Rj.js → cameligo-DM9kSiq7.js} +0 -0
  116. /package/src/client/dist/spa/assets/{clojure-BMcQme0t.js → clojure-CZn9pzfW.js} +0 -0
  117. /package/src/client/dist/spa/assets/{coffee-BbMZaWx7.js → coffee-Bu_NglwI.js} +0 -0
  118. /package/src/client/dist/spa/assets/{cpp-CbrtEGgw.js → cpp-0KJLHDue.js} +0 -0
  119. /package/src/client/dist/spa/assets/{csharp-Bc0fjUxA.js → csharp-DNzOiVZu.js} +0 -0
  120. /package/src/client/dist/spa/assets/{csp-DmbXuMT0.js → csp-CUkzJlR0.js} +0 -0
  121. /package/src/client/dist/spa/assets/{css-gdwCt5by.js → css-Uav73wXk.js} +0 -0
  122. /package/src/client/dist/spa/assets/{cypher-ocmmfoQr.js → cypher-CP6eoQBS.js} +0 -0
  123. /package/src/client/dist/spa/assets/{dart-DbZ5eklb.js → dart-Bdl32fSd.js} +0 -0
  124. /package/src/client/dist/spa/assets/{dockerfile-BLaMayDc.js → dockerfile-BIRJ7ZNM.js} +0 -0
  125. /package/src/client/dist/spa/assets/{ecl-LxXpHirr.js → ecl-Di24nx2U.js} +0 -0
  126. /package/src/client/dist/spa/assets/{elixir-C_geKt5o.js → elixir-DzFG1iYF.js} +0 -0
  127. /package/src/client/dist/spa/assets/{flow9-DE2fI2ca.js → flow9-DNgNh1TU.js} +0 -0
  128. /package/src/client/dist/spa/assets/{fsharp-CJD6fImD.js → fsharp-CuO6_Oy9.js} +0 -0
  129. /package/src/client/dist/spa/assets/{go-jUCqQ7bD.js → go-Pj7ToRvM.js} +0 -0
  130. /package/src/client/dist/spa/assets/{graphql-rw7g9h7D.js → graphql-BoaedU4s.js} +0 -0
  131. /package/src/client/dist/spa/assets/{hcl-BKX27Mn7.js → hcl-olXtyJcc.js} +0 -0
  132. /package/src/client/dist/spa/assets/{ini-CrXjga2H.js → ini-BvGNUo-D.js} +0 -0
  133. /package/src/client/dist/spa/assets/{java-D4jksGBb.js → java-Z9-7Isu7.js} +0 -0
  134. /package/src/client/dist/spa/assets/{julia-CbWxfkeS.js → julia-Bdcb8Lkm.js} +0 -0
  135. /package/src/client/dist/spa/assets/{kotlin-B26Yx80V.js → kotlin-DR_I1UW_.js} +0 -0
  136. /package/src/client/dist/spa/assets/{less-DFzn-zC9.js → less-DZxcoWKd.js} +0 -0
  137. /package/src/client/dist/spa/assets/{lexon-C-w-W8Yv.js → lexon-s17AK9YH.js} +0 -0
  138. /package/src/client/dist/spa/assets/{lua-CHuE_HoG.js → lua-BzLfjAeg.js} +0 -0
  139. /package/src/client/dist/spa/assets/{m3-DEFZN2qS.js → m3-DN3Xgolo.js} +0 -0
  140. /package/src/client/dist/spa/assets/{markdown-Cbt4TlFt.js → markdown-DCCTbSQf.js} +0 -0
  141. /package/src/client/dist/spa/assets/{mips-C6m4XECw.js → mips-lh5qv6lw.js} +0 -0
  142. /package/src/client/dist/spa/assets/{msdax-un0CFb_S.js → msdax-ikHtaqdR.js} +0 -0
  143. /package/src/client/dist/spa/assets/{mysql-CuAPeiOV.js → mysql-yyeeFEN0.js} +0 -0
  144. /package/src/client/dist/spa/assets/{objective-c-DLVMdxAC.js → objective-c-BCpwk8Ct.js} +0 -0
  145. /package/src/client/dist/spa/assets/{pascal-BGCThuPY.js → pascal-PL6H1Gn7.js} +0 -0
  146. /package/src/client/dist/spa/assets/{pascaligo-DfxSVpdo.js → pascaligo-BCtUX6M4.js} +0 -0
  147. /package/src/client/dist/spa/assets/{perl-BOE6y94t.js → perl-DuluA5AL.js} +0 -0
  148. /package/src/client/dist/spa/assets/{pgsql-Dn7JkY4F.js → pgsql-B4LkSOFV.js} +0 -0
  149. /package/src/client/dist/spa/assets/{php-r1gD0KyT.js → php-B3Ske963.js} +0 -0
  150. /package/src/client/dist/spa/assets/{pla-CgXknhb0.js → pla-B0vdKSVA.js} +0 -0
  151. /package/src/client/dist/spa/assets/{postiats-CsIEtnRB.js → postiats-lYIY9h0z.js} +0 -0
  152. /package/src/client/dist/spa/assets/{powerquery-yNJCmC_6.js → powerquery-e_CNZlRH.js} +0 -0
  153. /package/src/client/dist/spa/assets/{powershell-CQcz1SqH.js → powershell-B7ny7eNr.js} +0 -0
  154. /package/src/client/dist/spa/assets/{protobuf-BmC34uvO.js → protobuf-T5b6INIm.js} +0 -0
  155. /package/src/client/dist/spa/assets/{pug-C20znvWM.js → pug-DB5iDfTd.js} +0 -0
  156. /package/src/client/dist/spa/assets/{qsharp-B7bnARMS.js → qsharp-DwB29woK.js} +0 -0
  157. /package/src/client/dist/spa/assets/{r-ClvcLdqC.js → r-DGAconUr.js} +0 -0
  158. /package/src/client/dist/spa/assets/{redis-DCyda7_S.js → redis-CBlUxt29.js} +0 -0
  159. /package/src/client/dist/spa/assets/{redshift-BtWDr4pb.js → redshift-DUdydw8b.js} +0 -0
  160. /package/src/client/dist/spa/assets/{restructuredtext-CLcnlkhl.js → restructuredtext-CpFidj3o.js} +0 -0
  161. /package/src/client/dist/spa/assets/{ruby-DY0SOSSZ.js → ruby-DSI1pDHV.js} +0 -0
  162. /package/src/client/dist/spa/assets/{rust-JQd-fJZI.js → rust-DuAjdlB2.js} +0 -0
  163. /package/src/client/dist/spa/assets/{sb-BV2j8yFF.js → sb-DHK18yUj.js} +0 -0
  164. /package/src/client/dist/spa/assets/{scala-DwbnREDs.js → scala-OUOqN_hV.js} +0 -0
  165. /package/src/client/dist/spa/assets/{scheme-CrtA-vei.js → scheme-1CW-bJnQ.js} +0 -0
  166. /package/src/client/dist/spa/assets/{scss-VxQz3zmI.js → scss-C5o7X-EM.js} +0 -0
  167. /package/src/client/dist/spa/assets/{shell-CP9faqFI.js → shell-CtGzMV7Q.js} +0 -0
  168. /package/src/client/dist/spa/assets/{solidity-9IIb0b89.js → solidity-DPkz2VqZ.js} +0 -0
  169. /package/src/client/dist/spa/assets/{sophia-D2LQU2AD.js → sophia-nnptfdLN.js} +0 -0
  170. /package/src/client/dist/spa/assets/{sparql-DONCa5dy.js → sparql-C6bexdnc.js} +0 -0
  171. /package/src/client/dist/spa/assets/{sql-DaAAHGEt.js → sql-CcLlvkTm.js} +0 -0
  172. /package/src/client/dist/spa/assets/{st-CRY2V-j3.js → st-C7Y7CLp_.js} +0 -0
  173. /package/src/client/dist/spa/assets/{swift-BlKbfloF.js → swift-7Jj4IMNp.js} +0 -0
  174. /package/src/client/dist/spa/assets/{systemverilog-B_h9Q_T_.js → systemverilog-BBh4Cn1v.js} +0 -0
  175. /package/src/client/dist/spa/assets/{tcl-C4wN3A6M.js → tcl-BivXXY1v.js} +0 -0
  176. /package/src/client/dist/spa/assets/{twig-DDdaBLC9.js → twig-CuImSpsA.js} +0 -0
  177. /package/src/client/dist/spa/assets/{typespec-Dc1ipt8A.js → typespec-s1tz7uxH.js} +0 -0
  178. /package/src/client/dist/spa/assets/{vb-C4BXIvrh.js → vb-AURJL6ia.js} +0 -0
  179. /package/src/client/dist/spa/assets/{wgsl-XVg3Pi-r.js → wgsl-Dq1-vM4I.js} +0 -0
package/AGENTS.md CHANGED
@@ -200,7 +200,7 @@ The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), Frenc
200
200
 
201
201
  ### Commit rules (mirrors `DEFAULT_GIT_CONVENTIONS` in `src/server/services/settings-service.ts`)
202
202
 
203
- These rules are the source of truth and are also written to `.ai/git-conventions.md` inside every workspace that the agent creates. Follow them when committing on this repository too.
203
+ These rules are the source of truth and are also written to `.ai/.git-conventions.md` inside every workspace that the agent creates. Follow them when committing on this repository too.
204
204
 
205
205
  **Commits**
206
206
  - Use Conventional Commits: `type(scope): subject`
package/README.md CHANGED
@@ -16,7 +16,7 @@ Think of it as an apprentice's hall: you hand out missions, each apprentice sets
16
16
  - **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
17
17
  - **Notion integration** — pull workspace missions straight from Notion pages, extract markdown, and use it as the source of truth for acceptance criteria
18
18
  - **Per-workspace dev servers** — start/stop Docker or Node dev servers scoped to each branch, with log streaming
19
- - **Conventional-commit enforcement** — project-level git conventions are written to `.ai/git-conventions.md` inside every workspace so Claude follows them during commits
19
+ - **Conventional-commit enforcement** — project-level git conventions are written to `.ai/.git-conventions.md` inside every workspace so Claude follows them during commits
20
20
  - **Pull request automation** — one-click `push` and `open-pr` endpoints integrate with the GitHub CLI, using a configurable prompt template
21
21
  - **Archive instead of delete** — soft-remove workspaces without losing the worktree, branches, or history; unarchive restores the exact pre-archive state
22
22
 
@@ -181,7 +181,7 @@ Kōbō reads settings from `~/.config/kobo/settings.json` (or falls back to defa
181
181
 
182
182
  - `defaultModel` — Claude model to use (e.g. `claude-opus-4-6`)
183
183
  - `prPromptTemplate` — template rendered when opening a PR via the `/open-pr` endpoint; supports `{{pr_number}}`, `{{pr_url}}`, `{{branch_name}}`, `{{diff_stats}}`, `{{commits}}`, etc.
184
- - `gitConventions` — markdown-formatted git conventions written to `.ai/git-conventions.md` in every workspace so the agent follows them when committing
184
+ - `gitConventions` — markdown-formatted git conventions written to `.ai/.git-conventions.md` in every workspace so the agent follows them when committing
185
185
  - `devServer` — per-project `startCommand` / `stopCommand` for launching workspace-scoped dev servers
186
186
 
187
187
  ## Contributing
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { nanoid } from 'nanoid';
4
+ /** Allowed task status values. */
4
5
  export const VALID_TASK_STATUSES = ['pending', 'in_progress', 'done'];
5
6
  function rowToDto(row) {
6
7
  return {
@@ -10,12 +11,14 @@ function rowToDto(row) {
10
11
  is_acceptance_criterion: row.is_acceptance_criterion === 1,
11
12
  };
12
13
  }
14
+ /** Return all tasks for a workspace, ordered by sort_order. */
13
15
  export function listTasksHandler(db, workspaceId) {
14
16
  const rows = db
15
17
  .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC')
16
18
  .all(workspaceId);
17
19
  return rows.map(rowToDto);
18
20
  }
21
+ /** Set a task's status to "done" and return the updated task. */
19
22
  export function markTaskDoneHandler(db, workspaceId, taskId) {
20
23
  const now = new Date().toISOString();
21
24
  const result = db
@@ -29,6 +32,7 @@ export function markTaskDoneHandler(db, workspaceId, taskId) {
29
32
  .get(taskId);
30
33
  return { success: true, task: rowToDto(row) };
31
34
  }
35
+ /** Create a new task appended at the end of the workspace's task list. */
32
36
  export function createTaskHandler(db, workspaceId, data) {
33
37
  if (!data.title?.trim()) {
34
38
  throw new Error('title is required');
@@ -50,6 +54,7 @@ export function createTaskHandler(db, workspaceId, data) {
50
54
  const row = db.prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?').get(id);
51
55
  return rowToDto(row);
52
56
  }
57
+ /** Update one or more fields of an existing task (title, status, or acceptance criterion flag). */
53
58
  export function updateTaskHandler(db, workspaceId, taskId, data) {
54
59
  // Verify task belongs to workspace
55
60
  const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId);
@@ -87,6 +92,7 @@ export function updateTaskHandler(db, workspaceId, taskId, data) {
87
92
  .get(taskId);
88
93
  return rowToDto(row);
89
94
  }
95
+ /** Permanently delete a task from a workspace. */
90
96
  export function deleteTaskHandler(db, workspaceId, taskId) {
91
97
  const result = db.prepare('DELETE FROM tasks WHERE id = ? AND workspace_id = ?').run(taskId, workspaceId);
92
98
  if (result.changes === 0) {
@@ -94,6 +100,7 @@ export function deleteTaskHandler(db, workspaceId, taskId) {
94
100
  }
95
101
  return { success: true, task_id: taskId };
96
102
  }
103
+ /** Read the dev-server status for a workspace directly from the database. */
97
104
  export function getDevServerStatusHandler(db, workspaceId) {
98
105
  const row = db.prepare('SELECT dev_server_status FROM workspaces WHERE id = ?').get(workspaceId);
99
106
  if (!row) {
@@ -101,6 +108,7 @@ export function getDevServerStatusHandler(db, workspaceId) {
101
108
  }
102
109
  return { workspaceId, status: row.dev_server_status };
103
110
  }
111
+ /** Read global and per-project settings from the JSON file on disk. */
104
112
  export function getSettingsHandler(settingsPath, projectPath) {
105
113
  // Shape is determined solely by whether projectPath was provided:
106
114
  // - with projectPath → { global, project }
@@ -125,9 +133,10 @@ export function getSettingsHandler(settingsPath, projectPath) {
125
133
  }
126
134
  return { global, projects };
127
135
  }
136
+ /** Fetch workspace metadata from the database, computing the worktree path from project_path and working_branch. */
128
137
  export function getWorkspaceInfoHandler(db, workspaceId) {
129
138
  const row = db
130
- .prepare('SELECT id, name, project_path, source_branch, working_branch, status, notion_url, notion_page_id, model, dev_server_status, created_at, updated_at FROM workspaces WHERE id = ?')
139
+ .prepare('SELECT id, name, project_path, source_branch, working_branch, status, notion_url, notion_page_id, model, dev_server_status, has_unread, created_at, updated_at FROM workspaces WHERE id = ?')
131
140
  .get(workspaceId);
132
141
  if (!row) {
133
142
  throw new Error(`Workspace '${workspaceId}' not found`);
@@ -144,10 +153,12 @@ export function getWorkspaceInfoHandler(db, workspaceId) {
144
153
  notionUrl: row.notion_url,
145
154
  notionPageId: row.notion_page_id,
146
155
  devServerStatus: row.dev_server_status,
156
+ hasUnread: row.has_unread === 1,
147
157
  createdAt: row.created_at,
148
158
  updatedAt: row.updated_at,
149
159
  };
150
160
  }
161
+ /** List images registered in the worktree's `.ai/images/index.json`, resolving each entry to its file path. */
151
162
  export function listWorkspaceImagesHandler(worktreePath) {
152
163
  const imagesDir = path.join(worktreePath, '.ai', 'images');
153
164
  const indexPath = path.join(imagesDir, 'index.json');
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
6
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -27,6 +29,7 @@ catch (err) {
27
29
  console.error('[kobo-tasks-server] Failed to open database:', err);
28
30
  process.exit(1);
29
31
  }
32
+ /** Fire-and-forget POST to the backend so the UI reflects a task marked as done. */
30
33
  async function notifyBackend(taskId) {
31
34
  try {
32
35
  const url = `${backendUrl}/api/workspaces/${workspaceId}/tasks/${taskId}/notify-done`;
@@ -39,6 +42,7 @@ async function notifyBackend(taskId) {
39
42
  console.error('[kobo-tasks-server] notify-done failed:', err);
40
43
  }
41
44
  }
45
+ /** Fire-and-forget POST to the backend so the UI refreshes the task list after a mutation. */
42
46
  async function notifyTasksUpdated() {
43
47
  try {
44
48
  const url = `${backendUrl}/api/workspaces/${workspaceId}/tasks/notify-updated`;
@@ -48,6 +52,7 @@ async function notifyTasksUpdated() {
48
52
  console.error('[kobo-tasks-server] notify-updated failed:', err);
49
53
  }
50
54
  }
55
+ /** Generic HTTP request to the Kobo backend, returning parsed JSON or null. */
51
56
  async function backendRequest(method, pathname, body) {
52
57
  const url = `${backendUrl}${pathname}`;
53
58
  const init = { method };
@@ -152,7 +157,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
152
157
  },
153
158
  {
154
159
  name: 'get_dev_server_status',
155
- description: 'Check whether the dev server is running for the current workspace.',
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.',
156
161
  inputSchema: {
157
162
  type: 'object',
158
163
  properties: {},
@@ -213,11 +218,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
213
218
  required: ['status'],
214
219
  },
215
220
  },
221
+ {
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.',
224
+ inputSchema: { type: 'object', properties: {}, required: [] },
225
+ },
216
226
  ],
217
227
  }));
228
+ /** Wrap a successful result as an MCP tool response with JSON text content. */
218
229
  function ok(data) {
219
230
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
220
231
  }
232
+ /** Wrap an error message as an MCP tool error response. */
221
233
  function fail(message) {
222
234
  return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
223
235
  }
@@ -271,7 +283,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
271
283
  return ok(getSettingsHandler(settingsPath, a.project_path));
272
284
  }
273
285
  if (name === 'get_dev_server_status') {
274
- return ok(getDevServerStatusHandler(db, workspaceId));
286
+ try {
287
+ const result = await backendRequest('GET', `/api/dev-server/${workspaceId}/status`);
288
+ return ok(result);
289
+ }
290
+ catch {
291
+ // Fallback to DB if the backend HTTP API is unreachable
292
+ return ok(getDevServerStatusHandler(db, workspaceId));
293
+ }
275
294
  }
276
295
  if (name === 'get_workspace_info') {
277
296
  return ok(getWorkspaceInfoHandler(db, workspaceId));
@@ -293,6 +312,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
293
312
  const info = getWorkspaceInfoHandler(db, workspaceId);
294
313
  return ok(listWorkspaceImagesHandler(info.worktreePath));
295
314
  }
315
+ if (name === 'get_notion_ticket') {
316
+ const info = getWorkspaceInfoHandler(db, workspaceId);
317
+ const thoughtsDir = path.join(info.worktreePath, '.ai', 'thoughts');
318
+ let ticketContent = '';
319
+ if (fs.existsSync(thoughtsDir)) {
320
+ const files = fs.readdirSync(thoughtsDir).filter((f) => f.endsWith('.md'));
321
+ for (const file of files) {
322
+ ticketContent += fs.readFileSync(path.join(thoughtsDir, file), 'utf-8') + '\n';
323
+ }
324
+ }
325
+ return ok({
326
+ notionUrl: info.notionUrl,
327
+ notionPageId: info.notionPageId,
328
+ ticketContent: ticketContent.trim() || null,
329
+ });
330
+ }
296
331
  if (name === 'get_git_info') {
297
332
  const result = await backendRequest('GET', `/api/workspaces/${workspaceId}/git-stats`);
298
333
  return ok(result);
@@ -1,6 +1,10 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { ensureKoboHome, getDbPath } from '../utils/paths.js';
3
3
  let instance = null;
4
+ /**
5
+ * Return the singleton SQLite database connection, creating it on first call.
6
+ * Configures WAL mode, busy timeout, and foreign keys.
7
+ */
4
8
  export function getDb(dbPath) {
5
9
  if (instance)
6
10
  return instance;
@@ -15,6 +19,7 @@ export function getDb(dbPath) {
15
19
  instance.pragma('foreign_keys=ON');
16
20
  return instance;
17
21
  }
22
+ /** Close the singleton database connection and release resources. */
18
23
  export function closeDb() {
19
24
  if (instance) {
20
25
  instance.close();
@@ -1,4 +1,5 @@
1
1
  import { initSchema } from './schema.js';
2
+ /** Ordered registry of all schema migrations. Append new entries at the end. */
2
3
  export const migrations = [
3
4
  {
4
5
  version: 2,
@@ -18,9 +19,17 @@ export const migrations = [
18
19
  `);
19
20
  },
20
21
  },
22
+ {
23
+ version: 4,
24
+ name: 'add-has-unread',
25
+ migrate: (db) => {
26
+ db.prepare('ALTER TABLE workspaces ADD COLUMN has_unread INTEGER NOT NULL DEFAULT 0').run();
27
+ },
28
+ },
21
29
  ];
22
30
  /** Current schema version — always equals the highest migration version. */
23
31
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
32
+ /** Apply all pending migrations sequentially, or bootstrap a fresh database via initSchema. */
24
33
  export function runMigrations(db) {
25
34
  // Create the history table (replaces the old single-row schema_version table).
26
35
  db.exec(`
@@ -1,3 +1,4 @@
1
+ /** Create all tables and indexes for a fresh install. Not used for upgrades -- see migrations.ts. */
1
2
  export function initSchema(db) {
2
3
  db.exec(`
3
4
  CREATE TABLE IF NOT EXISTS workspaces (
@@ -12,6 +13,7 @@ export function initSchema(db) {
12
13
  model TEXT NOT NULL DEFAULT 'claude-opus-4-6',
13
14
  permission_mode TEXT NOT NULL DEFAULT 'auto-accept',
14
15
  dev_server_status TEXT NOT NULL DEFAULT 'stopped',
16
+ has_unread INTEGER NOT NULL DEFAULT 0,
15
17
  archived_at TEXT,
16
18
  created_at TEXT NOT NULL,
17
19
  updated_at TEXT NOT NULL
@@ -20,9 +20,8 @@ import { emit, handleConnection, setMessageHandler } from './services/websocket-
20
20
  import { getLatestSession, getWorkspace, updateWorkspaceStatus } from './services/workspace-service.js';
21
21
  import { getClientSpaPath, getKoboHome, getPackageVersion } from './utils/paths.js';
22
22
  import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/process-tracker.js';
23
- // 0. Runtime prerequisite check — warn if claude CLI is missing. Don't block
24
- // startup: the user may still want to configure settings or browse workspaces
25
- // before installing Claude Code.
23
+ // Runtime prerequisite check — warn if the claude CLI is missing. Don't block
24
+ // startup: the user may still want to configure settings or browse workspaces.
26
25
  {
27
26
  const check = spawnSync('claude', ['--version'], { stdio: 'ignore' });
28
27
  if (check.error && check.error.code === 'ENOENT') {
@@ -30,18 +29,18 @@ import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/
30
29
  }
31
30
  }
32
31
  console.log(`[kobo] Kōbō home: ${getKoboHome()}`);
33
- // 1. Initialize DB + run migrations
32
+ // Initialize DB + run migrations
34
33
  const db = getDb();
35
34
  runMigrations(db);
36
- // 2. Initialize process cleanup, agent watchdog, and PR watcher
35
+ // Initialize process cleanup, agent watchdog, and PR watcher
37
36
  initProcessCleanup();
38
37
  startWatchdog();
39
38
  startPrWatcher();
40
- // 3. Create Hono app
39
+ // Create Hono app
41
40
  const app = new Hono();
42
41
  // Health check (root / is handled by the SPA catch-all below)
43
42
  app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion() }));
44
- // 4. Mount route sub-routers
43
+ // Mount route sub-routers
45
44
  app.route('/api/workspaces', workspacesRouter);
46
45
  app.route('/api/workspaces', imagesRouter);
47
46
  app.route('/api/notion', notionRouter);
@@ -51,7 +50,7 @@ app.route('/api/dev-server', devServerRouter);
51
50
  // Skills endpoint
52
51
  app.get('/api/skills', (c) => c.json(getAvailableSkills()));
53
52
  const PORT = parseInt(process.env.SERVER_PORT || process.env.PORT || '3000', 10);
54
- // 9. Serve static files from the built SPA if present (production mode).
53
+ // Serve static files from the built SPA if present (production mode).
55
54
  // The path is resolved relative to the package install directory, so this
56
55
  // works both in dev (tsx running from src/) and when installed via npm / npx
57
56
  // (node running from dist/).
@@ -91,7 +90,7 @@ if (clientDistPath) {
91
90
  });
92
91
  });
93
92
  }
94
- // 5. Create HTTP server via @hono/node-server
93
+ // Create HTTP server via @hono/node-server
95
94
  const server = serve({
96
95
  fetch: app.fetch,
97
96
  port: PORT,
@@ -99,13 +98,13 @@ const server = serve({
99
98
  setBackendPort(info.port);
100
99
  console.log(`Server running at http://localhost:${info.port}`);
101
100
  });
102
- // 6. Create WebSocketServer attached to the HTTP server
101
+ // Create WebSocketServer attached to the HTTP server
103
102
  const wss = new WebSocketServer({ noServer: true });
104
- // 7. Wire WebSocket connections to websocket-service.handleConnection()
103
+ // Wire WebSocket connections to websocket-service.handleConnection()
105
104
  wss.on('connection', (ws) => {
106
105
  handleConnection(ws);
107
106
  });
108
- // 8. Wire websocket-service message handler to agent-manager
107
+ // Wire websocket-service message handler to agent-manager
109
108
  setMessageHandler((type, payload) => {
110
109
  const p = payload;
111
110
  if (type === 'chat:message' && p?.workspaceId && p?.content) {
@@ -182,21 +181,21 @@ server.on('upgrade', (request, socket, head) => {
182
181
  socket.destroy();
183
182
  }
184
183
  });
185
- // 9. Graceful shutdown handler
184
+ // Graceful shutdown handler
186
185
  let isShuttingDown = false;
187
186
  function gracefulShutdown(signal) {
188
187
  if (isShuttingDown)
189
188
  return;
190
189
  isShuttingDown = true;
191
190
  console.log(`\n[kobo] Received ${signal}, shutting down gracefully…`);
192
- // 1. Stop accepting new connections
191
+ // Stop accepting new connections
193
192
  wss.close(() => {
194
193
  console.log('[kobo] WebSocket server closed');
195
194
  });
196
195
  server.close(() => {
197
196
  console.log('[kobo] HTTP server closed');
198
197
  });
199
- // 2. Stop background services
198
+ // Stop background services
200
199
  try {
201
200
  stopWatchdog();
202
201
  }
@@ -209,7 +208,7 @@ function gracefulShutdown(signal) {
209
208
  catch {
210
209
  // Best-effort
211
210
  }
212
- // 3. Kill all tracked child processes (agents, dev servers)
211
+ // Kill all tracked child processes (agents, dev servers)
213
212
  try {
214
213
  killAllTrackedProcesses();
215
214
  console.log('[kobo] Tracked processes killed');
@@ -217,7 +216,7 @@ function gracefulShutdown(signal) {
217
216
  catch {
218
217
  // Best-effort
219
218
  }
220
- // 4. Close database
219
+ // Close database
221
220
  try {
222
221
  closeDb();
223
222
  console.log('[kobo] Database closed');
@@ -225,7 +224,7 @@ function gracefulShutdown(signal) {
225
224
  catch {
226
225
  // Best-effort
227
226
  }
228
- // 4. Give a short grace period for in-flight requests, then exit
227
+ // Give a short grace period for in-flight requests, then exit
229
228
  setTimeout(() => {
230
229
  console.log('[kobo] Shutdown complete');
231
230
  process.exit(0);
@@ -1,6 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { getDevServerLogs, getStatus, startDevServer, stopDevServer } from '../services/dev-server-service.js';
3
3
  import { getWorkspace } from '../services/workspace-service.js';
4
+ /** Hono sub-router for per-workspace dev server lifecycle (start, stop, status, logs). */
4
5
  const app = new Hono();
5
6
  // GET /api/dev-server/:workspaceId/status
6
7
  app.get('/:workspaceId/status', (c) => {
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { listBranches, listRemoteBranches } from '../utils/git-ops.js';
3
+ /** Hono sub-router for git-related endpoints (branch listing). */
3
4
  const app = new Hono();
4
5
  // GET /api/git/branches?path=<repoPath> — list branches for a repo
5
6
  app.get('/branches', (c) => {
@@ -1,8 +1,11 @@
1
1
  import { Hono } from 'hono';
2
2
  import * as imageService from '../services/image-service.js';
3
3
  import * as workspaceService from '../services/workspace-service.js';
4
+ /** Maximum allowed upload size for a single image (10 MB). */
4
5
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
6
+ /** MIME types accepted for image uploads. */
5
7
  const ALLOWED_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
8
+ /** Hono sub-router for workspace image upload and deletion. */
6
9
  const app = new Hono();
7
10
  // POST /:id/images — upload an image
8
11
  app.post('/:id/images', async (c) => {
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { extractNotionPage } from '../services/notion-service.js';
3
+ /** Hono sub-router for Notion page extraction. */
3
4
  const app = new Hono();
4
5
  // POST /api/notion/extract — extract a Notion page
5
6
  app.post('/extract', async (c) => {
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import * as settingsService from '../services/settings-service.js';
3
+ /** Hono sub-router for global and per-project settings CRUD. */
3
4
  const app = new Hono();
4
5
  // GET /api/settings — return full settings
5
6
  app.get('/', (c) => {