@loicngr/kobo 1.5.5 → 1.5.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 (183) hide show
  1. package/dist/server/db/migrations.js +14 -0
  2. package/dist/server/db/schema.js +3 -1
  3. package/dist/server/index.js +17 -1
  4. package/dist/server/routes/dev-server.js +1 -1
  5. package/dist/server/routes/health.js +95 -0
  6. package/dist/server/routes/search.js +33 -0
  7. package/dist/server/routes/settings.js +30 -0
  8. package/dist/server/routes/workspaces.js +55 -0
  9. package/dist/server/services/db-backup-service.js +66 -0
  10. package/dist/server/services/dev-server-service.js +24 -1
  11. package/dist/server/services/search-service.js +102 -0
  12. package/dist/server/services/settings-service.js +98 -10
  13. package/dist/server/services/templates-service.js +31 -0
  14. package/dist/server/services/workspace-service.js +55 -1
  15. package/dist/server/services/worktree-service.js +3 -2
  16. package/dist/server/utils/git-ops.js +14 -0
  17. package/dist/server/utils/paths.js +13 -1
  18. package/package.json +1 -1
  19. package/src/client/dist/spa/assets/ActivityFeed-Dc1oLbwJ.js +10 -0
  20. package/src/client/dist/spa/assets/ClosePopup-CdSn7HO8.js +1 -0
  21. package/src/client/dist/spa/assets/CreatePage-BDKfkW-N.js +2 -0
  22. package/src/client/dist/spa/assets/CreatePage-dMi4xVYN.css +1 -0
  23. package/src/client/dist/spa/assets/DiffViewer-DZ9h2M2n.js +2 -0
  24. package/src/client/dist/spa/assets/HealthPage-BkqexlJb.js +1 -0
  25. package/src/client/dist/spa/assets/{MainLayout-D9UzZ0l0.js → MainLayout-VxUBOt-P.js} +17 -17
  26. package/src/client/dist/spa/assets/MainLayout-rVleAIBi.css +1 -0
  27. package/src/client/dist/spa/assets/QBadge-Bvh-hQ8K.js +1 -0
  28. package/src/client/dist/spa/assets/QBtn-BsD8vrWq.js +1 -0
  29. package/src/client/dist/spa/assets/QDialog-CkbLS1If.js +1 -0
  30. package/src/client/dist/spa/assets/QExpansionItem-C735ptO9.js +1 -0
  31. package/src/client/dist/spa/assets/QItem-DfoP6eYj.js +1 -0
  32. package/src/client/dist/spa/assets/QList-D80ms7bw.js +1 -0
  33. package/src/client/dist/spa/assets/QMenu-DU-wiY_A.js +1 -0
  34. package/src/client/dist/spa/assets/QPage-BKY2-sf-.js +1 -0
  35. package/src/client/dist/spa/assets/QSpace-C5Ebr0vq.js +1 -0
  36. package/src/client/dist/spa/assets/QSpinner-CliSLjf8.js +1 -0
  37. package/src/client/dist/spa/assets/QSpinnerDots-Dp12eHrB.js +1 -0
  38. package/src/client/dist/spa/assets/QTabPanels-DV1b1MQb.js +1 -0
  39. package/src/client/dist/spa/assets/QToggle-B0HvuNEg.js +1 -0
  40. package/src/client/dist/spa/assets/QTooltip-kLXuUa_m.js +1 -0
  41. package/src/client/dist/spa/assets/SearchPage-DWglAeQv.css +1 -0
  42. package/src/client/dist/spa/assets/SearchPage-ZDAo7WgD.js +1 -0
  43. package/src/client/dist/spa/assets/SettingsPage-CLMCHMpz.css +1 -0
  44. package/src/client/dist/spa/assets/SettingsPage-D89evCuo.js +1 -0
  45. package/src/client/dist/spa/assets/TouchPan-CVMnGs0y.js +1 -0
  46. package/src/client/dist/spa/assets/WorkspacePage-CWRMLYs-.css +1 -0
  47. package/src/client/dist/spa/assets/WorkspacePage-Ds5Dqxas.js +4 -0
  48. package/src/client/dist/spa/assets/_plugin-vue_export-helper-Cxt1D8wE.js +1 -0
  49. package/src/client/dist/spa/assets/{cssMode-CzYfUUcg.js → cssMode-C9wGTDAD.js} +1 -1
  50. package/src/client/dist/spa/assets/{editor.api-Do3RvGrr.js → editor.api-K7V5sl05.js} +1 -1
  51. package/src/client/dist/spa/assets/{editor.main-BXGLrNaH.js → editor.main-vZ6V2hrP.js} +3 -3
  52. package/src/client/dist/spa/assets/focus-manager-DYbz9jFW.js +1 -0
  53. package/src/client/dist/spa/assets/formatters-BzaS4w0I.js +1 -0
  54. package/src/client/dist/spa/assets/{freemarker2-DD7dLGS5.js → freemarker2-CRk6pTND.js} +1 -1
  55. package/src/client/dist/spa/assets/{handlebars-C6fNrY_c.js → handlebars-Cs3bFomb.js} +1 -1
  56. package/src/client/dist/spa/assets/{html-CsN_ZrNj.js → html-BT4-1gwt.js} +1 -1
  57. package/src/client/dist/spa/assets/{htmlMode-BePbSvxH.js → htmlMode-DZ9LYDVG.js} +1 -1
  58. package/src/client/dist/spa/assets/i18n-C0RbMxeL.js +1 -0
  59. package/src/client/dist/spa/assets/i18n-CkN9X6lQ.js +1 -0
  60. package/src/client/dist/spa/assets/index-Br4eMfSu.js +5 -0
  61. package/src/client/dist/spa/assets/{javascript-CkZZHPRD.js → javascript-C0nTLIDg.js} +1 -1
  62. package/src/client/dist/spa/assets/{jsonMode-CsqbZ0c2.js → jsonMode-LIrD4Pxq.js} +1 -1
  63. package/src/client/dist/spa/assets/{liquid-CB4bbF-9.js → liquid-BbaUnvHA.js} +1 -1
  64. package/src/client/dist/spa/assets/{marked.esm-DIsIySzr.js → marked.esm-gIBce057.js} +1 -1
  65. package/src/client/dist/spa/assets/{mdx-goM-jFgd.js → mdx-B6iNIRi2.js} +1 -1
  66. package/src/client/dist/spa/assets/models-C3h6lSte.js +1 -0
  67. package/src/client/dist/spa/assets/{monaco.contribution-tq8HWx81.js → monaco.contribution-CZ_PxrB6.js} +2 -2
  68. package/src/client/dist/spa/assets/pinia-C3JsrLkB.js +1 -0
  69. package/src/client/dist/spa/assets/private.use-form-BhKyDtO7.js +1 -0
  70. package/src/client/dist/spa/assets/{python-BKb3PkXw.js → python-C0uk6BYc.js} +1 -1
  71. package/src/client/dist/spa/assets/rate-limit-labels-dCPVjS61.js +6 -0
  72. package/src/client/dist/spa/assets/{razor-rLKJylZh.js → razor-Bj__h6OQ.js} +1 -1
  73. package/src/client/dist/spa/assets/runtime-core.esm-bundler-C3IgBgY5.js +1 -0
  74. package/src/client/dist/spa/assets/scroll-CLibRGI-.js +1 -0
  75. package/src/client/dist/spa/assets/settings-B69lIVX0.js +1 -0
  76. package/src/client/dist/spa/assets/symbols-CAg-nBkV.js +1 -0
  77. package/src/client/dist/spa/assets/touch-ChrvzrnI.js +1 -0
  78. package/src/client/dist/spa/assets/{tsMode-CjX_dUdq.js → tsMode-ZSZcAFKU.js} +1 -1
  79. package/src/client/dist/spa/assets/{typescript-Bg5NM6yP.js → typescript-Bgw42jIH.js} +1 -1
  80. package/src/client/dist/spa/assets/use-dark-DnuCB6tC.js +1 -0
  81. package/src/client/dist/spa/assets/vue-i18n-eUDnMrPl.js +3 -0
  82. package/src/client/dist/spa/assets/{xml-BMWcIRqp.js → xml-B9gVeW9V.js} +1 -1
  83. package/src/client/dist/spa/assets/{yaml-C2uOvXS0.js → yaml-DUqEwwz-.js} +1 -1
  84. package/src/client/dist/spa/index.html +15 -7
  85. package/src/client/dist/spa/assets/ActivityFeed-CAgtfoqj.js +0 -10
  86. package/src/client/dist/spa/assets/ClosePopup-BfANRQ7n.js +0 -1
  87. package/src/client/dist/spa/assets/CreatePage-CNnVeRGC.css +0 -1
  88. package/src/client/dist/spa/assets/CreatePage-CnsmjbZl.js +0 -2
  89. package/src/client/dist/spa/assets/DiffViewer-CwAhR0Bt.js +0 -2
  90. package/src/client/dist/spa/assets/MainLayout-1KXWlMYS.css +0 -1
  91. package/src/client/dist/spa/assets/QBadge-DENdn6Dv.js +0 -1
  92. package/src/client/dist/spa/assets/QExpansionItem-ZAI_l6AW.js +0 -1
  93. package/src/client/dist/spa/assets/QList-aKZe9BNZ.js +0 -1
  94. package/src/client/dist/spa/assets/QMenu-DDXq2SFM.js +0 -1
  95. package/src/client/dist/spa/assets/QSpinnerDots-DytntAbf.js +0 -1
  96. package/src/client/dist/spa/assets/SettingsPage-DCgRTq-c.js +0 -1
  97. package/src/client/dist/spa/assets/SettingsPage-oWZ8sGFm.css +0 -1
  98. package/src/client/dist/spa/assets/TouchPan-D_gmnARo.js +0 -1
  99. package/src/client/dist/spa/assets/WorkspacePage-BSBiQTKy.js +0 -4
  100. package/src/client/dist/spa/assets/WorkspacePage-DZwv9kFd.css +0 -1
  101. package/src/client/dist/spa/assets/_plugin-vue_export-helper-DkL3SKZ8.js +0 -1
  102. package/src/client/dist/spa/assets/formatters-hnFdqDO2.js +0 -6
  103. package/src/client/dist/spa/assets/i18n-C_tQYbyT.js +0 -1
  104. package/src/client/dist/spa/assets/i18n-zwZDkEfm.js +0 -1
  105. package/src/client/dist/spa/assets/index-BJT6ZfVp.js +0 -5
  106. package/src/client/dist/spa/assets/models-Dwzf2zDK.js +0 -1
  107. package/src/client/dist/spa/assets/private.use-form-BWfHn8G9.js +0 -1
  108. package/src/client/dist/spa/assets/scroll-CHtHNzvE.js +0 -1
  109. package/src/client/dist/spa/assets/settings-BdrDg_dO.js +0 -1
  110. package/src/client/dist/spa/assets/touch-5Jv2Ep3F.js +0 -1
  111. package/src/client/dist/spa/assets/use-checkbox-By4PLtaB.js +0 -1
  112. package/src/client/dist/spa/assets/vue-i18n-CFW7O-jl.js +0 -3
  113. /package/src/client/dist/spa/assets/{abap-Dvixoi-w.js → abap-CFuyUYKP.js} +0 -0
  114. /package/src/client/dist/spa/assets/{apex-DP7X7Z9Q.js → apex-Ctq_xcrv.js} +0 -0
  115. /package/src/client/dist/spa/assets/{azcli-BdzyZYsP.js → azcli-BBQSVn-C.js} +0 -0
  116. /package/src/client/dist/spa/assets/{bat-BskgK8bi.js → bat-DbnqAfvr.js} +0 -0
  117. /package/src/client/dist/spa/assets/{bicep-BB_erhjR.js → bicep-BtDlIXop.js} +0 -0
  118. /package/src/client/dist/spa/assets/{cameligo-PvLD8t4t.js → cameligo-BLeJgKTj.js} +0 -0
  119. /package/src/client/dist/spa/assets/{clojure-CjHVLzZR.js → clojure-aZUQIUKP.js} +0 -0
  120. /package/src/client/dist/spa/assets/{coffee-DCoMPIwW.js → coffee-Secadq9U.js} +0 -0
  121. /package/src/client/dist/spa/assets/{cpp-kftb9yup.js → cpp-JicRPTRv.js} +0 -0
  122. /package/src/client/dist/spa/assets/{csharp-CYKl40UV.js → csharp-C7NSOZyj.js} +0 -0
  123. /package/src/client/dist/spa/assets/{csp-d1_9kWqP.js → csp-CIje7830.js} +0 -0
  124. /package/src/client/dist/spa/assets/{css-CSa8r6i8.js → css-G0bm1q_M.js} +0 -0
  125. /package/src/client/dist/spa/assets/{cypher-FoBj7udD.js → cypher-CldD5D0u.js} +0 -0
  126. /package/src/client/dist/spa/assets/{dart-BoITdHw1.js → dart-DIK3l8YT.js} +0 -0
  127. /package/src/client/dist/spa/assets/{dockerfile-CRYieHSB.js → dockerfile-czxaGh2L.js} +0 -0
  128. /package/src/client/dist/spa/assets/{ecl-BisizQmF.js → ecl-BqdYhwmw.js} +0 -0
  129. /package/src/client/dist/spa/assets/{elixir-DMza7PS-.js → elixir-m52LePTW.js} +0 -0
  130. /package/src/client/dist/spa/assets/{flow9-DZyDUZ6t.js → flow9-B5QJ9GvZ.js} +0 -0
  131. /package/src/client/dist/spa/assets/{format-Ex4UmWdz.js → format-Cyg8IgRi.js} +0 -0
  132. /package/src/client/dist/spa/assets/{fsharp-CKv7fFew.js → fsharp-B15czHsH.js} +0 -0
  133. /package/src/client/dist/spa/assets/{go-DXZqPXQn.js → go-BkoQxDo1.js} +0 -0
  134. /package/src/client/dist/spa/assets/{graphql-DrGE7IlY.js → graphql-BnI6uRa_.js} +0 -0
  135. /package/src/client/dist/spa/assets/{hcl-DLEN_UIX.js → hcl-CAwwENT7.js} +0 -0
  136. /package/src/client/dist/spa/assets/{ini-mQpaJDpd.js → ini-BHM5zh1H.js} +0 -0
  137. /package/src/client/dist/spa/assets/{java-jjG-xxwG.js → java-B5i95QvQ.js} +0 -0
  138. /package/src/client/dist/spa/assets/{julia-C50bVRho.js → julia-DPDm885q.js} +0 -0
  139. /package/src/client/dist/spa/assets/{kotlin-DwU9Bxzl.js → kotlin-qoccd5BP.js} +0 -0
  140. /package/src/client/dist/spa/assets/{less-Duxayr-t.js → less-B6RU166D.js} +0 -0
  141. /package/src/client/dist/spa/assets/{lexon-D3Je_oKk.js → lexon-YfUeoL1V.js} +0 -0
  142. /package/src/client/dist/spa/assets/{lua-DqW2q2h7.js → lua-BIUI5y9b.js} +0 -0
  143. /package/src/client/dist/spa/assets/{m3-CO3sU7vD.js → m3-D5SAbSdU.js} +0 -0
  144. /package/src/client/dist/spa/assets/{markdown-CbqjBAOS.js → markdown-CVJLwHzJ.js} +0 -0
  145. /package/src/client/dist/spa/assets/{mips-Bj7AN96v.js → mips-R-FZ3zOR.js} +0 -0
  146. /package/src/client/dist/spa/assets/{msdax-aLgc6HYp.js → msdax-Blveyl9r.js} +0 -0
  147. /package/src/client/dist/spa/assets/{mysql-QJHu-LiC.js → mysql-D4mY1AFx.js} +0 -0
  148. /package/src/client/dist/spa/assets/{objective-c-BRcZj02O.js → objective-c-BmXrLr4h.js} +0 -0
  149. /package/src/client/dist/spa/assets/{pascal-j04gUgnY.js → pascal-yxckoyvV.js} +0 -0
  150. /package/src/client/dist/spa/assets/{pascaligo-6Lz1Pduc.js → pascaligo-Q5JCwXMI.js} +0 -0
  151. /package/src/client/dist/spa/assets/{perl-Ny9A9N4B.js → perl-BF1Rrs5h.js} +0 -0
  152. /package/src/client/dist/spa/assets/{pgsql-gEZ-9kd2.js → pgsql-CnYB97wm.js} +0 -0
  153. /package/src/client/dist/spa/assets/{php-B81GxKax.js → php-CdDfQfSg.js} +0 -0
  154. /package/src/client/dist/spa/assets/{pla-DS3E9Auu.js → pla-whj-d71F.js} +0 -0
  155. /package/src/client/dist/spa/assets/{postiats-CHuHziKk.js → postiats-ClfLr4I-.js} +0 -0
  156. /package/src/client/dist/spa/assets/{powerquery-DWZsw4AU.js → powerquery-iRaBhuuk.js} +0 -0
  157. /package/src/client/dist/spa/assets/{powershell-MAGzBvoK.js → powershell-DjiEt5xK.js} +0 -0
  158. /package/src/client/dist/spa/assets/{protobuf-CuqThPZN.js → protobuf-B6dcIEUr.js} +0 -0
  159. /package/src/client/dist/spa/assets/{pug-DpwILTue.js → pug-DtmHnjM9.js} +0 -0
  160. /package/src/client/dist/spa/assets/{qsharp-Clbrosr3.js → qsharp-CELCyd79.js} +0 -0
  161. /package/src/client/dist/spa/assets/{r-dNsdrNRn.js → r-ZpJXWV-o.js} +0 -0
  162. /package/src/client/dist/spa/assets/{redis-DRY-nYja.js → redis-BiHSNkAl.js} +0 -0
  163. /package/src/client/dist/spa/assets/{redshift-CGzDbZLe.js → redshift-DzuwYCHP.js} +0 -0
  164. /package/src/client/dist/spa/assets/{restructuredtext-DiI6OCjv.js → restructuredtext-YOT94bbS.js} +0 -0
  165. /package/src/client/dist/spa/assets/{ruby-DTTFtlqw.js → ruby-BfiHr6Uu.js} +0 -0
  166. /package/src/client/dist/spa/assets/{rust-Cv26V_Fa.js → rust-JZ-uOoYM.js} +0 -0
  167. /package/src/client/dist/spa/assets/{sb-WM3UPTLr.js → sb-CBglP1-t.js} +0 -0
  168. /package/src/client/dist/spa/assets/{scala-Brw-oZFc.js → scala-C9l41paw.js} +0 -0
  169. /package/src/client/dist/spa/assets/{scheme-B9_NJi2n.js → scheme-B-InQ6hy.js} +0 -0
  170. /package/src/client/dist/spa/assets/{scss-AJXcdTUI.js → scss-v6OmJRN9.js} +0 -0
  171. /package/src/client/dist/spa/assets/{shell-B8rXUKDl.js → shell-Dyp6iwB6.js} +0 -0
  172. /package/src/client/dist/spa/assets/{solidity-DBduMAba.js → solidity-D5epNWue.js} +0 -0
  173. /package/src/client/dist/spa/assets/{sophia-CLkSrv5f.js → sophia-Eva-79sB.js} +0 -0
  174. /package/src/client/dist/spa/assets/{sparql-8jfjvjhF.js → sparql-gvALLO1w.js} +0 -0
  175. /package/src/client/dist/spa/assets/{sql-DCI2HndY.js → sql-COdamZYI.js} +0 -0
  176. /package/src/client/dist/spa/assets/{st-YiBlEXuf.js → st-eMoImIwE.js} +0 -0
  177. /package/src/client/dist/spa/assets/{swift-BpRZ1CVX.js → swift-7R_T9RYH.js} +0 -0
  178. /package/src/client/dist/spa/assets/{systemverilog-2_XLvhYi.js → systemverilog-1pCEfaHU.js} +0 -0
  179. /package/src/client/dist/spa/assets/{tcl-6xehSJT3.js → tcl-B_KgnhfE.js} +0 -0
  180. /package/src/client/dist/spa/assets/{twig-B80I9c0D.js → twig-CFZUJxb9.js} +0 -0
  181. /package/src/client/dist/spa/assets/{typespec-BsAPJtGA.js → typespec-B1ZgHlud.js} +0 -0
  182. /package/src/client/dist/spa/assets/{vb-B6Q-99Bj.js → vb-DKdun5tL.js} +0 -0
  183. /package/src/client/dist/spa/assets/{wgsl-DPkCsCSS.js → wgsl-CzNaxTrn.js} +0 -0
@@ -64,6 +64,20 @@ export const migrations = [
64
64
  db.exec("ALTER TABLE workspaces ADD COLUMN reasoning_effort TEXT NOT NULL DEFAULT 'auto'");
65
65
  },
66
66
  },
67
+ {
68
+ version: 8,
69
+ name: 'add-workspace-favorited-at',
70
+ migrate: (db) => {
71
+ db.prepare('ALTER TABLE workspaces ADD COLUMN favorited_at TEXT').run();
72
+ },
73
+ },
74
+ {
75
+ version: 9,
76
+ name: 'add-workspace-tags',
77
+ migrate: (db) => {
78
+ db.prepare("ALTER TABLE workspaces ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'").run();
79
+ },
80
+ },
67
81
  ];
68
82
  /** Current schema version — always equals the highest migration version. */
69
83
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -10,12 +10,14 @@ export function initSchema(db) {
10
10
  status TEXT NOT NULL DEFAULT 'created',
11
11
  notion_url TEXT,
12
12
  notion_page_id TEXT,
13
- model TEXT NOT NULL DEFAULT 'claude-opus-4-6',
13
+ model TEXT NOT NULL DEFAULT 'claude-opus-4-7',
14
14
  reasoning_effort TEXT NOT NULL DEFAULT 'auto',
15
15
  permission_mode TEXT NOT NULL DEFAULT 'auto-accept',
16
16
  dev_server_status TEXT NOT NULL DEFAULT 'stopped',
17
17
  has_unread INTEGER NOT NULL DEFAULT 0,
18
18
  archived_at TEXT,
19
+ favorited_at TEXT,
20
+ tags TEXT NOT NULL DEFAULT '[]',
19
21
  created_at TEXT NOT NULL,
20
22
  updated_at TEXT NOT NULL
21
23
  );
@@ -9,20 +9,23 @@ 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
11
  import gitRouter from './routes/git.js';
12
+ import healthRouter from './routes/health.js';
12
13
  import imagesRouter from './routes/images.js';
13
14
  import notionRouter from './routes/notion.js';
14
15
  import plansRouter from './routes/plans.js';
16
+ import searchRouter from './routes/search.js';
15
17
  import sentryRouter from './routes/sentry.js';
16
18
  import settingsRouter from './routes/settings.js';
17
19
  import templatesRouter from './routes/templates.js';
18
20
  import workspacesRouter from './routes/workspaces.js';
19
21
  import { getAvailableSkills, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopWatchdog, } from './services/agent-manager.js';
22
+ import { createDailyDbBackupIfNeeded } from './services/db-backup-service.js';
20
23
  import { startDevServer, stopDevServer } from './services/dev-server-service.js';
21
24
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
22
25
  import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
23
26
  import { emit, handleConnection, setMessageHandler } from './services/websocket-service.js';
24
27
  import { getActiveSession, getWorkspace, updateWorkspaceStatus } from './services/workspace-service.js';
25
- import { getClientSpaPath, getKoboHome, getPackageVersion } from './utils/paths.js';
28
+ import { getClientSpaPath, getDbPath, getKoboHome, getPackageVersion } from './utils/paths.js';
26
29
  import { initProcessCleanup, killAll as killAllTrackedProcesses } from './utils/process-tracker.js';
27
30
  // Runtime prerequisite check — warn if the claude CLI is missing. Don't block
28
31
  // startup: the user may still want to configure settings or browse workspaces.
@@ -36,6 +39,17 @@ console.log(`[kobo] Kōbō home: ${getKoboHome()}`);
36
39
  // Initialize DB + run migrations
37
40
  const db = getDb();
38
41
  runMigrations(db);
42
+ // Daily DB backup (best-effort, fire-and-forget — never blocks boot).
43
+ // Creates a WAL-safe snapshot alongside kobo.db if no backup exists in the
44
+ // last 24h, and rotates out older backups beyond the retention window.
45
+ void createDailyDbBackupIfNeeded(db, getDbPath()).then((r) => {
46
+ if (r.created) {
47
+ console.log(`[kobo] Daily DB backup: ${r.created}`);
48
+ if (r.deleted.length > 0) {
49
+ console.log(`[kobo] Rotated ${r.deleted.length} old DB backup(s)`);
50
+ }
51
+ }
52
+ });
39
53
  // Initialize process cleanup, agent watchdog, and PR watcher
40
54
  initProcessCleanup();
41
55
  startWatchdog();
@@ -54,6 +68,8 @@ app.route('/api/settings', settingsRouter);
54
68
  app.route('/api/dev-server', devServerRouter);
55
69
  app.route('/api/templates', templatesRouter);
56
70
  app.route('/api/workspaces', plansRouter);
71
+ app.route('/api/search', searchRouter);
72
+ app.route('/api/health', healthRouter);
57
73
  // Skills endpoint
58
74
  app.get('/api/skills', (c) => c.json(getAvailableSkills()));
59
75
  const PORT = parseInt(process.env.SERVER_PORT || process.env.PORT || '3000', 10);
@@ -11,7 +11,7 @@ app.get('/:workspaceId/status', (c) => {
11
11
  if (!workspace) {
12
12
  return c.json({ error: `Workspace '${workspaceId}' not found` }, 404);
13
13
  }
14
- const status = getStatus(workspace.projectPath, workspace.workingBranch);
14
+ const status = getStatus(workspace.projectPath, workspace.workingBranch, workspaceId);
15
15
  // If runtime detection returns unknown, use persisted status from DB
16
16
  if (status.status === 'unknown' && workspace.devServerStatus && workspace.devServerStatus !== 'stopped') {
17
17
  status.status = workspace.devServerStatus;
@@ -0,0 +1,95 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { Hono } from 'hono';
5
+ import { getDb } from '../db/index.js';
6
+ import { SCHEMA_VERSION } from '../db/migrations.js';
7
+ import { getGlobalSettings, SETTINGS_SCHEMA_VERSION } from '../services/settings-service.js';
8
+ import { getDbPath, getKoboHome } from '../utils/paths.js';
9
+ const app = new Hono();
10
+ function checkClaudeCli() {
11
+ try {
12
+ const r = spawnSync('claude', ['--version'], { encoding: 'utf-8' });
13
+ if (r.error || r.status !== 0)
14
+ return { available: false, version: null };
15
+ return { available: true, version: (r.stdout ?? '').trim() || null };
16
+ }
17
+ catch {
18
+ return { available: false, version: null };
19
+ }
20
+ }
21
+ function isProcessAlive(pid) {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ function safeFileSize(p) {
31
+ try {
32
+ return fs.statSync(p).size;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ // GET /api/health/report — detailed health diagnostics for the Health panel.
39
+ app.get('/report', (c) => {
40
+ const db = getDb();
41
+ const dbPath = getDbPath();
42
+ const home = getKoboHome();
43
+ // DB schema version
44
+ const row = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get();
45
+ const dbSchemaVersion = row?.v ?? 0;
46
+ // Workspaces + worktrees on-disk check
47
+ const workspaces = db
48
+ .prepare('SELECT id, name, project_path, working_branch, archived_at FROM workspaces')
49
+ .all();
50
+ const worktreesMissing = [];
51
+ for (const ws of workspaces) {
52
+ if (ws.archived_at)
53
+ continue;
54
+ const wtPath = path.join(ws.project_path, '.worktrees', ws.working_branch);
55
+ if (!fs.existsSync(wtPath)) {
56
+ worktreesMissing.push({ workspaceId: ws.id, name: ws.name, path: wtPath, exists: false });
57
+ }
58
+ }
59
+ // Orphan agent sessions — marked running but PID no longer alive
60
+ const runningSessions = db
61
+ .prepare("SELECT pid FROM agent_sessions WHERE status = 'running' AND pid IS NOT NULL")
62
+ .all();
63
+ let orphaned = 0;
64
+ for (const s of runningSessions) {
65
+ if (s.pid && !isProcessAlive(s.pid))
66
+ orphaned++;
67
+ }
68
+ const globalSettings = getGlobalSettings();
69
+ const settingsRow = db.prepare('SELECT COUNT(*) as n FROM workspaces').get();
70
+ const archivedRow = db.prepare('SELECT COUNT(*) as n FROM workspaces WHERE archived_at IS NOT NULL').get();
71
+ const report = {
72
+ koboHome: home,
73
+ db: {
74
+ path: dbPath,
75
+ sizeBytes: safeFileSize(dbPath),
76
+ schemaVersion: dbSchemaVersion,
77
+ currentSchemaVersion: SCHEMA_VERSION,
78
+ },
79
+ settings: { schemaVersion: SETTINGS_SCHEMA_VERSION },
80
+ claudeCli: checkClaudeCli(),
81
+ workspaces: {
82
+ total: settingsRow.n,
83
+ archived: archivedRow.n,
84
+ worktreesMissing,
85
+ },
86
+ agentSessions: { orphaned },
87
+ integrations: {
88
+ notion: { configured: Boolean(globalSettings.notionMcpKey) },
89
+ sentry: { configured: Boolean(globalSettings.sentryMcpKey) },
90
+ editor: { configured: Boolean(globalSettings.editorCommand) },
91
+ },
92
+ };
93
+ return c.json(report);
94
+ });
95
+ export default app;
@@ -0,0 +1,33 @@
1
+ import { Hono } from 'hono';
2
+ import { searchEvents } from '../services/search-service.js';
3
+ const DEFAULT_LIMIT = 50;
4
+ const MAX_LIMIT = 200;
5
+ const app = new Hono();
6
+ // GET /api/search?q=...&limit=50&includeArchived=true
7
+ // Search readable text across ws_events (user messages + agent outputs),
8
+ // joined with workspaces. Returns up to `limit` snippets, most recent first.
9
+ app.get('/', (c) => {
10
+ const qRaw = c.req.query('q') ?? '';
11
+ const q = qRaw.trim();
12
+ if (!q) {
13
+ return c.json({ error: "Missing or empty 'q' query parameter" }, 400);
14
+ }
15
+ let limit = DEFAULT_LIMIT;
16
+ const limitRaw = c.req.query('limit');
17
+ if (limitRaw) {
18
+ const parsed = parseInt(limitRaw, 10);
19
+ if (Number.isFinite(parsed) && parsed > 0) {
20
+ limit = Math.min(parsed, MAX_LIMIT);
21
+ }
22
+ }
23
+ const includeArchived = c.req.query('includeArchived') === 'true';
24
+ try {
25
+ const results = searchEvents(q, { limit, includeArchived });
26
+ return c.json(results);
27
+ }
28
+ catch (err) {
29
+ const message = err instanceof Error ? err.message : String(err);
30
+ return c.json({ error: message }, 500);
31
+ }
32
+ });
33
+ export default app;
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import * as settingsService from '../services/settings-service.js';
3
+ import { listTemplates, replaceAllTemplates } from '../services/templates-service.js';
3
4
  /** Hono sub-router for global and per-project settings CRUD. */
4
5
  const app = new Hono();
5
6
  // GET /api/settings — return full settings
@@ -101,4 +102,33 @@ app.delete('/projects/:encodedPath', (c) => {
101
102
  return c.json({ error: message }, 500);
102
103
  }
103
104
  });
105
+ // GET /api/settings/export — download a JSON bundle of settings + templates (MCP keys stripped)
106
+ app.get('/export', (c) => {
107
+ try {
108
+ const bundle = settingsService.exportConfigBundle(listTemplates());
109
+ return c.json(bundle);
110
+ }
111
+ catch (err) {
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ return c.json({ error: message }, 500);
114
+ }
115
+ });
116
+ // POST /api/settings/import — replace settings + templates from an uploaded bundle
117
+ app.post('/import', async (c) => {
118
+ try {
119
+ const body = (await c.req.json());
120
+ // Validate settings first — throws on malformed payload before we touch disk.
121
+ settingsService.importConfigBundle(body);
122
+ if (body.templates !== undefined) {
123
+ // Accept missing templates (backward-compatible). Otherwise validate and replace.
124
+ replaceAllTemplates(body.templates);
125
+ }
126
+ return c.json({ ok: true });
127
+ }
128
+ catch (err) {
129
+ const message = err instanceof Error ? err.message : String(err);
130
+ const isValidation = message.includes('Invalid bundle') || message.includes('Invalid template');
131
+ return c.json({ error: message }, isValidation ? 400 : 500);
132
+ }
133
+ });
104
134
  export default app;
@@ -38,6 +38,15 @@ app.post('/', async (c) => {
38
38
  if (!body.name || !body.projectPath || !body.sourceBranch || !body.workingBranch) {
39
39
  return c.json({ error: 'Missing required fields: name, projectPath, sourceBranch, workingBranch' }, 400);
40
40
  }
41
+ // Fetch the source branch from origin first — if this fails, block creation
42
+ // immediately (no DB records created, user stays on the create page).
43
+ try {
44
+ gitOps.fetchSourceBranch(body.projectPath, body.sourceBranch);
45
+ }
46
+ catch (err) {
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ return c.json({ error: message }, 422);
49
+ }
41
50
  // Create workspace record
42
51
  const globalSettings = settingsService.getGlobalSettings();
43
52
  // workingBranch may be updated after Notion extraction to inject the ticket ID
@@ -747,6 +756,52 @@ app.get('/:id', (c) => {
747
756
  return c.json({ error: message }, 500);
748
757
  }
749
758
  });
759
+ // POST /api/workspaces/:id/favorite — mark workspace as favorite
760
+ app.post('/:id/favorite', (c) => {
761
+ const { id } = c.req.param();
762
+ try {
763
+ const ws = workspaceService.setFavorite(id);
764
+ return c.json(ws);
765
+ }
766
+ catch (err) {
767
+ const msg = err instanceof Error ? err.message : 'Unknown error';
768
+ const status = msg.includes('not found') ? 404 : 500;
769
+ return c.json({ error: msg }, status);
770
+ }
771
+ });
772
+ // DELETE /api/workspaces/:id/favorite — remove favorite from workspace
773
+ app.delete('/:id/favorite', (c) => {
774
+ const { id } = c.req.param();
775
+ try {
776
+ const ws = workspaceService.unsetFavorite(id);
777
+ return c.json(ws);
778
+ }
779
+ catch (err) {
780
+ const msg = err instanceof Error ? err.message : 'Unknown error';
781
+ const status = msg.includes('not found') ? 404 : 500;
782
+ return c.json({ error: msg }, status);
783
+ }
784
+ });
785
+ // PUT /api/workspaces/:id/tags — replace the workspace's tag list
786
+ app.put('/:id/tags', async (c) => {
787
+ const { id } = c.req.param();
788
+ try {
789
+ const body = await c.req.json();
790
+ if (!Array.isArray(body.tags)) {
791
+ return c.json({ error: 'tags must be an array of strings' }, 400);
792
+ }
793
+ if (body.tags.some((t) => typeof t !== 'string')) {
794
+ return c.json({ error: 'tags must contain only strings' }, 400);
795
+ }
796
+ const ws = workspaceService.setWorkspaceTags(id, body.tags);
797
+ return c.json(ws);
798
+ }
799
+ catch (err) {
800
+ const msg = err instanceof Error ? err.message : 'Unknown error';
801
+ const status = msg.includes('not found') ? 404 : 500;
802
+ return c.json({ error: msg }, status);
803
+ }
804
+ });
750
805
  // PATCH /api/workspaces/:id — update workspace fields (status, model, permissionMode, name)
751
806
  app.patch('/:id', async (c) => {
752
807
  try {
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const BACKUP_PREFIX = 'kobo.db.backup-';
4
+ const DEFAULT_KEEP = 7;
5
+ const DAILY_MS = 24 * 60 * 60 * 1000;
6
+ let backupSequence = 0;
7
+ /** Test-only: reset the in-process monotonic sequence to 0. */
8
+ export function _resetBackupSequenceForTests() {
9
+ backupSequence = 0;
10
+ }
11
+ function listBackups(dir) {
12
+ if (!fs.existsSync(dir))
13
+ return [];
14
+ return fs
15
+ .readdirSync(dir)
16
+ .filter((f) => f.startsWith(BACKUP_PREFIX))
17
+ .map((f) => {
18
+ const full = path.join(dir, f);
19
+ const stat = fs.statSync(full);
20
+ return { path: full, mtimeMs: stat.mtimeMs };
21
+ })
22
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
23
+ }
24
+ /**
25
+ * Create a WAL-safe snapshot of the Kōbō DB if no backup has been made in the
26
+ * last 24h. Rotates old backups, keeping only the `keepCount` most recent.
27
+ *
28
+ * - Location: same directory as `dbPath`, named `kobo.db.backup-<ISO>-<seq>`
29
+ * (mirrors the settings.json backup convention).
30
+ * - Uses better-sqlite3's online `.backup()` API, which is safe while the DB
31
+ * is open and under write load (WAL mode).
32
+ * - Best-effort: never throws. On failure, logs to console.error and returns
33
+ * `{ created: null, deleted: [] }` so the boot path is never blocked.
34
+ */
35
+ export async function createDailyDbBackupIfNeeded(db, dbPath, keepCount = DEFAULT_KEEP, nowMs = Date.now()) {
36
+ const result = { created: null, deleted: [] };
37
+ const dir = path.dirname(dbPath);
38
+ try {
39
+ const existing = listBackups(dir);
40
+ const latestMtime = existing[0]?.mtimeMs ?? 0;
41
+ if (latestMtime && nowMs - latestMtime < DAILY_MS) {
42
+ return result;
43
+ }
44
+ const stamp = new Date(nowMs).toISOString().replace(/[:.]/g, '-');
45
+ backupSequence += 1;
46
+ const backupPath = path.join(dir, `${BACKUP_PREFIX}${stamp}-${backupSequence}`);
47
+ await db.backup(backupPath);
48
+ result.created = backupPath;
49
+ const all = listBackups(dir);
50
+ if (all.length > keepCount) {
51
+ for (const entry of all.slice(keepCount)) {
52
+ try {
53
+ fs.unlinkSync(entry.path);
54
+ result.deleted.push(entry.path);
55
+ }
56
+ catch (err) {
57
+ console.error(`[kobo] Failed to delete old DB backup ${entry.path}:`, err);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ catch (err) {
63
+ console.error('[kobo] DB daily backup failed:', err);
64
+ }
65
+ return result;
66
+ }
@@ -16,6 +16,10 @@ function cleanEnv() {
16
16
  // ── State ──────────────────────────────────────────────────────────────────────
17
17
  /** workspaceId -> spawned dev-server process */
18
18
  const trackedProcesses = new Map();
19
+ /** Test-only: clear the tracked-processes map between tests. */
20
+ export function _resetTrackedProcessesForTests() {
21
+ trackedProcesses.clear();
22
+ }
19
23
  // ── Pure helpers ───────────────────────────────────────────────────────────────
20
24
  /**
21
25
  * Sanitize a branch name for use as a Docker instance name.
@@ -94,8 +98,14 @@ export function listRunningContainers() {
94
98
  // ── Status ─────────────────────────────────────────────────────────────────────
95
99
  /**
96
100
  * Get the dev-server status for a given project + branch.
101
+ *
102
+ * When `workspaceId` is provided and a start process for that workspace is
103
+ * still running (e.g. `docker compose up -d` is pulling/building images), the
104
+ * status is reported as `'starting'` even if no matching container is visible
105
+ * in `docker ps` yet. This prevents the UI from flashing to `'stopped'` during
106
+ * long build phases.
97
107
  */
98
- export function getStatus(projectPath, workingBranch) {
108
+ export function getStatus(projectPath, workingBranch, workspaceId) {
99
109
  const config = resolveInstance(projectPath, workingBranch);
100
110
  if (!config) {
101
111
  return {
@@ -119,6 +129,19 @@ export function getStatus(projectPath, workingBranch) {
119
129
  containers: matching,
120
130
  };
121
131
  }
132
+ // No matching container yet — but is a start process still in flight?
133
+ // This covers the long `docker compose up -d` build/pull phase where the
134
+ // CLI hasn't exited yet and containers haven't appeared in `docker ps`.
135
+ if (workspaceId && trackedProcesses.has(workspaceId)) {
136
+ return {
137
+ status: 'starting',
138
+ instanceName: config.instanceName,
139
+ projectName: config.projectName,
140
+ httpPort: config.httpPort,
141
+ url: '',
142
+ containers: [],
143
+ };
144
+ }
122
145
  return {
123
146
  status: 'stopped',
124
147
  instanceName: config.instanceName,
@@ -0,0 +1,102 @@
1
+ import { getDb } from '../db/index.js';
2
+ /** Event types that carry user-authored or assistant-authored readable text. */
3
+ const SEARCHABLE_TYPES = ['user:message', 'agent:output'];
4
+ const SNIPPET_CONTEXT = 100; // chars on each side of the match
5
+ /**
6
+ * Extract the readable text content of a ws_events payload, or `null` when
7
+ * the event carries no natural-language content (system events, rate-limit
8
+ * pings, etc.).
9
+ */
10
+ function extractReadableText(type, payload) {
11
+ if (!payload || typeof payload !== 'object')
12
+ return null;
13
+ const p = payload;
14
+ if (type === 'user:message') {
15
+ return typeof p.content === 'string' ? p.content : null;
16
+ }
17
+ if (type === 'agent:output') {
18
+ // Claude Code streams `{type: 'assistant', message: {content: [{type: 'text', text: '...'}, ...]}}`
19
+ const msg = p.message;
20
+ if (msg && Array.isArray(msg.content)) {
21
+ const parts = [];
22
+ for (const block of msg.content) {
23
+ if (block && typeof block === 'object') {
24
+ const b = block;
25
+ if (b.type === 'text' && typeof b.text === 'string')
26
+ parts.push(b.text);
27
+ }
28
+ }
29
+ return parts.length > 0 ? parts.join('\n') : null;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ function buildSnippet(text, matchIndex, query) {
35
+ const start = Math.max(0, matchIndex - SNIPPET_CONTEXT);
36
+ const end = Math.min(text.length, matchIndex + query.length + SNIPPET_CONTEXT);
37
+ const prefix = start > 0 ? '…' : '';
38
+ const suffix = end < text.length ? '…' : '';
39
+ return `${prefix}${text.slice(start, end).trim()}${suffix}`;
40
+ }
41
+ /**
42
+ * Full-text-ish search across `ws_events.payload` joined with `workspaces`.
43
+ *
44
+ * - Trimmed empty queries return `[]` without hitting the database.
45
+ * - SQLite does the first filter via `LIKE '%q%'` on the raw payload JSON.
46
+ * Results are then post-filtered in JS against the **readable** text
47
+ * (extracted from the JSON) to avoid false positives like matches inside
48
+ * field names or schema strings.
49
+ * - Snippets are 100 chars of context on either side of the first match.
50
+ */
51
+ export function searchEvents(query, options = {}) {
52
+ const trimmed = query.trim();
53
+ if (trimmed.length === 0)
54
+ return [];
55
+ const { limit = 50, includeArchived = false } = options;
56
+ const db = getDb();
57
+ const typePlaceholders = SEARCHABLE_TYPES.map(() => '?').join(', ');
58
+ const archiveFilter = includeArchived ? '' : 'AND w.archived_at IS NULL';
59
+ const likePattern = `%${trimmed}%`;
60
+ // Over-fetch a bit so post-filter rejects don't shrink us below `limit` on
61
+ // realistic datasets, without scanning the entire table.
62
+ const dbLimit = Math.max(limit * 3, 100);
63
+ const rows = db
64
+ .prepare(`SELECT e.workspace_id, e.type, e.created_at, e.payload,
65
+ w.name AS workspace_name, w.archived_at
66
+ FROM ws_events e
67
+ JOIN workspaces w ON e.workspace_id = w.id
68
+ WHERE e.type IN (${typePlaceholders})
69
+ AND e.payload LIKE ?
70
+ ${archiveFilter}
71
+ ORDER BY e.created_at DESC
72
+ LIMIT ?`)
73
+ .all(...SEARCHABLE_TYPES, likePattern, dbLimit);
74
+ const needle = trimmed.toLowerCase();
75
+ const results = [];
76
+ for (const row of rows) {
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(row.payload);
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ const text = extractReadableText(row.type, parsed);
85
+ if (!text)
86
+ continue;
87
+ const idx = text.toLowerCase().indexOf(needle);
88
+ if (idx < 0)
89
+ continue;
90
+ results.push({
91
+ workspaceId: row.workspace_id,
92
+ workspaceName: row.workspace_name,
93
+ archived: row.archived_at !== null,
94
+ type: row.type,
95
+ timestamp: row.created_at,
96
+ snippet: buildSnippet(text, idx, trimmed),
97
+ });
98
+ if (results.length >= limit)
99
+ break;
100
+ }
101
+ return results;
102
+ }