agor-live 0.3.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 (297) hide show
  1. package/LICENSE +94 -0
  2. package/README.md +163 -0
  3. package/bin/agor-daemon.js +20 -0
  4. package/bin/agor.js +14 -0
  5. package/dist/cli/base-command.d.ts +29 -0
  6. package/dist/cli/base-command.js +41 -0
  7. package/dist/cli/commands/board/add-session.d.ts +15 -0
  8. package/dist/cli/commands/board/add-session.js +102 -0
  9. package/dist/cli/commands/board/list.d.ts +14 -0
  10. package/dist/cli/commands/board/list.js +74 -0
  11. package/dist/cli/commands/config/clear.d.ts +13 -0
  12. package/dist/cli/commands/config/clear.js +21 -0
  13. package/dist/cli/commands/config/get.d.ts +13 -0
  14. package/dist/cli/commands/config/get.js +41 -0
  15. package/dist/cli/commands/config/index.d.ts +13 -0
  16. package/dist/cli/commands/config/index.js +118 -0
  17. package/dist/cli/commands/config/set.d.ts +14 -0
  18. package/dist/cli/commands/config/set.js +50 -0
  19. package/dist/cli/commands/config/unset.d.ts +13 -0
  20. package/dist/cli/commands/config/unset.js +35 -0
  21. package/dist/cli/commands/daemon/index.d.ts +13 -0
  22. package/dist/cli/commands/daemon/index.js +65 -0
  23. package/dist/cli/commands/daemon/logs.d.ts +13 -0
  24. package/dist/cli/commands/daemon/logs.js +78 -0
  25. package/dist/cli/commands/daemon/restart.d.ts +13 -0
  26. package/dist/cli/commands/daemon/restart.js +177 -0
  27. package/dist/cli/commands/daemon/start.d.ts +13 -0
  28. package/dist/cli/commands/daemon/start.js +193 -0
  29. package/dist/cli/commands/daemon/status.d.ts +13 -0
  30. package/dist/cli/commands/daemon/status.js +93 -0
  31. package/dist/cli/commands/daemon/stop.d.ts +13 -0
  32. package/dist/cli/commands/daemon/stop.js +108 -0
  33. package/dist/cli/commands/init.d.ts +44 -0
  34. package/dist/cli/commands/init.js +459 -0
  35. package/dist/cli/commands/mcp/add.d.ts +26 -0
  36. package/dist/cli/commands/mcp/add.js +162 -0
  37. package/dist/cli/commands/mcp/list.d.ts +16 -0
  38. package/dist/cli/commands/mcp/list.js +89 -0
  39. package/dist/cli/commands/mcp/remove.d.ts +17 -0
  40. package/dist/cli/commands/mcp/remove.js +86 -0
  41. package/dist/cli/commands/mcp/show.d.ts +14 -0
  42. package/dist/cli/commands/mcp/show.js +131 -0
  43. package/dist/cli/commands/repo/add.d.ts +16 -0
  44. package/dist/cli/commands/repo/add.js +105 -0
  45. package/dist/cli/commands/repo/list.d.ts +17 -0
  46. package/dist/cli/commands/repo/list.js +99 -0
  47. package/dist/cli/commands/repo/rm.d.ts +17 -0
  48. package/dist/cli/commands/repo/rm.js +126 -0
  49. package/dist/cli/commands/repo/worktree/add.d.ts +21 -0
  50. package/dist/cli/commands/repo/worktree/add.js +145 -0
  51. package/dist/cli/commands/repo/worktree/list.d.ts +21 -0
  52. package/dist/cli/commands/repo/worktree/list.js +136 -0
  53. package/dist/cli/commands/session/list.d.ts +30 -0
  54. package/dist/cli/commands/session/list.js +204 -0
  55. package/dist/cli/commands/session/load-claude.d.ts +16 -0
  56. package/dist/cli/commands/session/load-claude.js +211 -0
  57. package/dist/cli/commands/user/create-admin.d.ts +13 -0
  58. package/dist/cli/commands/user/create-admin.js +65 -0
  59. package/dist/cli/commands/user/create.d.ts +16 -0
  60. package/dist/cli/commands/user/create.js +126 -0
  61. package/dist/cli/commands/user/delete.d.ts +16 -0
  62. package/dist/cli/commands/user/delete.js +77 -0
  63. package/dist/cli/commands/user/list.d.ts +13 -0
  64. package/dist/cli/commands/user/list.js +78 -0
  65. package/dist/cli/commands/user/update.d.ts +19 -0
  66. package/dist/cli/commands/user/update.js +149 -0
  67. package/dist/cli/hooks/command-not-found.d.ts +9 -0
  68. package/dist/cli/hooks/command-not-found.js +14 -0
  69. package/dist/cli/lib/banner.d.ts +25 -0
  70. package/dist/cli/lib/banner.js +25 -0
  71. package/dist/cli/lib/context.d.ts +27 -0
  72. package/dist/cli/lib/context.js +32 -0
  73. package/dist/cli/lib/daemon-manager.d.ts +48 -0
  74. package/dist/cli/lib/daemon-manager.js +109 -0
  75. package/dist/cli/lib/help.d.ts +13 -0
  76. package/dist/cli/lib/help.js +46 -0
  77. package/dist/core/agentic-tool-B_gFNpk5.d.ts +33 -0
  78. package/dist/core/agentic-tool-DsyX8diw.d.cts +33 -0
  79. package/dist/core/api/index.cjs +98 -0
  80. package/dist/core/api/index.d.cts +174 -0
  81. package/dist/core/api/index.d.ts +174 -0
  82. package/dist/core/api/index.js +62 -0
  83. package/dist/core/board-comment-BUm0fpmD.d.cts +134 -0
  84. package/dist/core/board-comment-gC_-twPx.d.ts +134 -0
  85. package/dist/core/claude/index.cjs +673 -0
  86. package/dist/core/claude/index.d.cts +124 -0
  87. package/dist/core/claude/index.d.ts +124 -0
  88. package/dist/core/claude/index.js +629 -0
  89. package/dist/core/config/browser.cjs +165 -0
  90. package/dist/core/config/browser.d.cts +289 -0
  91. package/dist/core/config/browser.d.ts +289 -0
  92. package/dist/core/config/browser.js +131 -0
  93. package/dist/core/config/index.cjs +518 -0
  94. package/dist/core/config/index.d.cts +246 -0
  95. package/dist/core/config/index.d.ts +246 -0
  96. package/dist/core/config/index.js +451 -0
  97. package/dist/core/db/index.cjs +3726 -0
  98. package/dist/core/db/index.d.cts +631 -0
  99. package/dist/core/db/index.d.ts +631 -0
  100. package/dist/core/db/index.js +3649 -0
  101. package/dist/core/dist/agentic-tool-B_gFNpk5.d.ts +33 -0
  102. package/dist/core/dist/agentic-tool-DsyX8diw.d.cts +33 -0
  103. package/dist/core/dist/api/index.cjs +98 -0
  104. package/dist/core/dist/api/index.d.cts +174 -0
  105. package/dist/core/dist/api/index.d.ts +174 -0
  106. package/dist/core/dist/api/index.js +62 -0
  107. package/dist/core/dist/board-comment-BUm0fpmD.d.cts +134 -0
  108. package/dist/core/dist/board-comment-gC_-twPx.d.ts +134 -0
  109. package/dist/core/dist/claude/index.cjs +673 -0
  110. package/dist/core/dist/claude/index.d.cts +124 -0
  111. package/dist/core/dist/claude/index.d.ts +124 -0
  112. package/dist/core/dist/claude/index.js +629 -0
  113. package/dist/core/dist/config/browser.cjs +165 -0
  114. package/dist/core/dist/config/browser.d.cts +289 -0
  115. package/dist/core/dist/config/browser.d.ts +289 -0
  116. package/dist/core/dist/config/browser.js +131 -0
  117. package/dist/core/dist/config/index.cjs +518 -0
  118. package/dist/core/dist/config/index.d.cts +246 -0
  119. package/dist/core/dist/config/index.d.ts +246 -0
  120. package/dist/core/dist/config/index.js +451 -0
  121. package/dist/core/dist/db/index.cjs +3726 -0
  122. package/dist/core/dist/db/index.d.cts +631 -0
  123. package/dist/core/dist/db/index.d.ts +631 -0
  124. package/dist/core/dist/db/index.js +3649 -0
  125. package/dist/core/dist/environment/variable-resolver.cjs +92 -0
  126. package/dist/core/dist/environment/variable-resolver.d.cts +52 -0
  127. package/dist/core/dist/environment/variable-resolver.d.ts +52 -0
  128. package/dist/core/dist/environment/variable-resolver.js +53 -0
  129. package/dist/core/dist/feathers/index.cjs +66 -0
  130. package/dist/core/dist/feathers/index.d.cts +7 -0
  131. package/dist/core/dist/feathers/index.d.ts +7 -0
  132. package/dist/core/dist/feathers/index.js +25 -0
  133. package/dist/core/dist/feathers-BzHEPnpl.d.cts +228 -0
  134. package/dist/core/dist/feathers-BzHEPnpl.d.ts +228 -0
  135. package/dist/core/dist/git/index.cjs +302 -0
  136. package/dist/core/dist/git/index.d.cts +137 -0
  137. package/dist/core/dist/git/index.d.ts +137 -0
  138. package/dist/core/dist/git/index.js +260 -0
  139. package/dist/core/dist/id-DMqyogFB.d.cts +131 -0
  140. package/dist/core/dist/id-DMqyogFB.d.ts +131 -0
  141. package/dist/core/dist/index.cjs +4653 -0
  142. package/dist/core/dist/index.d.cts +23 -0
  143. package/dist/core/dist/index.d.ts +23 -0
  144. package/dist/core/dist/index.js +4509 -0
  145. package/dist/core/dist/message-BoxZISHg.d.cts +120 -0
  146. package/dist/core/dist/message-DvBzHu7V.d.ts +120 -0
  147. package/dist/core/dist/permissions/index.cjs +112 -0
  148. package/dist/core/dist/permissions/index.d.cts +81 -0
  149. package/dist/core/dist/permissions/index.d.ts +81 -0
  150. package/dist/core/dist/permissions/index.js +85 -0
  151. package/dist/core/dist/repo-3CUrCRbq.d.cts +405 -0
  152. package/dist/core/dist/repo-CnvJ0B6-.d.ts +405 -0
  153. package/dist/core/dist/session-BPjJlVdZ.d.cts +429 -0
  154. package/dist/core/dist/session-wAzjHatv.d.ts +429 -0
  155. package/dist/core/dist/task-BIEgT1DK.d.cts +163 -0
  156. package/dist/core/dist/task-DuIfiUbW.d.ts +163 -0
  157. package/dist/core/dist/templates/handlebars-helpers.cjs +156 -0
  158. package/dist/core/dist/templates/handlebars-helpers.d.cts +45 -0
  159. package/dist/core/dist/templates/handlebars-helpers.d.ts +45 -0
  160. package/dist/core/dist/templates/handlebars-helpers.js +119 -0
  161. package/dist/core/dist/tools/claude/models.cjs +70 -0
  162. package/dist/core/dist/tools/claude/models.d.cts +27 -0
  163. package/dist/core/dist/tools/claude/models.d.ts +27 -0
  164. package/dist/core/dist/tools/claude/models.js +44 -0
  165. package/dist/core/dist/tools/index.cjs +3367 -0
  166. package/dist/core/dist/tools/index.d.cts +967 -0
  167. package/dist/core/dist/tools/index.d.ts +967 -0
  168. package/dist/core/dist/tools/index.js +3314 -0
  169. package/dist/core/dist/tools/models.cjs +119 -0
  170. package/dist/core/dist/tools/models.d.cts +47 -0
  171. package/dist/core/dist/tools/models.d.ts +47 -0
  172. package/dist/core/dist/tools/models.js +86 -0
  173. package/dist/core/dist/types/index.cjs +152 -0
  174. package/dist/core/dist/types/index.d.cts +214 -0
  175. package/dist/core/dist/types/index.d.ts +214 -0
  176. package/dist/core/dist/types/index.js +112 -0
  177. package/dist/core/dist/user-BmL3kFol.d.ts +50 -0
  178. package/dist/core/dist/user-eUuKj7yM.d.cts +50 -0
  179. package/dist/core/dist/utils/pricing.cjs +102 -0
  180. package/dist/core/dist/utils/pricing.d.cts +43 -0
  181. package/dist/core/dist/utils/pricing.d.ts +43 -0
  182. package/dist/core/dist/utils/pricing.js +75 -0
  183. package/dist/core/dist/worktrees-BzIxB1U6.d.cts +2745 -0
  184. package/dist/core/dist/worktrees-CYem1ya2.d.ts +2745 -0
  185. package/dist/core/environment/variable-resolver.cjs +92 -0
  186. package/dist/core/environment/variable-resolver.d.cts +52 -0
  187. package/dist/core/environment/variable-resolver.d.ts +52 -0
  188. package/dist/core/environment/variable-resolver.js +53 -0
  189. package/dist/core/feathers/index.cjs +66 -0
  190. package/dist/core/feathers/index.d.cts +7 -0
  191. package/dist/core/feathers/index.d.ts +7 -0
  192. package/dist/core/feathers/index.js +25 -0
  193. package/dist/core/feathers-BzHEPnpl.d.cts +228 -0
  194. package/dist/core/feathers-BzHEPnpl.d.ts +228 -0
  195. package/dist/core/git/index.cjs +302 -0
  196. package/dist/core/git/index.d.cts +137 -0
  197. package/dist/core/git/index.d.ts +137 -0
  198. package/dist/core/git/index.js +260 -0
  199. package/dist/core/id-DMqyogFB.d.cts +131 -0
  200. package/dist/core/id-DMqyogFB.d.ts +131 -0
  201. package/dist/core/index.cjs +4653 -0
  202. package/dist/core/index.d.cts +23 -0
  203. package/dist/core/index.d.ts +23 -0
  204. package/dist/core/index.js +4509 -0
  205. package/dist/core/message-BoxZISHg.d.cts +120 -0
  206. package/dist/core/message-DvBzHu7V.d.ts +120 -0
  207. package/dist/core/package.json +133 -0
  208. package/dist/core/permissions/index.cjs +112 -0
  209. package/dist/core/permissions/index.d.cts +81 -0
  210. package/dist/core/permissions/index.d.ts +81 -0
  211. package/dist/core/permissions/index.js +85 -0
  212. package/dist/core/repo-3CUrCRbq.d.cts +405 -0
  213. package/dist/core/repo-CnvJ0B6-.d.ts +405 -0
  214. package/dist/core/session-BPjJlVdZ.d.cts +429 -0
  215. package/dist/core/session-wAzjHatv.d.ts +429 -0
  216. package/dist/core/task-BIEgT1DK.d.cts +163 -0
  217. package/dist/core/task-DuIfiUbW.d.ts +163 -0
  218. package/dist/core/templates/handlebars-helpers.cjs +156 -0
  219. package/dist/core/templates/handlebars-helpers.d.cts +45 -0
  220. package/dist/core/templates/handlebars-helpers.d.ts +45 -0
  221. package/dist/core/templates/handlebars-helpers.js +119 -0
  222. package/dist/core/tools/claude/models.cjs +70 -0
  223. package/dist/core/tools/claude/models.d.cts +27 -0
  224. package/dist/core/tools/claude/models.d.ts +27 -0
  225. package/dist/core/tools/claude/models.js +44 -0
  226. package/dist/core/tools/index.cjs +3367 -0
  227. package/dist/core/tools/index.d.cts +967 -0
  228. package/dist/core/tools/index.d.ts +967 -0
  229. package/dist/core/tools/index.js +3314 -0
  230. package/dist/core/tools/models.cjs +119 -0
  231. package/dist/core/tools/models.d.cts +47 -0
  232. package/dist/core/tools/models.d.ts +47 -0
  233. package/dist/core/tools/models.js +86 -0
  234. package/dist/core/types/index.cjs +152 -0
  235. package/dist/core/types/index.d.cts +214 -0
  236. package/dist/core/types/index.d.ts +214 -0
  237. package/dist/core/types/index.js +112 -0
  238. package/dist/core/user-BmL3kFol.d.ts +50 -0
  239. package/dist/core/user-eUuKj7yM.d.cts +50 -0
  240. package/dist/core/utils/pricing.cjs +102 -0
  241. package/dist/core/utils/pricing.d.cts +43 -0
  242. package/dist/core/utils/pricing.d.ts +43 -0
  243. package/dist/core/utils/pricing.js +75 -0
  244. package/dist/core/worktrees-BzIxB1U6.d.cts +2745 -0
  245. package/dist/core/worktrees-CYem1ya2.d.ts +2745 -0
  246. package/dist/daemon/adapters/drizzle.d.ts +114 -0
  247. package/dist/daemon/adapters/drizzle.js +219 -0
  248. package/dist/daemon/declarations.d.ts +101 -0
  249. package/dist/daemon/declarations.js +0 -0
  250. package/dist/daemon/index.d.ts +2 -0
  251. package/dist/daemon/index.js +4093 -0
  252. package/dist/daemon/mcp/routes.d.ts +15 -0
  253. package/dist/daemon/mcp/routes.js +641 -0
  254. package/dist/daemon/mcp/tokens.d.ts +50 -0
  255. package/dist/daemon/mcp/tokens.js +85 -0
  256. package/dist/daemon/services/board-comments.d.ts +97 -0
  257. package/dist/daemon/services/board-comments.js +326 -0
  258. package/dist/daemon/services/board-objects.d.ts +71 -0
  259. package/dist/daemon/services/board-objects.js +117 -0
  260. package/dist/daemon/services/boards.d.ts +64 -0
  261. package/dist/daemon/services/boards.js +286 -0
  262. package/dist/daemon/services/config.d.ts +35 -0
  263. package/dist/daemon/services/config.js +68 -0
  264. package/dist/daemon/services/context.d.ts +55 -0
  265. package/dist/daemon/services/context.js +113 -0
  266. package/dist/daemon/services/health-monitor.d.ts +58 -0
  267. package/dist/daemon/services/health-monitor.js +158 -0
  268. package/dist/daemon/services/mcp-servers.d.ts +42 -0
  269. package/dist/daemon/services/mcp-servers.js +275 -0
  270. package/dist/daemon/services/messages.d.ts +49 -0
  271. package/dist/daemon/services/messages.js +269 -0
  272. package/dist/daemon/services/repos.d.ts +61 -0
  273. package/dist/daemon/services/repos.js +350 -0
  274. package/dist/daemon/services/session-mcp-servers.d.ts +56 -0
  275. package/dist/daemon/services/session-mcp-servers.js +51 -0
  276. package/dist/daemon/services/sessions.d.ts +64 -0
  277. package/dist/daemon/services/sessions.js +398 -0
  278. package/dist/daemon/services/tasks.d.ts +55 -0
  279. package/dist/daemon/services/tasks.js +318 -0
  280. package/dist/daemon/services/terminals.d.ts +75 -0
  281. package/dist/daemon/services/terminals.js +110 -0
  282. package/dist/daemon/services/users.d.ts +98 -0
  283. package/dist/daemon/services/users.js +177 -0
  284. package/dist/daemon/services/worktrees.d.ts +98 -0
  285. package/dist/daemon/services/worktrees.js +719 -0
  286. package/dist/daemon/strategies/anonymous.d.ts +20 -0
  287. package/dist/daemon/strategies/anonymous.js +32 -0
  288. package/dist/ui/assets/cc-CYmbalCD.png +0 -0
  289. package/dist/ui/assets/codex-4sLD1mVS.png +0 -0
  290. package/dist/ui/assets/cursor-BUy5pFVL.png +0 -0
  291. package/dist/ui/assets/gemini-ajOb7iAl.png +0 -0
  292. package/dist/ui/assets/index-Dc4ELxry.css +32 -0
  293. package/dist/ui/assets/index-KfIu8v4V.js +578 -0
  294. package/dist/ui/favicon.png +0 -0
  295. package/dist/ui/index.html +26 -0
  296. package/dist/ui/vite.svg +1 -0
  297. package/package.json +90 -0
@@ -0,0 +1,3726 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc2) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/lib/ids.ts
34
+ var ids_exports = {};
35
+ __export(ids_exports, {
36
+ IdResolutionError: () => IdResolutionError,
37
+ default: () => ids_default,
38
+ expandPrefix: () => expandPrefix,
39
+ findMinimumPrefixLength: () => findMinimumPrefixLength,
40
+ formatIdForDisplay: () => formatIdForDisplay,
41
+ formatShortId: () => formatShortId,
42
+ generateId: () => generateId,
43
+ isUniquePrefix: () => isUniquePrefix,
44
+ isValidShortID: () => isValidShortID,
45
+ isValidUUID: () => isValidUUID,
46
+ resolveShortId: () => resolveShortId,
47
+ shortId: () => shortId
48
+ });
49
+ function generateId() {
50
+ return (0, import_uuidv7.uuidv7)();
51
+ }
52
+ function isValidUUID(value) {
53
+ const uuidv7Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
54
+ return uuidv7Pattern.test(value);
55
+ }
56
+ function isValidShortID(value) {
57
+ return /^[0-9a-f]{8,32}$/i.test(value);
58
+ }
59
+ function shortId(uuid, length = 8) {
60
+ const cleanUuid = uuid.replace(/-/g, "");
61
+ return cleanUuid.slice(0, Math.min(length, 32));
62
+ }
63
+ function formatShortId(uuid, length = 8) {
64
+ return shortId(uuid, length);
65
+ }
66
+ function formatIdForDisplay(uuid, options = {}) {
67
+ if (options.verbose) {
68
+ return uuid;
69
+ }
70
+ return shortId(uuid, options.length);
71
+ }
72
+ function expandPrefix(prefix) {
73
+ const clean = prefix.replace(/-/g, "").toLowerCase();
74
+ if (clean.length === 0) {
75
+ throw new Error("ID prefix cannot be empty");
76
+ }
77
+ if (!isValidShortID(clean)) {
78
+ throw new Error(`Invalid ID prefix: ${prefix} (must be hexadecimal)`);
79
+ }
80
+ if (clean.length === 32) {
81
+ return formatUUIDWithHyphens(clean);
82
+ }
83
+ let formatted = "";
84
+ let pos = 0;
85
+ const sections = [8, 4, 4, 4, 12];
86
+ let offset = 0;
87
+ for (const sectionLength of sections) {
88
+ if (pos >= clean.length) break;
89
+ const section = clean.slice(pos, pos + sectionLength);
90
+ formatted += (offset > 0 ? "-" : "") + section;
91
+ pos += section.length;
92
+ if (section.length < sectionLength) {
93
+ return `${formatted}%`;
94
+ }
95
+ offset++;
96
+ }
97
+ return `${formatted}%`;
98
+ }
99
+ function formatUUIDWithHyphens(hex) {
100
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
101
+ }
102
+ function resolveShortId(prefix, entities) {
103
+ const cleanPrefix = prefix.replace(/-/g, "").toLowerCase();
104
+ const matches = entities.filter((e) => {
105
+ const cleanId = e.id.replace(/-/g, "").toLowerCase();
106
+ return cleanId.startsWith(cleanPrefix);
107
+ });
108
+ if (matches.length === 0) {
109
+ throw new IdResolutionError(
110
+ `No entity found with ID prefix: ${prefix}
111
+
112
+ Use 'agor <entity> list' to see available IDs.`,
113
+ "not_found",
114
+ prefix
115
+ );
116
+ }
117
+ if (matches.length === 1) {
118
+ return matches[0];
119
+ }
120
+ const suggestions = matches.slice(0, 10).map((m) => {
121
+ const description = getEntityDescription(m);
122
+ return ` - ${shortId(m.id, 12)}: ${description}`;
123
+ }).join("\n");
124
+ const ellipsis = matches.length > 10 ? `
125
+ ... and ${matches.length - 10} more` : "";
126
+ throw new IdResolutionError(
127
+ `Ambiguous ID prefix: ${prefix}
128
+
129
+ ${matches.length} matches found:
130
+ ${suggestions}${ellipsis}
131
+
132
+ Use a longer prefix to disambiguate.`,
133
+ "ambiguous",
134
+ prefix,
135
+ matches.map((m) => ({ id: m.id }))
136
+ );
137
+ }
138
+ function getEntityDescription(entity) {
139
+ if (entity.description) return entity.description;
140
+ if (entity.full_prompt) return truncate(entity.full_prompt, 60);
141
+ if (entity.name) return entity.name;
142
+ if (entity.agent) return `(${entity.agent} session)`;
143
+ return "(no description)";
144
+ }
145
+ function truncate(str, maxLength) {
146
+ if (str.length <= maxLength) return str;
147
+ return `${str.slice(0, maxLength - 3)}...`;
148
+ }
149
+ function findMinimumPrefixLength(ids) {
150
+ if (ids.length <= 1) return 8;
151
+ for (let length = 8; length <= 32; length++) {
152
+ const prefixes = new Set(ids.map((id) => shortId(id, length)));
153
+ if (prefixes.size === ids.length) {
154
+ return length;
155
+ }
156
+ }
157
+ return 32;
158
+ }
159
+ function isUniquePrefix(prefix, entities) {
160
+ try {
161
+ resolveShortId(prefix, entities);
162
+ return true;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+ var import_uuidv7, IdResolutionError, ids_default;
168
+ var init_ids = __esm({
169
+ "src/lib/ids.ts"() {
170
+ "use strict";
171
+ import_uuidv7 = require("uuidv7");
172
+ IdResolutionError = class extends Error {
173
+ constructor(message, type, prefix, candidates) {
174
+ super(message);
175
+ this.type = type;
176
+ this.prefix = prefix;
177
+ this.candidates = candidates;
178
+ this.name = "IdResolutionError";
179
+ }
180
+ };
181
+ ids_default = {
182
+ generateId,
183
+ isValidUUID,
184
+ isValidShortID,
185
+ shortId,
186
+ formatShortId,
187
+ formatIdForDisplay,
188
+ expandPrefix,
189
+ resolveShortId,
190
+ findMinimumPrefixLength,
191
+ isUniquePrefix
192
+ };
193
+ }
194
+ });
195
+
196
+ // src/db/index.ts
197
+ var db_exports = {};
198
+ __export(db_exports, {
199
+ AmbiguousIdError: () => AmbiguousIdError,
200
+ BoardCommentsRepository: () => BoardCommentsRepository,
201
+ BoardObjectRepository: () => BoardObjectRepository,
202
+ BoardRepository: () => BoardRepository,
203
+ DEFAULT_ADMIN_USER: () => DEFAULT_ADMIN_USER,
204
+ DEFAULT_DB_PATH: () => DEFAULT_DB_PATH,
205
+ DatabaseConnectionError: () => DatabaseConnectionError,
206
+ EntityNotFoundError: () => EntityNotFoundError,
207
+ IdResolutionError: () => IdResolutionError,
208
+ MCPServerRepository: () => MCPServerRepository,
209
+ MessagesRepository: () => MessagesRepository,
210
+ MigrationError: () => MigrationError,
211
+ RepoRepository: () => RepoRepository,
212
+ RepositoryError: () => RepositoryError,
213
+ SessionMCPServerRepository: () => SessionMCPServerRepository,
214
+ SessionRepository: () => SessionRepository,
215
+ TaskRepository: () => TaskRepository,
216
+ WorktreeRepository: () => WorktreeRepository,
217
+ and: () => import_drizzle_orm14.and,
218
+ boardComments: () => boardComments,
219
+ boardObjects: () => boardObjects,
220
+ boards: () => boards,
221
+ compare: () => compare,
222
+ createDatabase: () => createDatabase,
223
+ createDefaultAdminUser: () => createDefaultAdminUser,
224
+ createLocalDatabase: () => createLocalDatabase,
225
+ createUser: () => createUser,
226
+ desc: () => import_drizzle_orm14.desc,
227
+ eq: () => import_drizzle_orm14.eq,
228
+ formatShortId: () => formatShortId,
229
+ generateId: () => generateId,
230
+ getUserByEmail: () => getUserByEmail,
231
+ hash: () => hash,
232
+ inArray: () => import_drizzle_orm14.inArray,
233
+ initializeDatabase: () => initializeDatabase,
234
+ like: () => import_drizzle_orm14.like,
235
+ mcpServers: () => mcpServers,
236
+ messages: () => messages,
237
+ or: () => import_drizzle_orm14.or,
238
+ repos: () => repos,
239
+ resolveShortId: () => resolveShortId,
240
+ runMigrations: () => runMigrations,
241
+ seedInitialData: () => seedInitialData,
242
+ sessionMcpServers: () => sessionMcpServers,
243
+ sessions: () => sessions,
244
+ sql: () => import_drizzle_orm14.sql,
245
+ tasks: () => tasks,
246
+ userExists: () => userExists,
247
+ users: () => users,
248
+ worktrees: () => worktrees
249
+ });
250
+ module.exports = __toCommonJS(db_exports);
251
+ var import_drizzle_orm14 = require("drizzle-orm");
252
+ var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
253
+ init_ids();
254
+
255
+ // src/db/client.ts
256
+ var import_client = require("@libsql/client");
257
+ var import_libsql = require("drizzle-orm/libsql");
258
+
259
+ // src/db/schema.ts
260
+ var schema_exports = {};
261
+ __export(schema_exports, {
262
+ boardComments: () => boardComments,
263
+ boardObjects: () => boardObjects,
264
+ boards: () => boards,
265
+ mcpServers: () => mcpServers,
266
+ messages: () => messages,
267
+ repos: () => repos,
268
+ sessionMcpServers: () => sessionMcpServers,
269
+ sessions: () => sessions,
270
+ tasks: () => tasks,
271
+ users: () => users,
272
+ worktrees: () => worktrees
273
+ });
274
+ var import_drizzle_orm = require("drizzle-orm");
275
+ var import_sqlite_core = require("drizzle-orm/sqlite-core");
276
+ var sessions = (0, import_sqlite_core.sqliteTable)(
277
+ "sessions",
278
+ {
279
+ // Primary identity
280
+ session_id: (0, import_sqlite_core.text)("session_id", { length: 36 }).primaryKey(),
281
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
282
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
283
+ // User attribution
284
+ created_by: (0, import_sqlite_core.text)("created_by", { length: 36 }).notNull().default("anonymous"),
285
+ // Materialized for filtering/joins (cross-DB compatible)
286
+ status: (0, import_sqlite_core.text)("status", {
287
+ enum: ["idle", "running", "completed", "failed"]
288
+ }).notNull(),
289
+ agentic_tool: (0, import_sqlite_core.text)("agentic_tool", {
290
+ enum: ["claude-code", "cursor", "codex", "gemini"]
291
+ }).notNull(),
292
+ board_id: (0, import_sqlite_core.text)("board_id", { length: 36 }),
293
+ // NULL = no board
294
+ // Genealogy (materialized for tree queries)
295
+ parent_session_id: (0, import_sqlite_core.text)("parent_session_id", { length: 36 }),
296
+ forked_from_session_id: (0, import_sqlite_core.text)("forked_from_session_id", { length: 36 }),
297
+ // Worktree reference (REQUIRED: all sessions must have a worktree)
298
+ worktree_id: (0, import_sqlite_core.text)("worktree_id", { length: 36 }).notNull().references(() => worktrees.worktree_id, {
299
+ onDelete: "cascade"
300
+ // Cascade delete sessions when worktree is deleted
301
+ }),
302
+ // JSON blob for everything else (cross-DB via json() type)
303
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
304
+ },
305
+ (table) => ({
306
+ statusIdx: (0, import_sqlite_core.index)("sessions_status_idx").on(table.status),
307
+ agenticToolIdx: (0, import_sqlite_core.index)("sessions_agentic_tool_idx").on(table.agentic_tool),
308
+ boardIdx: (0, import_sqlite_core.index)("sessions_board_idx").on(table.board_id),
309
+ worktreeIdx: (0, import_sqlite_core.index)("sessions_worktree_idx").on(table.worktree_id),
310
+ createdIdx: (0, import_sqlite_core.index)("sessions_created_idx").on(table.created_at),
311
+ parentIdx: (0, import_sqlite_core.index)("sessions_parent_idx").on(table.parent_session_id),
312
+ forkedIdx: (0, import_sqlite_core.index)("sessions_forked_idx").on(table.forked_from_session_id)
313
+ })
314
+ );
315
+ var tasks = (0, import_sqlite_core.sqliteTable)(
316
+ "tasks",
317
+ {
318
+ task_id: (0, import_sqlite_core.text)("task_id", { length: 36 }).primaryKey(),
319
+ session_id: (0, import_sqlite_core.text)("session_id", { length: 36 }).notNull().references(() => sessions.session_id, { onDelete: "cascade" }),
320
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
321
+ completed_at: (0, import_sqlite_core.integer)("completed_at", { mode: "timestamp_ms" }),
322
+ status: (0, import_sqlite_core.text)("status", {
323
+ enum: [
324
+ "created",
325
+ "running",
326
+ "stopping",
327
+ "awaiting_permission",
328
+ "completed",
329
+ "failed",
330
+ "stopped"
331
+ ]
332
+ }).notNull(),
333
+ // User attribution
334
+ created_by: (0, import_sqlite_core.text)("created_by", { length: 36 }).notNull().default("anonymous"),
335
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
336
+ },
337
+ (table) => ({
338
+ sessionIdx: (0, import_sqlite_core.index)("tasks_session_idx").on(table.session_id),
339
+ statusIdx: (0, import_sqlite_core.index)("tasks_status_idx").on(table.status),
340
+ createdIdx: (0, import_sqlite_core.index)("tasks_created_idx").on(table.created_at)
341
+ })
342
+ );
343
+ var messages = (0, import_sqlite_core.sqliteTable)(
344
+ "messages",
345
+ {
346
+ // Primary identity
347
+ message_id: (0, import_sqlite_core.text)("message_id", { length: 36 }).primaryKey(),
348
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
349
+ // Foreign keys (materialized for indexes)
350
+ session_id: (0, import_sqlite_core.text)("session_id", { length: 36 }).notNull().references(() => sessions.session_id, { onDelete: "cascade" }),
351
+ task_id: (0, import_sqlite_core.text)("task_id", { length: 36 }).references(() => tasks.task_id, {
352
+ onDelete: "set null"
353
+ }),
354
+ // Materialized for queries
355
+ type: (0, import_sqlite_core.text)("type", {
356
+ enum: ["user", "assistant", "system", "file-history-snapshot", "permission_request"]
357
+ }).notNull(),
358
+ role: (0, import_sqlite_core.text)("role", {
359
+ enum: ["user", "assistant", "system"]
360
+ }).notNull(),
361
+ index: (0, import_sqlite_core.integer)("index").notNull(),
362
+ // Position in conversation (0-based)
363
+ timestamp: (0, import_sqlite_core.integer)("timestamp", { mode: "timestamp_ms" }).notNull(),
364
+ content_preview: (0, import_sqlite_core.text)("content_preview"),
365
+ // First 200 chars for list views
366
+ // Full data (JSON blob)
367
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
368
+ },
369
+ (table) => ({
370
+ // Indexes for efficient lookups
371
+ sessionIdx: (0, import_sqlite_core.index)("messages_session_id_idx").on(table.session_id),
372
+ taskIdx: (0, import_sqlite_core.index)("messages_task_id_idx").on(table.task_id),
373
+ sessionIndexIdx: (0, import_sqlite_core.index)("messages_session_index_idx").on(table.session_id, table.index)
374
+ })
375
+ );
376
+ var boards = (0, import_sqlite_core.sqliteTable)(
377
+ "boards",
378
+ {
379
+ board_id: (0, import_sqlite_core.text)("board_id", { length: 36 }).primaryKey(),
380
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
381
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
382
+ // User attribution
383
+ created_by: (0, import_sqlite_core.text)("created_by", { length: 36 }).notNull().default("anonymous"),
384
+ // Materialized for lookups
385
+ name: (0, import_sqlite_core.text)("name").notNull(),
386
+ slug: (0, import_sqlite_core.text)("slug").unique(),
387
+ // JSON blob for the rest
388
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
389
+ },
390
+ (table) => ({
391
+ nameIdx: (0, import_sqlite_core.index)("boards_name_idx").on(table.name),
392
+ slugIdx: (0, import_sqlite_core.index)("boards_slug_idx").on(table.slug)
393
+ })
394
+ );
395
+ var repos = (0, import_sqlite_core.sqliteTable)(
396
+ "repos",
397
+ {
398
+ repo_id: (0, import_sqlite_core.text)("repo_id", { length: 36 }).primaryKey(),
399
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
400
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
401
+ // Materialized for querying
402
+ slug: (0, import_sqlite_core.text)("slug").notNull().unique(),
403
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
404
+ },
405
+ (table) => ({
406
+ slugIdx: (0, import_sqlite_core.index)("repos_slug_idx").on(table.slug)
407
+ })
408
+ );
409
+ var worktrees = (0, import_sqlite_core.sqliteTable)(
410
+ "worktrees",
411
+ {
412
+ // Primary identity
413
+ worktree_id: (0, import_sqlite_core.text)("worktree_id", { length: 36 }).primaryKey(),
414
+ repo_id: (0, import_sqlite_core.text)("repo_id", { length: 36 }).notNull().references(() => repos.repo_id, { onDelete: "cascade" }),
415
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
416
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
417
+ // User attribution
418
+ created_by: (0, import_sqlite_core.text)("created_by", { length: 36 }).notNull().default("anonymous"),
419
+ // Materialized for queries
420
+ name: (0, import_sqlite_core.text)("name").notNull(),
421
+ // "feat-auth", "main"
422
+ ref: (0, import_sqlite_core.text)("ref").notNull(),
423
+ // Current branch/tag/commit
424
+ worktree_unique_id: (0, import_sqlite_core.integer)("worktree_unique_id").notNull(),
425
+ // Auto-assigned sequential ID for templates
426
+ // Board relationship (nullable - worktrees can exist without boards)
427
+ board_id: (0, import_sqlite_core.text)("board_id", { length: 36 }).references(() => boards.board_id, {
428
+ onDelete: "set null"
429
+ // If board is deleted, worktree remains but loses board association
430
+ }),
431
+ // JSON blob for everything else
432
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
433
+ },
434
+ (table) => ({
435
+ repoIdx: (0, import_sqlite_core.index)("worktrees_repo_idx").on(table.repo_id),
436
+ nameIdx: (0, import_sqlite_core.index)("worktrees_name_idx").on(table.name),
437
+ refIdx: (0, import_sqlite_core.index)("worktrees_ref_idx").on(table.ref),
438
+ boardIdx: (0, import_sqlite_core.index)("worktrees_board_idx").on(table.board_id),
439
+ createdIdx: (0, import_sqlite_core.index)("worktrees_created_idx").on(table.created_at),
440
+ updatedIdx: (0, import_sqlite_core.index)("worktrees_updated_idx").on(table.updated_at),
441
+ // Composite unique constraint (repo + name)
442
+ uniqueRepoName: (0, import_sqlite_core.index)("worktrees_repo_name_unique").on(table.repo_id, table.name)
443
+ })
444
+ );
445
+ var users = (0, import_sqlite_core.sqliteTable)(
446
+ "users",
447
+ {
448
+ // Primary identity
449
+ user_id: (0, import_sqlite_core.text)("user_id", { length: 36 }).primaryKey(),
450
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
451
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
452
+ // Materialized for auth lookups
453
+ email: (0, import_sqlite_core.text)("email").unique().notNull(),
454
+ password: (0, import_sqlite_core.text)("password").notNull(),
455
+ // bcrypt hashed
456
+ // Basic profile (materialized for display)
457
+ name: (0, import_sqlite_core.text)("name"),
458
+ emoji: (0, import_sqlite_core.text)("emoji"),
459
+ role: (0, import_sqlite_core.text)("role", {
460
+ enum: ["owner", "admin", "member", "viewer"]
461
+ }).notNull().default("member"),
462
+ // Onboarding state
463
+ onboarding_completed: (0, import_sqlite_core.integer)("onboarding_completed", { mode: "boolean" }).notNull().default(false),
464
+ // JSON blob for profile/preferences
465
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
466
+ },
467
+ (table) => ({
468
+ emailIdx: (0, import_sqlite_core.index)("users_email_idx").on(table.email)
469
+ })
470
+ );
471
+ var mcpServers = (0, import_sqlite_core.sqliteTable)(
472
+ "mcp_servers",
473
+ {
474
+ // Primary identity
475
+ mcp_server_id: (0, import_sqlite_core.text)("mcp_server_id", { length: 36 }).primaryKey(),
476
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
477
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
478
+ // Materialized for filtering
479
+ name: (0, import_sqlite_core.text)("name").notNull(),
480
+ // e.g., "filesystem", "sentry"
481
+ transport: (0, import_sqlite_core.text)("transport", {
482
+ enum: ["stdio", "http", "sse"]
483
+ }).notNull(),
484
+ scope: (0, import_sqlite_core.text)("scope", {
485
+ enum: ["global", "team", "repo", "session"]
486
+ }).notNull(),
487
+ enabled: (0, import_sqlite_core.integer)("enabled", { mode: "boolean" }).notNull().default(true),
488
+ // Scope foreign keys (materialized for indexes)
489
+ owner_user_id: (0, import_sqlite_core.text)("owner_user_id", { length: 36 }),
490
+ team_id: (0, import_sqlite_core.text)("team_id", { length: 36 }),
491
+ repo_id: (0, import_sqlite_core.text)("repo_id", { length: 36 }).references(() => repos.repo_id, {
492
+ onDelete: "cascade"
493
+ }),
494
+ session_id: (0, import_sqlite_core.text)("session_id", { length: 36 }).references(() => sessions.session_id, {
495
+ onDelete: "cascade"
496
+ }),
497
+ // Source tracking (materialized for queries)
498
+ source: (0, import_sqlite_core.text)("source", {
499
+ enum: ["user", "imported", "agor"]
500
+ }).notNull(),
501
+ // JSON blob for configuration and capabilities
502
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
503
+ },
504
+ (table) => ({
505
+ nameIdx: (0, import_sqlite_core.index)("mcp_servers_name_idx").on(table.name),
506
+ scopeIdx: (0, import_sqlite_core.index)("mcp_servers_scope_idx").on(table.scope),
507
+ ownerIdx: (0, import_sqlite_core.index)("mcp_servers_owner_idx").on(table.owner_user_id),
508
+ teamIdx: (0, import_sqlite_core.index)("mcp_servers_team_idx").on(table.team_id),
509
+ repoIdx: (0, import_sqlite_core.index)("mcp_servers_repo_idx").on(table.repo_id),
510
+ sessionIdx: (0, import_sqlite_core.index)("mcp_servers_session_idx").on(table.session_id),
511
+ enabledIdx: (0, import_sqlite_core.index)("mcp_servers_enabled_idx").on(table.enabled)
512
+ })
513
+ );
514
+ var boardObjects = (0, import_sqlite_core.sqliteTable)(
515
+ "board_objects",
516
+ {
517
+ // Primary identity
518
+ object_id: (0, import_sqlite_core.text)("object_id", { length: 36 }).primaryKey(),
519
+ board_id: (0, import_sqlite_core.text)("board_id", { length: 36 }).notNull().references(() => boards.board_id, { onDelete: "cascade" }),
520
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
521
+ // Worktree reference
522
+ worktree_id: (0, import_sqlite_core.text)("worktree_id", { length: 36 }).notNull().references(() => worktrees.worktree_id, {
523
+ onDelete: "cascade"
524
+ }),
525
+ // Position data (JSON)
526
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
527
+ },
528
+ (table) => ({
529
+ boardIdx: (0, import_sqlite_core.index)("board_objects_board_idx").on(table.board_id),
530
+ worktreeIdx: (0, import_sqlite_core.index)("board_objects_worktree_idx").on(table.worktree_id)
531
+ })
532
+ );
533
+ var sessionMcpServers = (0, import_sqlite_core.sqliteTable)(
534
+ "session_mcp_servers",
535
+ {
536
+ session_id: (0, import_sqlite_core.text)("session_id", { length: 36 }).notNull().references(() => sessions.session_id, { onDelete: "cascade" }),
537
+ mcp_server_id: (0, import_sqlite_core.text)("mcp_server_id", { length: 36 }).notNull().references(() => mcpServers.mcp_server_id, { onDelete: "cascade" }),
538
+ enabled: (0, import_sqlite_core.integer)("enabled", { mode: "boolean" }).notNull().default(true),
539
+ added_at: (0, import_sqlite_core.integer)("added_at", { mode: "timestamp_ms" }).notNull()
540
+ },
541
+ (table) => ({
542
+ // Composite primary key
543
+ pk: (0, import_sqlite_core.index)("session_mcp_servers_pk").on(table.session_id, table.mcp_server_id),
544
+ // Indexes for queries
545
+ sessionIdx: (0, import_sqlite_core.index)("session_mcp_servers_session_idx").on(table.session_id),
546
+ serverIdx: (0, import_sqlite_core.index)("session_mcp_servers_server_idx").on(table.mcp_server_id),
547
+ enabledIdx: (0, import_sqlite_core.index)("session_mcp_servers_enabled_idx").on(table.session_id, table.enabled)
548
+ })
549
+ );
550
+ var boardComments = (0, import_sqlite_core.sqliteTable)(
551
+ "board_comments",
552
+ {
553
+ // Primary identity
554
+ comment_id: (0, import_sqlite_core.text)("comment_id", { length: 36 }).primaryKey(),
555
+ created_at: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp_ms" }).notNull(),
556
+ updated_at: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp_ms" }),
557
+ // Scoping & authorship
558
+ board_id: (0, import_sqlite_core.text)("board_id", { length: 36 }).notNull().references(() => boards.board_id, { onDelete: "cascade" }),
559
+ created_by: (0, import_sqlite_core.text)("created_by", { length: 36 }).notNull().default("anonymous"),
560
+ // FLEXIBLE ATTACHMENTS (all optional)
561
+ // Phase 1: board-level only (all NULL)
562
+ // Phase 2: object attachments (session, task, message, worktree)
563
+ // Phase 3: spatial positioning
564
+ session_id: (0, import_sqlite_core.text)("session_id", { length: 36 }).references(() => sessions.session_id, {
565
+ onDelete: "set null"
566
+ }),
567
+ task_id: (0, import_sqlite_core.text)("task_id", { length: 36 }).references(() => tasks.task_id, {
568
+ onDelete: "set null"
569
+ }),
570
+ message_id: (0, import_sqlite_core.text)("message_id", { length: 36 }).references(() => messages.message_id, {
571
+ onDelete: "set null"
572
+ }),
573
+ worktree_id: (0, import_sqlite_core.text)("worktree_id", { length: 36 }).references(() => worktrees.worktree_id, {
574
+ onDelete: "set null"
575
+ }),
576
+ // Content (materialized for display)
577
+ content: (0, import_sqlite_core.text)("content").notNull(),
578
+ // Markdown-supported text
579
+ content_preview: (0, import_sqlite_core.text)("content_preview").notNull(),
580
+ // First 200 chars
581
+ // Thread support (optional)
582
+ parent_comment_id: (0, import_sqlite_core.text)("parent_comment_id", { length: 36 }),
583
+ // Metadata (materialized for filtering)
584
+ resolved: (0, import_sqlite_core.integer)("resolved", { mode: "boolean" }).notNull().default(false),
585
+ edited: (0, import_sqlite_core.integer)("edited", { mode: "boolean" }).notNull().default(false),
586
+ // Reactions (for BOTH thread roots and replies)
587
+ // Stored as JSON array: [{ user_id: "abc", emoji: "👍" }, ...]
588
+ // Display grouped by emoji: { "👍": ["alice", "bob"], "🎉": ["charlie"] }
589
+ reactions: (0, import_sqlite_core.text)("reactions", { mode: "json" }).$type().notNull().default(import_drizzle_orm.sql`'[]'`),
590
+ // JSON blob for advanced features
591
+ data: (0, import_sqlite_core.text)("data", { mode: "json" }).$type().notNull()
592
+ },
593
+ (table) => ({
594
+ boardIdx: (0, import_sqlite_core.index)("board_comments_board_idx").on(table.board_id),
595
+ sessionIdx: (0, import_sqlite_core.index)("board_comments_session_idx").on(table.session_id),
596
+ taskIdx: (0, import_sqlite_core.index)("board_comments_task_idx").on(table.task_id),
597
+ messageIdx: (0, import_sqlite_core.index)("board_comments_message_idx").on(table.message_id),
598
+ worktreeIdx: (0, import_sqlite_core.index)("board_comments_worktree_idx").on(table.worktree_id),
599
+ createdByIdx: (0, import_sqlite_core.index)("board_comments_created_by_idx").on(table.created_by),
600
+ parentIdx: (0, import_sqlite_core.index)("board_comments_parent_idx").on(table.parent_comment_id),
601
+ createdIdx: (0, import_sqlite_core.index)("board_comments_created_idx").on(table.created_at),
602
+ resolvedIdx: (0, import_sqlite_core.index)("board_comments_resolved_idx").on(table.resolved)
603
+ })
604
+ );
605
+
606
+ // src/db/client.ts
607
+ var DatabaseConnectionError = class extends Error {
608
+ constructor(message, cause) {
609
+ super(message);
610
+ this.cause = cause;
611
+ this.name = "DatabaseConnectionError";
612
+ }
613
+ };
614
+ function expandPath(path) {
615
+ if (path.startsWith("~/")) {
616
+ const home = process.env.HOME || process.env.USERPROFILE || "";
617
+ return path.replace("~", home);
618
+ }
619
+ return path;
620
+ }
621
+ function createLibSQLClient(config) {
622
+ try {
623
+ let url = config.url;
624
+ if (url.startsWith("file:")) {
625
+ const filePath = url.slice(5);
626
+ const expandedPath = expandPath(filePath);
627
+ url = `file:${expandedPath}`;
628
+ }
629
+ const clientConfig = { url };
630
+ if (config.authToken) {
631
+ clientConfig.authToken = config.authToken;
632
+ }
633
+ if (config.syncUrl) {
634
+ clientConfig.syncUrl = config.syncUrl;
635
+ clientConfig.syncInterval = config.syncInterval ?? 60;
636
+ }
637
+ return (0, import_client.createClient)(clientConfig);
638
+ } catch (error) {
639
+ throw new DatabaseConnectionError(
640
+ `Failed to create LibSQL client: ${error instanceof Error ? error.message : String(error)}`,
641
+ error
642
+ );
643
+ }
644
+ }
645
+ function createDatabase(config) {
646
+ const client = createLibSQLClient(config);
647
+ return (0, import_libsql.drizzle)(client, { schema: schema_exports });
648
+ }
649
+ var DEFAULT_DB_PATH = "file:~/.agor/agor.db";
650
+ function createLocalDatabase(customPath) {
651
+ return createDatabase({ url: customPath ?? DEFAULT_DB_PATH });
652
+ }
653
+
654
+ // src/db/migrate.ts
655
+ var import_drizzle_orm2 = require("drizzle-orm");
656
+ var import_migrator = require("drizzle-orm/libsql/migrator");
657
+ var MigrationError = class extends Error {
658
+ constructor(message, cause) {
659
+ super(message);
660
+ this.cause = cause;
661
+ this.name = "MigrationError";
662
+ }
663
+ };
664
+ async function tablesExist(db) {
665
+ try {
666
+ const result = await db.run(import_drizzle_orm2.sql`
667
+ SELECT name FROM sqlite_master
668
+ WHERE type='table' AND name IN ('sessions', 'tasks', 'boards', 'repos', 'worktrees', 'messages', 'users', 'board_comments')
669
+ `);
670
+ return result.rows.length > 0;
671
+ } catch (error) {
672
+ throw new MigrationError(
673
+ `Failed to check if tables exist: ${error instanceof Error ? error.message : String(error)}`,
674
+ error
675
+ );
676
+ }
677
+ }
678
+ async function tableExists(db, tableName) {
679
+ try {
680
+ const result = await db.run(import_drizzle_orm2.sql`
681
+ SELECT name FROM sqlite_master
682
+ WHERE type='table' AND name = ${tableName}
683
+ `);
684
+ return result.rows.length > 0;
685
+ } catch (error) {
686
+ throw new MigrationError(
687
+ `Failed to check if table ${tableName} exists: ${error instanceof Error ? error.message : String(error)}`,
688
+ error
689
+ );
690
+ }
691
+ }
692
+ async function createInitialSchema(db) {
693
+ try {
694
+ await db.run(import_drizzle_orm2.sql`
695
+ CREATE TABLE IF NOT EXISTS sessions (
696
+ session_id TEXT PRIMARY KEY,
697
+ created_at INTEGER NOT NULL,
698
+ updated_at INTEGER,
699
+ created_by TEXT NOT NULL DEFAULT 'anonymous',
700
+ status TEXT NOT NULL CHECK(status IN ('idle', 'running', 'completed', 'failed')),
701
+ agentic_tool TEXT NOT NULL CHECK(agentic_tool IN ('claude-code', 'cursor', 'codex', 'gemini')),
702
+ board_id TEXT,
703
+ parent_session_id TEXT,
704
+ forked_from_session_id TEXT,
705
+ worktree_id TEXT NOT NULL,
706
+ data TEXT NOT NULL,
707
+ FOREIGN KEY (worktree_id) REFERENCES worktrees(worktree_id) ON DELETE CASCADE
708
+ )
709
+ `);
710
+ await db.run(import_drizzle_orm2.sql`
711
+ CREATE INDEX IF NOT EXISTS sessions_status_idx ON sessions(status)
712
+ `);
713
+ await db.run(import_drizzle_orm2.sql`
714
+ CREATE INDEX IF NOT EXISTS sessions_agentic_tool_idx ON sessions(agentic_tool)
715
+ `);
716
+ await db.run(import_drizzle_orm2.sql`
717
+ CREATE INDEX IF NOT EXISTS sessions_board_idx ON sessions(board_id)
718
+ `);
719
+ await db.run(import_drizzle_orm2.sql`
720
+ CREATE INDEX IF NOT EXISTS sessions_worktree_idx ON sessions(worktree_id)
721
+ `);
722
+ await db.run(import_drizzle_orm2.sql`
723
+ CREATE INDEX IF NOT EXISTS sessions_created_idx ON sessions(created_at)
724
+ `);
725
+ await db.run(import_drizzle_orm2.sql`
726
+ CREATE INDEX IF NOT EXISTS sessions_parent_idx ON sessions(parent_session_id)
727
+ `);
728
+ await db.run(import_drizzle_orm2.sql`
729
+ CREATE INDEX IF NOT EXISTS sessions_forked_idx ON sessions(forked_from_session_id)
730
+ `);
731
+ await db.run(import_drizzle_orm2.sql`
732
+ CREATE TABLE IF NOT EXISTS tasks (
733
+ task_id TEXT PRIMARY KEY,
734
+ session_id TEXT NOT NULL,
735
+ created_at INTEGER NOT NULL,
736
+ completed_at INTEGER,
737
+ status TEXT NOT NULL CHECK(status IN ('created', 'running', 'stopping', 'awaiting_permission', 'completed', 'failed', 'stopped')),
738
+ created_by TEXT NOT NULL DEFAULT 'anonymous',
739
+ data TEXT NOT NULL,
740
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
741
+ )
742
+ `);
743
+ await db.run(import_drizzle_orm2.sql`
744
+ CREATE INDEX IF NOT EXISTS tasks_session_idx ON tasks(session_id)
745
+ `);
746
+ await db.run(import_drizzle_orm2.sql`
747
+ CREATE INDEX IF NOT EXISTS tasks_status_idx ON tasks(status)
748
+ `);
749
+ await db.run(import_drizzle_orm2.sql`
750
+ CREATE INDEX IF NOT EXISTS tasks_created_idx ON tasks(created_at)
751
+ `);
752
+ await db.run(import_drizzle_orm2.sql`
753
+ CREATE TABLE IF NOT EXISTS boards (
754
+ board_id TEXT PRIMARY KEY,
755
+ created_at INTEGER NOT NULL,
756
+ updated_at INTEGER,
757
+ created_by TEXT NOT NULL DEFAULT 'anonymous',
758
+ name TEXT NOT NULL,
759
+ slug TEXT UNIQUE,
760
+ data TEXT NOT NULL
761
+ )
762
+ `);
763
+ await db.run(import_drizzle_orm2.sql`
764
+ CREATE INDEX IF NOT EXISTS boards_name_idx ON boards(name)
765
+ `);
766
+ await db.run(import_drizzle_orm2.sql`
767
+ CREATE INDEX IF NOT EXISTS boards_slug_idx ON boards(slug)
768
+ `);
769
+ await db.run(import_drizzle_orm2.sql`
770
+ CREATE TABLE IF NOT EXISTS repos (
771
+ repo_id TEXT PRIMARY KEY,
772
+ created_at INTEGER NOT NULL,
773
+ updated_at INTEGER,
774
+ slug TEXT NOT NULL UNIQUE,
775
+ data TEXT NOT NULL
776
+ )
777
+ `);
778
+ await db.run(import_drizzle_orm2.sql`
779
+ CREATE INDEX IF NOT EXISTS repos_slug_idx ON repos(slug)
780
+ `);
781
+ await db.run(import_drizzle_orm2.sql`
782
+ CREATE TABLE IF NOT EXISTS worktrees (
783
+ worktree_id TEXT PRIMARY KEY,
784
+ repo_id TEXT NOT NULL,
785
+ created_at INTEGER NOT NULL,
786
+ updated_at INTEGER,
787
+ created_by TEXT NOT NULL DEFAULT 'anonymous',
788
+ name TEXT NOT NULL,
789
+ ref TEXT NOT NULL,
790
+ worktree_unique_id INTEGER NOT NULL,
791
+ board_id TEXT,
792
+ data TEXT NOT NULL,
793
+ FOREIGN KEY (repo_id) REFERENCES repos(repo_id) ON DELETE CASCADE,
794
+ FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE SET NULL
795
+ )
796
+ `);
797
+ await db.run(import_drizzle_orm2.sql`
798
+ CREATE INDEX IF NOT EXISTS worktrees_repo_idx ON worktrees(repo_id)
799
+ `);
800
+ await db.run(import_drizzle_orm2.sql`
801
+ CREATE INDEX IF NOT EXISTS worktrees_name_idx ON worktrees(name)
802
+ `);
803
+ await db.run(import_drizzle_orm2.sql`
804
+ CREATE INDEX IF NOT EXISTS worktrees_ref_idx ON worktrees(ref)
805
+ `);
806
+ await db.run(import_drizzle_orm2.sql`
807
+ CREATE INDEX IF NOT EXISTS worktrees_board_idx ON worktrees(board_id)
808
+ `);
809
+ await db.run(import_drizzle_orm2.sql`
810
+ CREATE INDEX IF NOT EXISTS worktrees_created_idx ON worktrees(created_at)
811
+ `);
812
+ await db.run(import_drizzle_orm2.sql`
813
+ CREATE INDEX IF NOT EXISTS worktrees_updated_idx ON worktrees(updated_at)
814
+ `);
815
+ await db.run(import_drizzle_orm2.sql`
816
+ CREATE INDEX IF NOT EXISTS worktrees_repo_name_unique ON worktrees(repo_id, name)
817
+ `);
818
+ await db.run(import_drizzle_orm2.sql`
819
+ CREATE TABLE IF NOT EXISTS messages (
820
+ message_id TEXT PRIMARY KEY,
821
+ created_at INTEGER NOT NULL,
822
+ session_id TEXT NOT NULL,
823
+ task_id TEXT,
824
+ type TEXT NOT NULL CHECK(type IN ('user', 'assistant', 'system', 'file-history-snapshot', 'permission_request')),
825
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
826
+ "index" INTEGER NOT NULL,
827
+ timestamp INTEGER NOT NULL,
828
+ content_preview TEXT,
829
+ data TEXT NOT NULL,
830
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
831
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE SET NULL
832
+ )
833
+ `);
834
+ await db.run(import_drizzle_orm2.sql`
835
+ CREATE INDEX IF NOT EXISTS messages_session_id_idx ON messages(session_id)
836
+ `);
837
+ await db.run(import_drizzle_orm2.sql`
838
+ CREATE INDEX IF NOT EXISTS messages_task_id_idx ON messages(task_id)
839
+ `);
840
+ await db.run(import_drizzle_orm2.sql`
841
+ CREATE INDEX IF NOT EXISTS messages_session_index_idx ON messages(session_id, "index")
842
+ `);
843
+ await db.run(import_drizzle_orm2.sql`
844
+ CREATE TABLE IF NOT EXISTS users (
845
+ user_id TEXT PRIMARY KEY,
846
+ created_at INTEGER NOT NULL,
847
+ updated_at INTEGER,
848
+ email TEXT UNIQUE NOT NULL,
849
+ password TEXT NOT NULL,
850
+ name TEXT,
851
+ emoji TEXT,
852
+ role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('owner', 'admin', 'member', 'viewer')),
853
+ onboarding_completed INTEGER NOT NULL DEFAULT 0,
854
+ data TEXT NOT NULL
855
+ )
856
+ `);
857
+ await db.run(import_drizzle_orm2.sql`
858
+ CREATE INDEX IF NOT EXISTS users_email_idx ON users(email)
859
+ `);
860
+ await db.run(import_drizzle_orm2.sql`
861
+ CREATE TABLE IF NOT EXISTS mcp_servers (
862
+ mcp_server_id TEXT PRIMARY KEY,
863
+ created_at INTEGER NOT NULL,
864
+ updated_at INTEGER,
865
+ name TEXT NOT NULL,
866
+ transport TEXT NOT NULL CHECK(transport IN ('stdio', 'http', 'sse')),
867
+ scope TEXT NOT NULL CHECK(scope IN ('global', 'team', 'repo', 'session')),
868
+ enabled INTEGER NOT NULL DEFAULT 1,
869
+ owner_user_id TEXT,
870
+ team_id TEXT,
871
+ repo_id TEXT,
872
+ session_id TEXT,
873
+ source TEXT NOT NULL CHECK(source IN ('user', 'imported', 'agor')),
874
+ data TEXT NOT NULL,
875
+ FOREIGN KEY (repo_id) REFERENCES repos(repo_id) ON DELETE CASCADE,
876
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
877
+ )
878
+ `);
879
+ await db.run(import_drizzle_orm2.sql`
880
+ CREATE INDEX IF NOT EXISTS mcp_servers_name_idx ON mcp_servers(name)
881
+ `);
882
+ await db.run(import_drizzle_orm2.sql`
883
+ CREATE INDEX IF NOT EXISTS mcp_servers_scope_idx ON mcp_servers(scope)
884
+ `);
885
+ await db.run(import_drizzle_orm2.sql`
886
+ CREATE INDEX IF NOT EXISTS mcp_servers_owner_idx ON mcp_servers(owner_user_id)
887
+ `);
888
+ await db.run(import_drizzle_orm2.sql`
889
+ CREATE INDEX IF NOT EXISTS mcp_servers_team_idx ON mcp_servers(team_id)
890
+ `);
891
+ await db.run(import_drizzle_orm2.sql`
892
+ CREATE INDEX IF NOT EXISTS mcp_servers_repo_idx ON mcp_servers(repo_id)
893
+ `);
894
+ await db.run(import_drizzle_orm2.sql`
895
+ CREATE INDEX IF NOT EXISTS mcp_servers_session_idx ON mcp_servers(session_id)
896
+ `);
897
+ await db.run(import_drizzle_orm2.sql`
898
+ CREATE INDEX IF NOT EXISTS mcp_servers_enabled_idx ON mcp_servers(enabled)
899
+ `);
900
+ await db.run(import_drizzle_orm2.sql`
901
+ CREATE TABLE IF NOT EXISTS session_mcp_servers (
902
+ session_id TEXT NOT NULL,
903
+ mcp_server_id TEXT NOT NULL,
904
+ enabled INTEGER NOT NULL DEFAULT 1,
905
+ added_at INTEGER NOT NULL,
906
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
907
+ FOREIGN KEY (mcp_server_id) REFERENCES mcp_servers(mcp_server_id) ON DELETE CASCADE
908
+ )
909
+ `);
910
+ await db.run(import_drizzle_orm2.sql`
911
+ CREATE INDEX IF NOT EXISTS session_mcp_servers_pk ON session_mcp_servers(session_id, mcp_server_id)
912
+ `);
913
+ await db.run(import_drizzle_orm2.sql`
914
+ CREATE INDEX IF NOT EXISTS session_mcp_servers_session_idx ON session_mcp_servers(session_id)
915
+ `);
916
+ await db.run(import_drizzle_orm2.sql`
917
+ CREATE INDEX IF NOT EXISTS session_mcp_servers_server_idx ON session_mcp_servers(mcp_server_id)
918
+ `);
919
+ await db.run(import_drizzle_orm2.sql`
920
+ CREATE INDEX IF NOT EXISTS session_mcp_servers_enabled_idx ON session_mcp_servers(session_id, enabled)
921
+ `);
922
+ await db.run(import_drizzle_orm2.sql`
923
+ CREATE TABLE IF NOT EXISTS board_objects (
924
+ object_id TEXT PRIMARY KEY,
925
+ board_id TEXT NOT NULL,
926
+ created_at INTEGER NOT NULL,
927
+ worktree_id TEXT NOT NULL,
928
+ data TEXT NOT NULL,
929
+ FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE,
930
+ FOREIGN KEY (worktree_id) REFERENCES worktrees(worktree_id) ON DELETE CASCADE
931
+ )
932
+ `);
933
+ await db.run(import_drizzle_orm2.sql`
934
+ CREATE INDEX IF NOT EXISTS board_objects_board_idx ON board_objects(board_id)
935
+ `);
936
+ await db.run(import_drizzle_orm2.sql`
937
+ CREATE INDEX IF NOT EXISTS board_objects_worktree_idx ON board_objects(worktree_id)
938
+ `);
939
+ await db.run(import_drizzle_orm2.sql`
940
+ CREATE TABLE IF NOT EXISTS board_comments (
941
+ comment_id TEXT PRIMARY KEY,
942
+ created_at INTEGER NOT NULL,
943
+ updated_at INTEGER,
944
+ board_id TEXT NOT NULL,
945
+ created_by TEXT NOT NULL DEFAULT 'anonymous',
946
+ session_id TEXT,
947
+ task_id TEXT,
948
+ message_id TEXT,
949
+ worktree_id TEXT,
950
+ content TEXT NOT NULL,
951
+ content_preview TEXT NOT NULL,
952
+ parent_comment_id TEXT,
953
+ resolved INTEGER NOT NULL DEFAULT 0,
954
+ edited INTEGER NOT NULL DEFAULT 0,
955
+ reactions TEXT NOT NULL DEFAULT '[]',
956
+ data TEXT NOT NULL,
957
+ FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE,
958
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE SET NULL,
959
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE SET NULL,
960
+ FOREIGN KEY (message_id) REFERENCES messages(message_id) ON DELETE SET NULL,
961
+ FOREIGN KEY (worktree_id) REFERENCES worktrees(worktree_id) ON DELETE SET NULL
962
+ )
963
+ `);
964
+ await db.run(import_drizzle_orm2.sql`
965
+ CREATE INDEX IF NOT EXISTS board_comments_board_idx ON board_comments(board_id)
966
+ `);
967
+ await db.run(import_drizzle_orm2.sql`
968
+ CREATE INDEX IF NOT EXISTS board_comments_session_idx ON board_comments(session_id)
969
+ `);
970
+ await db.run(import_drizzle_orm2.sql`
971
+ CREATE INDEX IF NOT EXISTS board_comments_task_idx ON board_comments(task_id)
972
+ `);
973
+ await db.run(import_drizzle_orm2.sql`
974
+ CREATE INDEX IF NOT EXISTS board_comments_message_idx ON board_comments(message_id)
975
+ `);
976
+ await db.run(import_drizzle_orm2.sql`
977
+ CREATE INDEX IF NOT EXISTS board_comments_worktree_idx ON board_comments(worktree_id)
978
+ `);
979
+ await db.run(import_drizzle_orm2.sql`
980
+ CREATE INDEX IF NOT EXISTS board_comments_created_by_idx ON board_comments(created_by)
981
+ `);
982
+ await db.run(import_drizzle_orm2.sql`
983
+ CREATE INDEX IF NOT EXISTS board_comments_parent_idx ON board_comments(parent_comment_id)
984
+ `);
985
+ await db.run(import_drizzle_orm2.sql`
986
+ CREATE INDEX IF NOT EXISTS board_comments_created_idx ON board_comments(created_at)
987
+ `);
988
+ await db.run(import_drizzle_orm2.sql`
989
+ CREATE INDEX IF NOT EXISTS board_comments_resolved_idx ON board_comments(resolved)
990
+ `);
991
+ } catch (error) {
992
+ throw new MigrationError(
993
+ `Failed to create initial schema: ${error instanceof Error ? error.message : String(error)}`,
994
+ error
995
+ );
996
+ }
997
+ }
998
+ async function runMigrations(db, migrationsFolder = "./migrations") {
999
+ try {
1000
+ const exists = await tablesExist(db);
1001
+ if (!exists) {
1002
+ console.log("Creating initial database schema...");
1003
+ await createInitialSchema(db);
1004
+ console.log("Initial schema created successfully");
1005
+ } else {
1006
+ console.log("Running migrations...");
1007
+ await (0, import_migrator.migrate)(db, { migrationsFolder });
1008
+ console.log("Migrations applied successfully");
1009
+ }
1010
+ } catch (error) {
1011
+ throw new MigrationError(
1012
+ `Migration failed: ${error instanceof Error ? error.message : String(error)}`,
1013
+ error
1014
+ );
1015
+ }
1016
+ }
1017
+ async function initializeDatabase(db) {
1018
+ try {
1019
+ const exists = await tablesExist(db);
1020
+ if (!exists) {
1021
+ console.log("Initializing database schema...");
1022
+ await createInitialSchema(db);
1023
+ console.log("Database initialized successfully");
1024
+ } else {
1025
+ console.log("Checking for schema updates...");
1026
+ const hasBoardComments = await tableExists(db, "board_comments");
1027
+ if (!hasBoardComments) {
1028
+ console.log(" Adding board_comments table...");
1029
+ await db.run(import_drizzle_orm2.sql`
1030
+ CREATE TABLE board_comments (
1031
+ comment_id TEXT PRIMARY KEY,
1032
+ created_at INTEGER NOT NULL,
1033
+ updated_at INTEGER,
1034
+ board_id TEXT NOT NULL,
1035
+ created_by TEXT NOT NULL DEFAULT 'anonymous',
1036
+ session_id TEXT,
1037
+ task_id TEXT,
1038
+ message_id TEXT,
1039
+ worktree_id TEXT,
1040
+ content TEXT NOT NULL,
1041
+ content_preview TEXT NOT NULL,
1042
+ parent_comment_id TEXT,
1043
+ resolved INTEGER NOT NULL DEFAULT 0,
1044
+ edited INTEGER NOT NULL DEFAULT 0,
1045
+ reactions TEXT NOT NULL DEFAULT '[]',
1046
+ data TEXT NOT NULL,
1047
+ FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE,
1048
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE SET NULL,
1049
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE SET NULL,
1050
+ FOREIGN KEY (message_id) REFERENCES messages(message_id) ON DELETE SET NULL,
1051
+ FOREIGN KEY (worktree_id) REFERENCES worktrees(worktree_id) ON DELETE SET NULL
1052
+ )
1053
+ `);
1054
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_board_idx ON board_comments(board_id)`);
1055
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_session_idx ON board_comments(session_id)`);
1056
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_task_idx ON board_comments(task_id)`);
1057
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_message_idx ON board_comments(message_id)`);
1058
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_worktree_idx ON board_comments(worktree_id)`);
1059
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_created_by_idx ON board_comments(created_by)`);
1060
+ await db.run(
1061
+ import_drizzle_orm2.sql`CREATE INDEX board_comments_parent_idx ON board_comments(parent_comment_id)`
1062
+ );
1063
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_created_idx ON board_comments(created_at)`);
1064
+ await db.run(import_drizzle_orm2.sql`CREATE INDEX board_comments_resolved_idx ON board_comments(resolved)`);
1065
+ console.log(" \u2705 board_comments table added");
1066
+ } else {
1067
+ try {
1068
+ const tableInfo = await db.run(import_drizzle_orm2.sql`PRAGMA table_info(board_comments)`);
1069
+ const hasReactionsColumn = tableInfo.rows.some(
1070
+ (row) => row.name === "reactions"
1071
+ );
1072
+ if (!hasReactionsColumn) {
1073
+ console.log(" Adding reactions column to board_comments...");
1074
+ await db.run(import_drizzle_orm2.sql`
1075
+ ALTER TABLE board_comments ADD COLUMN reactions TEXT NOT NULL DEFAULT '[]'
1076
+ `);
1077
+ console.log(" \u2705 reactions column added");
1078
+ }
1079
+ } catch (error) {
1080
+ console.error(" \u26A0\uFE0F Failed to add reactions column:", error);
1081
+ }
1082
+ }
1083
+ console.log("Schema up to date");
1084
+ }
1085
+ } catch (error) {
1086
+ throw new MigrationError(
1087
+ `Database initialization failed: ${error instanceof Error ? error.message : String(error)}`,
1088
+ error
1089
+ );
1090
+ }
1091
+ }
1092
+ async function seedInitialData(db) {
1093
+ try {
1094
+ const result = await db.run(import_drizzle_orm2.sql`
1095
+ SELECT board_id FROM boards WHERE slug = 'default'
1096
+ `);
1097
+ if (result.rows.length === 0) {
1098
+ const { generateId: generateId2 } = await Promise.resolve().then(() => (init_ids(), ids_exports));
1099
+ const boardId = generateId2();
1100
+ const now = Date.now();
1101
+ await db.run(import_drizzle_orm2.sql`
1102
+ INSERT INTO boards (board_id, name, slug, created_at, updated_at, created_by, data)
1103
+ VALUES (
1104
+ ${boardId},
1105
+ ${"Main Board"},
1106
+ ${"default"},
1107
+ ${now},
1108
+ ${now},
1109
+ ${"anonymous"},
1110
+ ${JSON.stringify({
1111
+ description: "Main board for all sessions",
1112
+ sessions: [],
1113
+ color: "#1677ff",
1114
+ icon: "\u2B50"
1115
+ })}
1116
+ )
1117
+ `);
1118
+ console.log("Main Board created");
1119
+ }
1120
+ } catch (error) {
1121
+ throw new MigrationError(
1122
+ `Failed to seed initial data: ${error instanceof Error ? error.message : String(error)}`,
1123
+ error
1124
+ );
1125
+ }
1126
+ }
1127
+
1128
+ // src/db/repositories/base.ts
1129
+ var RepositoryError = class extends Error {
1130
+ constructor(message, cause) {
1131
+ super(message);
1132
+ this.cause = cause;
1133
+ this.name = "RepositoryError";
1134
+ }
1135
+ };
1136
+ var EntityNotFoundError = class extends RepositoryError {
1137
+ constructor(entityType, id) {
1138
+ super(`${entityType} with ID '${id}' not found`);
1139
+ this.entityType = entityType;
1140
+ this.id = id;
1141
+ this.name = "EntityNotFoundError";
1142
+ }
1143
+ };
1144
+ var AmbiguousIdError = class extends RepositoryError {
1145
+ constructor(entityType, prefix, matches) {
1146
+ super(
1147
+ `Ambiguous ID prefix '${prefix}' for ${entityType} (${matches.length} matches: ${matches.slice(0, 3).join(", ")}${matches.length > 3 ? "..." : ""})`
1148
+ );
1149
+ this.entityType = entityType;
1150
+ this.prefix = prefix;
1151
+ this.matches = matches;
1152
+ this.name = "AmbiguousIdError";
1153
+ }
1154
+ };
1155
+
1156
+ // src/db/repositories/board-comments.ts
1157
+ var import_drizzle_orm3 = require("drizzle-orm");
1158
+ init_ids();
1159
+ function generatePreview(content) {
1160
+ return content.length > 200 ? content.slice(0, 200) + "..." : content;
1161
+ }
1162
+ var BoardCommentsRepository = class {
1163
+ constructor(db) {
1164
+ this.db = db;
1165
+ }
1166
+ /**
1167
+ * Convert database row to BoardComment type
1168
+ */
1169
+ rowToComment(row) {
1170
+ const data = row.data;
1171
+ const reactions = row.reactions ? typeof row.reactions === "string" ? JSON.parse(row.reactions) : row.reactions : [];
1172
+ return {
1173
+ comment_id: row.comment_id,
1174
+ board_id: row.board_id,
1175
+ created_by: row.created_by,
1176
+ content: row.content,
1177
+ content_preview: row.content_preview,
1178
+ session_id: row.session_id ? row.session_id : void 0,
1179
+ task_id: row.task_id ? row.task_id : void 0,
1180
+ message_id: row.message_id ? row.message_id : void 0,
1181
+ worktree_id: row.worktree_id ? row.worktree_id : void 0,
1182
+ parent_comment_id: row.parent_comment_id ? row.parent_comment_id : void 0,
1183
+ resolved: Boolean(row.resolved),
1184
+ edited: Boolean(row.edited),
1185
+ reactions,
1186
+ position: data.position,
1187
+ mentions: data.mentions ? data.mentions : void 0,
1188
+ created_at: new Date(row.created_at),
1189
+ updated_at: row.updated_at ? new Date(row.updated_at) : void 0
1190
+ };
1191
+ }
1192
+ /**
1193
+ * Convert BoardComment to database insert format
1194
+ */
1195
+ commentToInsert(comment) {
1196
+ const now = Date.now();
1197
+ const commentId = comment.comment_id ?? generateId();
1198
+ const contentPreview = comment.content_preview ?? (comment.content ? generatePreview(comment.content) : "");
1199
+ return {
1200
+ comment_id: commentId,
1201
+ board_id: comment.board_id,
1202
+ created_by: comment.created_by ?? "anonymous",
1203
+ content: comment.content ?? "",
1204
+ content_preview: contentPreview,
1205
+ session_id: comment.session_id ?? null,
1206
+ task_id: comment.task_id ?? null,
1207
+ message_id: comment.message_id ?? null,
1208
+ worktree_id: comment.worktree_id ?? null,
1209
+ parent_comment_id: comment.parent_comment_id ?? null,
1210
+ resolved: comment.resolved ?? false,
1211
+ edited: comment.edited ?? false,
1212
+ reactions: comment.reactions ?? [],
1213
+ created_at: new Date(comment.created_at ?? now),
1214
+ updated_at: comment.updated_at ? new Date(comment.updated_at) : null,
1215
+ data: {
1216
+ position: comment.position,
1217
+ mentions: comment.mentions
1218
+ }
1219
+ };
1220
+ }
1221
+ /**
1222
+ * Resolve short ID to full ID
1223
+ */
1224
+ async resolveId(id) {
1225
+ if (id.length === 36 && id.includes("-")) {
1226
+ return id;
1227
+ }
1228
+ const normalized = id.replace(/-/g, "").toLowerCase();
1229
+ const pattern = `${normalized}%`;
1230
+ const results = await this.db.select({ comment_id: boardComments.comment_id }).from(boardComments).where((0, import_drizzle_orm3.like)(boardComments.comment_id, pattern)).all();
1231
+ if (results.length === 0) {
1232
+ throw new EntityNotFoundError("BoardComment", id);
1233
+ }
1234
+ if (results.length > 1) {
1235
+ throw new AmbiguousIdError(
1236
+ "BoardComment",
1237
+ id,
1238
+ results.map((r) => formatShortId(r.comment_id))
1239
+ );
1240
+ }
1241
+ return results[0].comment_id;
1242
+ }
1243
+ /**
1244
+ * Create a new comment
1245
+ */
1246
+ async create(data) {
1247
+ try {
1248
+ const insert = this.commentToInsert(data);
1249
+ await this.db.insert(boardComments).values(insert);
1250
+ const row = await this.db.select().from(boardComments).where((0, import_drizzle_orm3.eq)(boardComments.comment_id, insert.comment_id)).get();
1251
+ if (!row) {
1252
+ throw new RepositoryError("Failed to retrieve created comment");
1253
+ }
1254
+ return this.rowToComment(row);
1255
+ } catch (error) {
1256
+ if (error instanceof RepositoryError) throw error;
1257
+ throw new RepositoryError(
1258
+ `Failed to create comment: ${error instanceof Error ? error.message : String(error)}`,
1259
+ error
1260
+ );
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Find comment by ID (supports short ID)
1265
+ */
1266
+ async findById(id) {
1267
+ try {
1268
+ const fullId = await this.resolveId(id);
1269
+ const row = await this.db.select().from(boardComments).where((0, import_drizzle_orm3.eq)(boardComments.comment_id, fullId)).get();
1270
+ return row ? this.rowToComment(row) : null;
1271
+ } catch (error) {
1272
+ if (error instanceof EntityNotFoundError) return null;
1273
+ if (error instanceof AmbiguousIdError) throw error;
1274
+ throw new RepositoryError(
1275
+ `Failed to find comment: ${error instanceof Error ? error.message : String(error)}`,
1276
+ error
1277
+ );
1278
+ }
1279
+ }
1280
+ /**
1281
+ * Find all comments (optionally filtered by board, session, task, etc.)
1282
+ */
1283
+ async findAll(filters) {
1284
+ try {
1285
+ let query = this.db.select().from(boardComments);
1286
+ const conditions = [];
1287
+ if (filters?.board_id) {
1288
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.board_id, filters.board_id));
1289
+ }
1290
+ if (filters?.session_id !== void 0) {
1291
+ if (filters.session_id === null) {
1292
+ conditions.push((0, import_drizzle_orm3.isNull)(boardComments.session_id));
1293
+ } else {
1294
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.session_id, filters.session_id));
1295
+ }
1296
+ }
1297
+ if (filters?.task_id !== void 0) {
1298
+ if (filters.task_id === null) {
1299
+ conditions.push((0, import_drizzle_orm3.isNull)(boardComments.task_id));
1300
+ } else {
1301
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.task_id, filters.task_id));
1302
+ }
1303
+ }
1304
+ if (filters?.message_id !== void 0) {
1305
+ if (filters.message_id === null) {
1306
+ conditions.push((0, import_drizzle_orm3.isNull)(boardComments.message_id));
1307
+ } else {
1308
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.message_id, filters.message_id));
1309
+ }
1310
+ }
1311
+ if (filters?.worktree_id !== void 0) {
1312
+ if (filters.worktree_id === null) {
1313
+ conditions.push((0, import_drizzle_orm3.isNull)(boardComments.worktree_id));
1314
+ } else {
1315
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.worktree_id, filters.worktree_id));
1316
+ }
1317
+ }
1318
+ if (filters?.resolved !== void 0) {
1319
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.resolved, filters.resolved));
1320
+ }
1321
+ if (filters?.created_by) {
1322
+ conditions.push((0, import_drizzle_orm3.eq)(boardComments.created_by, filters.created_by));
1323
+ }
1324
+ if (conditions.length > 0) {
1325
+ query = query.where((0, import_drizzle_orm3.and)(...conditions));
1326
+ }
1327
+ const rows = await query.all();
1328
+ return rows.map((row) => this.rowToComment(row));
1329
+ } catch (error) {
1330
+ throw new RepositoryError(
1331
+ `Failed to find comments: ${error instanceof Error ? error.message : String(error)}`,
1332
+ error
1333
+ );
1334
+ }
1335
+ }
1336
+ /**
1337
+ * Update comment by ID
1338
+ */
1339
+ async update(id, updates) {
1340
+ try {
1341
+ const fullId = await this.resolveId(id);
1342
+ const current = await this.findById(fullId);
1343
+ if (!current) {
1344
+ throw new EntityNotFoundError("BoardComment", id);
1345
+ }
1346
+ const merged = { ...current, ...updates };
1347
+ if (updates.content && !updates.content_preview) {
1348
+ merged.content_preview = generatePreview(updates.content);
1349
+ }
1350
+ if (updates.content && updates.content !== current.content) {
1351
+ merged.edited = true;
1352
+ }
1353
+ const insert = this.commentToInsert(merged);
1354
+ await this.db.update(boardComments).set({
1355
+ content: insert.content,
1356
+ content_preview: insert.content_preview,
1357
+ session_id: insert.session_id,
1358
+ task_id: insert.task_id,
1359
+ message_id: insert.message_id,
1360
+ worktree_id: insert.worktree_id,
1361
+ parent_comment_id: insert.parent_comment_id,
1362
+ resolved: insert.resolved,
1363
+ edited: insert.edited,
1364
+ reactions: insert.reactions,
1365
+ updated_at: /* @__PURE__ */ new Date(),
1366
+ data: insert.data
1367
+ }).where((0, import_drizzle_orm3.eq)(boardComments.comment_id, fullId));
1368
+ const updated = await this.findById(fullId);
1369
+ if (!updated) {
1370
+ throw new RepositoryError("Failed to retrieve updated comment");
1371
+ }
1372
+ return updated;
1373
+ } catch (error) {
1374
+ if (error instanceof RepositoryError) throw error;
1375
+ if (error instanceof EntityNotFoundError) throw error;
1376
+ throw new RepositoryError(
1377
+ `Failed to update comment: ${error instanceof Error ? error.message : String(error)}`,
1378
+ error
1379
+ );
1380
+ }
1381
+ }
1382
+ /**
1383
+ * Delete comment by ID
1384
+ * If deleting a thread root, also deletes all replies (cascade)
1385
+ */
1386
+ async delete(id) {
1387
+ try {
1388
+ const fullId = await this.resolveId(id);
1389
+ await this.db.delete(boardComments).where((0, import_drizzle_orm3.eq)(boardComments.parent_comment_id, fullId)).run();
1390
+ const result = await this.db.delete(boardComments).where((0, import_drizzle_orm3.eq)(boardComments.comment_id, fullId)).run();
1391
+ if (result.rowsAffected === 0) {
1392
+ throw new EntityNotFoundError("BoardComment", id);
1393
+ }
1394
+ } catch (error) {
1395
+ if (error instanceof EntityNotFoundError) throw error;
1396
+ throw new RepositoryError(
1397
+ `Failed to delete comment: ${error instanceof Error ? error.message : String(error)}`,
1398
+ error
1399
+ );
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Resolve comment (mark as resolved)
1404
+ */
1405
+ async resolve(id) {
1406
+ return this.update(id, { resolved: true });
1407
+ }
1408
+ /**
1409
+ * Unresolve comment (mark as unresolved)
1410
+ */
1411
+ async unresolve(id) {
1412
+ return this.update(id, { resolved: false });
1413
+ }
1414
+ /**
1415
+ * Find comments by board ID with optional filters
1416
+ */
1417
+ async findByBoard(boardId, filters) {
1418
+ return this.findAll({ board_id: boardId, ...filters });
1419
+ }
1420
+ /**
1421
+ * Find comments for a specific session
1422
+ */
1423
+ async findBySession(sessionId) {
1424
+ return this.findAll({ session_id: sessionId });
1425
+ }
1426
+ /**
1427
+ * Find comments for a specific task
1428
+ */
1429
+ async findByTask(taskId) {
1430
+ return this.findAll({ task_id: taskId });
1431
+ }
1432
+ /**
1433
+ * Find comments mentioning a specific user
1434
+ */
1435
+ async findMentions(userId, boardId) {
1436
+ const comments = await this.findAll({ board_id: boardId });
1437
+ return comments.filter((comment) => comment.mentions?.includes(userId));
1438
+ }
1439
+ /**
1440
+ * Batch create comments (for bulk operations)
1441
+ */
1442
+ async bulkCreate(comments) {
1443
+ try {
1444
+ const inserts = comments.map((comment) => this.commentToInsert(comment));
1445
+ await this.db.insert(boardComments).values(inserts);
1446
+ const commentIds = inserts.map((insert) => insert.comment_id);
1447
+ const rows = await this.db.select().from(boardComments).where(
1448
+ (0, import_drizzle_orm3.eq)(
1449
+ boardComments.comment_id,
1450
+ commentIds[0]
1451
+ // TODO: Support proper IN clause when available
1452
+ )
1453
+ ).all();
1454
+ return rows.map((row) => this.rowToComment(row));
1455
+ } catch (error) {
1456
+ throw new RepositoryError(
1457
+ `Failed to bulk create comments: ${error instanceof Error ? error.message : String(error)}`,
1458
+ error
1459
+ );
1460
+ }
1461
+ }
1462
+ // ============================================================================
1463
+ // Phase 2: Threading + Reactions
1464
+ // ============================================================================
1465
+ /**
1466
+ * Toggle a reaction on a comment
1467
+ * If user has already reacted with this emoji, remove it. Otherwise, add it.
1468
+ */
1469
+ async toggleReaction(commentId, userId, emoji) {
1470
+ try {
1471
+ const comment = await this.findById(commentId);
1472
+ if (!comment) {
1473
+ throw new EntityNotFoundError("BoardComment", commentId);
1474
+ }
1475
+ const reactions = comment.reactions || [];
1476
+ const existingIndex = reactions.findIndex((r) => r.user_id === userId && r.emoji === emoji);
1477
+ let updatedReactions;
1478
+ if (existingIndex >= 0) {
1479
+ updatedReactions = reactions.filter((_, i) => i !== existingIndex);
1480
+ } else {
1481
+ updatedReactions = [...reactions, { user_id: userId, emoji }];
1482
+ }
1483
+ return this.update(commentId, { reactions: updatedReactions });
1484
+ } catch (error) {
1485
+ if (error instanceof EntityNotFoundError) throw error;
1486
+ throw new RepositoryError(
1487
+ `Failed to toggle reaction: ${error instanceof Error ? error.message : String(error)}`,
1488
+ error
1489
+ );
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Create a reply to a comment (thread root)
1494
+ * Validates that parent exists and is a thread root
1495
+ */
1496
+ async createReply(parentId, data) {
1497
+ try {
1498
+ const parent = await this.findById(parentId);
1499
+ if (!parent) {
1500
+ throw new EntityNotFoundError("BoardComment", parentId);
1501
+ }
1502
+ if (parent.parent_comment_id) {
1503
+ throw new RepositoryError(
1504
+ "Cannot reply to a reply. Replies can only be added to thread roots (2-layer limit)."
1505
+ );
1506
+ }
1507
+ const reply = {
1508
+ ...data,
1509
+ parent_comment_id: parent.comment_id,
1510
+ board_id: parent.board_id,
1511
+ // Inherit board_id from parent
1512
+ // Replies don't have attachments - they inherit context from parent
1513
+ session_id: void 0,
1514
+ task_id: void 0,
1515
+ message_id: void 0,
1516
+ worktree_id: void 0,
1517
+ position: void 0
1518
+ };
1519
+ return this.create(reply);
1520
+ } catch (error) {
1521
+ if (error instanceof EntityNotFoundError) throw error;
1522
+ if (error instanceof RepositoryError) throw error;
1523
+ throw new RepositoryError(
1524
+ `Failed to create reply: ${error instanceof Error ? error.message : String(error)}`,
1525
+ error
1526
+ );
1527
+ }
1528
+ }
1529
+ };
1530
+
1531
+ // src/db/repositories/board-objects.ts
1532
+ var import_drizzle_orm4 = require("drizzle-orm");
1533
+ init_ids();
1534
+ var BoardObjectRepository = class {
1535
+ constructor(db) {
1536
+ this.db = db;
1537
+ }
1538
+ /**
1539
+ * Find all board objects
1540
+ */
1541
+ async findAll() {
1542
+ try {
1543
+ const rows = await this.db.select().from(boardObjects).all();
1544
+ return rows.map(this.rowToEntity);
1545
+ } catch (error) {
1546
+ throw new RepositoryError(
1547
+ `Failed to find all board objects: ${error instanceof Error ? error.message : String(error)}`,
1548
+ error
1549
+ );
1550
+ }
1551
+ }
1552
+ /**
1553
+ * Find all board objects for a board
1554
+ */
1555
+ async findByBoardId(boardId) {
1556
+ try {
1557
+ const rows = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.board_id, boardId)).all();
1558
+ return rows.map(this.rowToEntity);
1559
+ } catch (error) {
1560
+ throw new RepositoryError(
1561
+ `Failed to find board objects: ${error instanceof Error ? error.message : String(error)}`,
1562
+ error
1563
+ );
1564
+ }
1565
+ }
1566
+ /**
1567
+ * Find board object by object ID
1568
+ */
1569
+ async findByObjectId(objectId) {
1570
+ try {
1571
+ const row = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId)).get();
1572
+ return row ? this.rowToEntity(row) : null;
1573
+ } catch (error) {
1574
+ throw new RepositoryError(
1575
+ `Failed to find board object by object_id: ${error instanceof Error ? error.message : String(error)}`,
1576
+ error
1577
+ );
1578
+ }
1579
+ }
1580
+ /**
1581
+ * Find board object by worktree ID
1582
+ */
1583
+ async findByWorktreeId(worktreeId) {
1584
+ try {
1585
+ const row = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.worktree_id, worktreeId)).get();
1586
+ return row ? this.rowToEntity(row) : null;
1587
+ } catch (error) {
1588
+ throw new RepositoryError(
1589
+ `Failed to find board object by worktree: ${error instanceof Error ? error.message : String(error)}`,
1590
+ error
1591
+ );
1592
+ }
1593
+ }
1594
+ /**
1595
+ * Create a board object (add worktree to board)
1596
+ */
1597
+ async create(data) {
1598
+ try {
1599
+ const existing = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.worktree_id, data.worktree_id)).get();
1600
+ if (existing) {
1601
+ throw new RepositoryError(`Worktree already on a board (object_id: ${existing.object_id})`);
1602
+ }
1603
+ const insert = {
1604
+ object_id: generateId(),
1605
+ board_id: data.board_id,
1606
+ worktree_id: data.worktree_id,
1607
+ created_at: /* @__PURE__ */ new Date(),
1608
+ data: {
1609
+ position: data.position,
1610
+ zone_id: data.zone_id
1611
+ }
1612
+ };
1613
+ await this.db.insert(boardObjects).values(insert);
1614
+ const row = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, insert.object_id)).get();
1615
+ if (!row) {
1616
+ throw new RepositoryError("Failed to retrieve created board object");
1617
+ }
1618
+ return this.rowToEntity(row);
1619
+ } catch (error) {
1620
+ if (error instanceof RepositoryError) throw error;
1621
+ throw new RepositoryError(
1622
+ `Failed to create board object: ${error instanceof Error ? error.message : String(error)}`,
1623
+ error
1624
+ );
1625
+ }
1626
+ }
1627
+ /**
1628
+ * Update position of board object (preserves zone_id)
1629
+ */
1630
+ async updatePosition(objectId, position) {
1631
+ try {
1632
+ const existing = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId)).get();
1633
+ if (!existing) {
1634
+ throw new EntityNotFoundError("BoardObject", objectId);
1635
+ }
1636
+ const existingData = typeof existing.data === "string" ? JSON.parse(existing.data) : existing.data;
1637
+ await this.db.update(boardObjects).set({
1638
+ data: {
1639
+ position,
1640
+ zone_id: existingData.zone_id
1641
+ }
1642
+ }).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId));
1643
+ const row = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId)).get();
1644
+ if (!row) {
1645
+ throw new RepositoryError("Failed to retrieve updated board object");
1646
+ }
1647
+ return this.rowToEntity(row);
1648
+ } catch (error) {
1649
+ if (error instanceof EntityNotFoundError) throw error;
1650
+ throw new RepositoryError(
1651
+ `Failed to update board object position: ${error instanceof Error ? error.message : String(error)}`,
1652
+ error
1653
+ );
1654
+ }
1655
+ }
1656
+ /**
1657
+ * Update zone pinning for board object
1658
+ */
1659
+ async updateZone(objectId, zoneId) {
1660
+ try {
1661
+ const existing = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId)).get();
1662
+ if (!existing) {
1663
+ throw new EntityNotFoundError("BoardObject", objectId);
1664
+ }
1665
+ const existingData = typeof existing.data === "string" ? JSON.parse(existing.data) : existing.data;
1666
+ await this.db.update(boardObjects).set({
1667
+ data: {
1668
+ position: existingData.position,
1669
+ // Convert null to undefined for consistency
1670
+ zone_id: zoneId === null ? void 0 : zoneId
1671
+ }
1672
+ }).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId));
1673
+ const row = await this.db.select().from(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId)).get();
1674
+ if (!row) {
1675
+ throw new RepositoryError("Failed to retrieve updated board object");
1676
+ }
1677
+ return this.rowToEntity(row);
1678
+ } catch (error) {
1679
+ if (error instanceof EntityNotFoundError) throw error;
1680
+ throw new RepositoryError(
1681
+ `Failed to update board object zone: ${error instanceof Error ? error.message : String(error)}`,
1682
+ error
1683
+ );
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Remove board object (remove entity from board)
1688
+ */
1689
+ async remove(objectId) {
1690
+ try {
1691
+ const result = await this.db.delete(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.object_id, objectId)).run();
1692
+ if (result.rowsAffected === 0) {
1693
+ throw new EntityNotFoundError("BoardObject", objectId);
1694
+ }
1695
+ } catch (error) {
1696
+ if (error instanceof EntityNotFoundError) throw error;
1697
+ throw new RepositoryError(
1698
+ `Failed to remove board object: ${error instanceof Error ? error.message : String(error)}`,
1699
+ error
1700
+ );
1701
+ }
1702
+ }
1703
+ /**
1704
+ * Remove all board objects for a worktree
1705
+ */
1706
+ async removeByWorktreeId(worktreeId) {
1707
+ try {
1708
+ await this.db.delete(boardObjects).where((0, import_drizzle_orm4.eq)(boardObjects.worktree_id, worktreeId));
1709
+ } catch (error) {
1710
+ throw new RepositoryError(
1711
+ `Failed to remove board objects by worktree: ${error instanceof Error ? error.message : String(error)}`,
1712
+ error
1713
+ );
1714
+ }
1715
+ }
1716
+ /**
1717
+ * Convert database row to entity
1718
+ */
1719
+ rowToEntity(row) {
1720
+ const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
1721
+ return {
1722
+ object_id: row.object_id,
1723
+ board_id: row.board_id,
1724
+ worktree_id: row.worktree_id,
1725
+ position: data.position,
1726
+ zone_id: data.zone_id,
1727
+ created_at: new Date(row.created_at).toISOString()
1728
+ };
1729
+ }
1730
+ };
1731
+
1732
+ // src/db/repositories/boards.ts
1733
+ var import_drizzle_orm5 = require("drizzle-orm");
1734
+ init_ids();
1735
+ var BoardRepository = class {
1736
+ constructor(db) {
1737
+ this.db = db;
1738
+ }
1739
+ /**
1740
+ * Convert database row to Board type
1741
+ */
1742
+ rowToBoard(row) {
1743
+ const data = row.data;
1744
+ return {
1745
+ board_id: row.board_id,
1746
+ name: row.name,
1747
+ slug: row.slug || void 0,
1748
+ created_at: new Date(row.created_at).toISOString(),
1749
+ last_updated: row.updated_at ? new Date(row.updated_at).toISOString() : new Date(row.created_at).toISOString(),
1750
+ created_by: row.created_by,
1751
+ ...data
1752
+ };
1753
+ }
1754
+ /**
1755
+ * Convert Board to database insert format
1756
+ */
1757
+ boardToInsert(board) {
1758
+ const now = Date.now();
1759
+ const boardId = board.board_id ?? generateId();
1760
+ return {
1761
+ board_id: boardId,
1762
+ name: board.name ?? "Untitled Board",
1763
+ slug: board.slug ?? null,
1764
+ created_at: new Date(board.created_at ?? now),
1765
+ updated_at: board.last_updated ? new Date(board.last_updated) : new Date(now),
1766
+ created_by: board.created_by ?? "anonymous",
1767
+ data: {
1768
+ description: board.description,
1769
+ color: board.color,
1770
+ icon: board.icon,
1771
+ objects: board.objects,
1772
+ custom_context: board.custom_context
1773
+ }
1774
+ };
1775
+ }
1776
+ /**
1777
+ * Resolve short ID to full ID
1778
+ */
1779
+ async resolveId(id) {
1780
+ if (id.length === 36 && id.includes("-")) {
1781
+ return id;
1782
+ }
1783
+ const normalized = id.replace(/-/g, "").toLowerCase();
1784
+ const pattern = `${normalized}%`;
1785
+ const results = await this.db.select({ board_id: boards.board_id }).from(boards).where((0, import_drizzle_orm5.like)(boards.board_id, pattern)).all();
1786
+ if (results.length === 0) {
1787
+ throw new EntityNotFoundError("Board", id);
1788
+ }
1789
+ if (results.length > 1) {
1790
+ throw new AmbiguousIdError(
1791
+ "Board",
1792
+ id,
1793
+ results.map((r) => formatShortId(r.board_id))
1794
+ );
1795
+ }
1796
+ return results[0].board_id;
1797
+ }
1798
+ /**
1799
+ * Create a new board
1800
+ */
1801
+ async create(data) {
1802
+ try {
1803
+ const insert = this.boardToInsert(data);
1804
+ await this.db.insert(boards).values(insert);
1805
+ const row = await this.db.select().from(boards).where((0, import_drizzle_orm5.eq)(boards.board_id, insert.board_id)).get();
1806
+ if (!row) {
1807
+ throw new RepositoryError("Failed to retrieve created board");
1808
+ }
1809
+ return this.rowToBoard(row);
1810
+ } catch (error) {
1811
+ if (error instanceof RepositoryError) throw error;
1812
+ throw new RepositoryError(
1813
+ `Failed to create board: ${error instanceof Error ? error.message : String(error)}`,
1814
+ error
1815
+ );
1816
+ }
1817
+ }
1818
+ /**
1819
+ * Find board by ID (supports short ID)
1820
+ */
1821
+ async findById(id) {
1822
+ try {
1823
+ const fullId = await this.resolveId(id);
1824
+ const row = await this.db.select().from(boards).where((0, import_drizzle_orm5.eq)(boards.board_id, fullId)).get();
1825
+ return row ? this.rowToBoard(row) : null;
1826
+ } catch (error) {
1827
+ if (error instanceof EntityNotFoundError) return null;
1828
+ if (error instanceof AmbiguousIdError) throw error;
1829
+ throw new RepositoryError(
1830
+ `Failed to find board: ${error instanceof Error ? error.message : String(error)}`,
1831
+ error
1832
+ );
1833
+ }
1834
+ }
1835
+ /**
1836
+ * Find board by slug
1837
+ */
1838
+ async findBySlug(slug) {
1839
+ try {
1840
+ const row = await this.db.select().from(boards).where((0, import_drizzle_orm5.eq)(boards.slug, slug)).get();
1841
+ return row ? this.rowToBoard(row) : null;
1842
+ } catch (error) {
1843
+ throw new RepositoryError(
1844
+ `Failed to find board by slug: ${error instanceof Error ? error.message : String(error)}`,
1845
+ error
1846
+ );
1847
+ }
1848
+ }
1849
+ /**
1850
+ * Find all boards
1851
+ */
1852
+ async findAll() {
1853
+ try {
1854
+ const rows = await this.db.select().from(boards).all();
1855
+ return rows.map((row) => this.rowToBoard(row));
1856
+ } catch (error) {
1857
+ throw new RepositoryError(
1858
+ `Failed to find all boards: ${error instanceof Error ? error.message : String(error)}`,
1859
+ error
1860
+ );
1861
+ }
1862
+ }
1863
+ /**
1864
+ * Update board by ID
1865
+ */
1866
+ async update(id, updates) {
1867
+ try {
1868
+ const fullId = await this.resolveId(id);
1869
+ const current = await this.findById(fullId);
1870
+ if (!current) {
1871
+ throw new EntityNotFoundError("Board", id);
1872
+ }
1873
+ const merged = { ...current, ...updates };
1874
+ const insert = this.boardToInsert(merged);
1875
+ await this.db.update(boards).set({
1876
+ name: insert.name,
1877
+ slug: insert.slug,
1878
+ updated_at: /* @__PURE__ */ new Date(),
1879
+ data: insert.data
1880
+ }).where((0, import_drizzle_orm5.eq)(boards.board_id, fullId));
1881
+ const updated = await this.findById(fullId);
1882
+ if (!updated) {
1883
+ throw new RepositoryError("Failed to retrieve updated board");
1884
+ }
1885
+ return updated;
1886
+ } catch (error) {
1887
+ if (error instanceof RepositoryError) throw error;
1888
+ if (error instanceof EntityNotFoundError) throw error;
1889
+ throw new RepositoryError(
1890
+ `Failed to update board: ${error instanceof Error ? error.message : String(error)}`,
1891
+ error
1892
+ );
1893
+ }
1894
+ }
1895
+ /**
1896
+ * Delete board by ID
1897
+ */
1898
+ async delete(id) {
1899
+ try {
1900
+ const fullId = await this.resolveId(id);
1901
+ const result = await this.db.delete(boards).where((0, import_drizzle_orm5.eq)(boards.board_id, fullId)).run();
1902
+ if (result.rowsAffected === 0) {
1903
+ throw new EntityNotFoundError("Board", id);
1904
+ }
1905
+ } catch (error) {
1906
+ if (error instanceof EntityNotFoundError) throw error;
1907
+ throw new RepositoryError(
1908
+ `Failed to delete board: ${error instanceof Error ? error.message : String(error)}`,
1909
+ error
1910
+ );
1911
+ }
1912
+ }
1913
+ /**
1914
+ * DEPRECATED: Add session to board
1915
+ * Use board-objects service instead
1916
+ */
1917
+ // async addSession(boardId: string, sessionId: string): Promise<Board> {
1918
+ // throw new RepositoryError('addSession is deprecated - use board-objects service');
1919
+ // }
1920
+ /**
1921
+ * DEPRECATED: Remove session from board
1922
+ * Use board-objects service instead
1923
+ */
1924
+ // async removeSession(boardId: string, sessionId: string): Promise<Board> {
1925
+ // throw new RepositoryError('removeSession is deprecated - use board-objects service');
1926
+ // }
1927
+ /**
1928
+ * Get default board (or create if doesn't exist)
1929
+ */
1930
+ async getDefault() {
1931
+ try {
1932
+ const defaultBoard = await this.findBySlug("default");
1933
+ if (defaultBoard) {
1934
+ return defaultBoard;
1935
+ }
1936
+ return this.create({
1937
+ name: "Main Board",
1938
+ slug: "default",
1939
+ description: "Main board for all sessions",
1940
+ color: "#1677ff",
1941
+ icon: "\u2B50"
1942
+ });
1943
+ } catch (error) {
1944
+ throw new RepositoryError(
1945
+ `Failed to get default board: ${error instanceof Error ? error.message : String(error)}`,
1946
+ error
1947
+ );
1948
+ }
1949
+ }
1950
+ /**
1951
+ * Atomically add or update a board object (text label or zone)
1952
+ *
1953
+ * Uses read-modify-write approach with proper serialization via update() method.
1954
+ */
1955
+ async upsertBoardObject(boardId, objectId, objectData) {
1956
+ try {
1957
+ const fullId = await this.resolveId(boardId);
1958
+ const current = await this.findById(fullId);
1959
+ if (!current) {
1960
+ throw new EntityNotFoundError("Board", boardId);
1961
+ }
1962
+ const updatedObjects = { ...current.objects || {}, [objectId]: objectData };
1963
+ return this.update(fullId, { objects: updatedObjects });
1964
+ } catch (error) {
1965
+ if (error instanceof RepositoryError) throw error;
1966
+ if (error instanceof EntityNotFoundError) throw error;
1967
+ throw new RepositoryError(
1968
+ `Failed to upsert board object: ${error instanceof Error ? error.message : String(error)}`,
1969
+ error
1970
+ );
1971
+ }
1972
+ }
1973
+ /**
1974
+ * Atomically remove a board object
1975
+ */
1976
+ async removeBoardObject(boardId, objectId) {
1977
+ try {
1978
+ const fullId = await this.resolveId(boardId);
1979
+ const current = await this.findById(fullId);
1980
+ if (!current) {
1981
+ throw new EntityNotFoundError("Board", boardId);
1982
+ }
1983
+ const updatedObjects = { ...current.objects || {} };
1984
+ delete updatedObjects[objectId];
1985
+ return this.update(fullId, { objects: updatedObjects });
1986
+ } catch (error) {
1987
+ if (error instanceof RepositoryError) throw error;
1988
+ if (error instanceof EntityNotFoundError) throw error;
1989
+ throw new RepositoryError(
1990
+ `Failed to remove board object: ${error instanceof Error ? error.message : String(error)}`,
1991
+ error
1992
+ );
1993
+ }
1994
+ }
1995
+ /**
1996
+ * Batch upsert multiple objects (sequential atomic updates)
1997
+ *
1998
+ * Note: Not a single transaction - each object is updated atomically.
1999
+ * This is safe for independent objects but may have partial failures.
2000
+ */
2001
+ async batchUpsertBoardObjects(boardId, objects) {
2002
+ try {
2003
+ for (const [objectId, objectData] of Object.entries(objects)) {
2004
+ await this.upsertBoardObject(boardId, objectId, objectData);
2005
+ }
2006
+ const fullId = await this.resolveId(boardId);
2007
+ const updated = await this.findById(fullId);
2008
+ if (!updated) {
2009
+ throw new RepositoryError("Failed to retrieve updated board");
2010
+ }
2011
+ return updated;
2012
+ } catch (error) {
2013
+ if (error instanceof RepositoryError) throw error;
2014
+ if (error instanceof EntityNotFoundError) throw error;
2015
+ throw new RepositoryError(
2016
+ `Failed to batch upsert board objects: ${error instanceof Error ? error.message : String(error)}`,
2017
+ error
2018
+ );
2019
+ }
2020
+ }
2021
+ /**
2022
+ * DEPRECATED: Delete a zone and handle associated sessions
2023
+ * TODO: Reimplement using board-objects table
2024
+ */
2025
+ async deleteZone(boardId, objectId, _deleteAssociatedSessions) {
2026
+ const updatedBoard = await this.removeBoardObject(boardId, objectId);
2027
+ return {
2028
+ board: updatedBoard,
2029
+ affectedSessions: []
2030
+ // No sessions to track yet
2031
+ };
2032
+ }
2033
+ };
2034
+
2035
+ // src/db/repositories/mcp-servers.ts
2036
+ var import_drizzle_orm6 = require("drizzle-orm");
2037
+ init_ids();
2038
+ var MCPServerRepository = class {
2039
+ constructor(db) {
2040
+ this.db = db;
2041
+ }
2042
+ /**
2043
+ * Convert database row to MCPServer type
2044
+ */
2045
+ rowToMCPServer(row) {
2046
+ return {
2047
+ mcp_server_id: row.mcp_server_id,
2048
+ name: row.name,
2049
+ transport: row.transport,
2050
+ scope: row.scope,
2051
+ enabled: Boolean(row.enabled),
2052
+ source: row.source,
2053
+ created_at: new Date(row.created_at),
2054
+ updated_at: row.updated_at ? new Date(row.updated_at) : new Date(row.created_at),
2055
+ // Optional fields from JSON data
2056
+ display_name: row.data.display_name,
2057
+ description: row.data.description,
2058
+ import_path: row.data.import_path,
2059
+ // Transport config
2060
+ command: row.data.command,
2061
+ args: row.data.args,
2062
+ url: row.data.url,
2063
+ env: row.data.env,
2064
+ // Scope foreign keys (nullable UUID strings - DB stores null, types expect undefined)
2065
+ owner_user_id: row.owner_user_id ?? void 0,
2066
+ team_id: row.team_id ?? void 0,
2067
+ repo_id: row.repo_id ?? void 0,
2068
+ session_id: row.session_id ?? void 0,
2069
+ // Capabilities
2070
+ tools: row.data.tools,
2071
+ resources: row.data.resources,
2072
+ prompts: row.data.prompts
2073
+ };
2074
+ }
2075
+ /**
2076
+ * Convert MCPServer to database insert format
2077
+ */
2078
+ mcpServerToInsert(data) {
2079
+ const now = Date.now();
2080
+ const serverId = "mcp_server_id" in data && data.mcp_server_id ? data.mcp_server_id : generateId();
2081
+ return {
2082
+ mcp_server_id: serverId,
2083
+ created_at: new Date(now),
2084
+ updated_at: new Date(now),
2085
+ // Materialized columns
2086
+ name: data.name,
2087
+ transport: data.transport,
2088
+ scope: data.scope,
2089
+ enabled: data.enabled ?? true,
2090
+ source: data.source ?? "user",
2091
+ // Scope foreign keys
2092
+ owner_user_id: data.owner_user_id ?? null,
2093
+ team_id: data.team_id ?? null,
2094
+ repo_id: data.repo_id ?? null,
2095
+ session_id: data.session_id ?? null,
2096
+ // JSON blob
2097
+ data: {
2098
+ display_name: data.display_name,
2099
+ description: data.description,
2100
+ import_path: data.import_path,
2101
+ command: data.command,
2102
+ args: data.args,
2103
+ url: data.url,
2104
+ env: data.env,
2105
+ tools: "tools" in data ? data.tools : void 0,
2106
+ resources: "resources" in data ? data.resources : void 0,
2107
+ prompts: "prompts" in data ? data.prompts : void 0
2108
+ }
2109
+ };
2110
+ }
2111
+ /**
2112
+ * Resolve short ID to full ID
2113
+ */
2114
+ async resolveId(id) {
2115
+ if (id.length === 36 && id.includes("-")) {
2116
+ return id;
2117
+ }
2118
+ const normalized = id.replace(/-/g, "").toLowerCase();
2119
+ const pattern = `${normalized}%`;
2120
+ const results = await this.db.select({ mcp_server_id: mcpServers.mcp_server_id }).from(mcpServers).where((0, import_drizzle_orm6.like)(mcpServers.mcp_server_id, pattern)).all();
2121
+ if (results.length === 0) {
2122
+ throw new EntityNotFoundError("MCPServer", id);
2123
+ }
2124
+ if (results.length > 1) {
2125
+ throw new AmbiguousIdError(
2126
+ "MCPServer",
2127
+ id,
2128
+ results.map((r) => formatShortId(r.mcp_server_id))
2129
+ );
2130
+ }
2131
+ return results[0].mcp_server_id;
2132
+ }
2133
+ /**
2134
+ * Create a new MCP server
2135
+ */
2136
+ async create(data) {
2137
+ try {
2138
+ const insert = this.mcpServerToInsert(data);
2139
+ await this.db.insert(mcpServers).values(insert);
2140
+ const row = await this.db.select().from(mcpServers).where((0, import_drizzle_orm6.eq)(mcpServers.mcp_server_id, insert.mcp_server_id)).get();
2141
+ if (!row) {
2142
+ throw new RepositoryError("Failed to retrieve created MCP server");
2143
+ }
2144
+ return this.rowToMCPServer(row);
2145
+ } catch (error) {
2146
+ if (error instanceof RepositoryError) throw error;
2147
+ throw new RepositoryError(
2148
+ `Failed to create MCP server: ${error instanceof Error ? error.message : String(error)}`,
2149
+ error
2150
+ );
2151
+ }
2152
+ }
2153
+ /**
2154
+ * Find MCP server by ID (supports short ID)
2155
+ */
2156
+ async findById(id) {
2157
+ try {
2158
+ const fullId = await this.resolveId(id);
2159
+ const row = await this.db.select().from(mcpServers).where((0, import_drizzle_orm6.eq)(mcpServers.mcp_server_id, fullId)).get();
2160
+ return row ? this.rowToMCPServer(row) : null;
2161
+ } catch (error) {
2162
+ if (error instanceof EntityNotFoundError) return null;
2163
+ if (error instanceof AmbiguousIdError) throw error;
2164
+ throw new RepositoryError(
2165
+ `Failed to find MCP server: ${error instanceof Error ? error.message : String(error)}`,
2166
+ error
2167
+ );
2168
+ }
2169
+ }
2170
+ /**
2171
+ * Find all MCP servers
2172
+ */
2173
+ async findAll(filters) {
2174
+ try {
2175
+ let query = this.db.select().from(mcpServers);
2176
+ const conditions = [];
2177
+ if (filters?.scope) {
2178
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.scope, filters.scope));
2179
+ }
2180
+ if (filters?.scopeId) {
2181
+ if (filters.scope === "global") {
2182
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.owner_user_id, filters.scopeId));
2183
+ } else if (filters.scope === "team") {
2184
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.team_id, filters.scopeId));
2185
+ } else if (filters.scope === "repo") {
2186
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.repo_id, filters.scopeId));
2187
+ } else if (filters.scope === "session") {
2188
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.session_id, filters.scopeId));
2189
+ }
2190
+ }
2191
+ if (filters?.transport) {
2192
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.transport, filters.transport));
2193
+ }
2194
+ if (filters?.enabled !== void 0) {
2195
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.enabled, filters.enabled));
2196
+ }
2197
+ if (filters?.source) {
2198
+ conditions.push((0, import_drizzle_orm6.eq)(mcpServers.source, filters.source));
2199
+ }
2200
+ if (conditions.length > 0) {
2201
+ query = query.where((0, import_drizzle_orm6.and)(...conditions));
2202
+ }
2203
+ const rows = await query.all();
2204
+ return rows.map((row) => this.rowToMCPServer(row));
2205
+ } catch (error) {
2206
+ throw new RepositoryError(
2207
+ `Failed to find MCP servers: ${error instanceof Error ? error.message : String(error)}`,
2208
+ error
2209
+ );
2210
+ }
2211
+ }
2212
+ /**
2213
+ * Find MCP servers by scope
2214
+ */
2215
+ async findByScope(scope, scopeId) {
2216
+ return this.findAll({ scope, scopeId });
2217
+ }
2218
+ /**
2219
+ * Update MCP server by ID
2220
+ */
2221
+ async update(id, updates) {
2222
+ try {
2223
+ const fullId = await this.resolveId(id);
2224
+ const current = await this.findById(fullId);
2225
+ if (!current) {
2226
+ throw new EntityNotFoundError("MCPServer", id);
2227
+ }
2228
+ const merged = { ...current, ...updates };
2229
+ const insert = this.mcpServerToInsert(merged);
2230
+ await this.db.update(mcpServers).set({
2231
+ enabled: insert.enabled,
2232
+ updated_at: /* @__PURE__ */ new Date(),
2233
+ data: insert.data
2234
+ }).where((0, import_drizzle_orm6.eq)(mcpServers.mcp_server_id, fullId));
2235
+ const updated = await this.findById(fullId);
2236
+ if (!updated) {
2237
+ throw new RepositoryError("Failed to retrieve updated MCP server");
2238
+ }
2239
+ return updated;
2240
+ } catch (error) {
2241
+ if (error instanceof RepositoryError) throw error;
2242
+ if (error instanceof EntityNotFoundError) throw error;
2243
+ throw new RepositoryError(
2244
+ `Failed to update MCP server: ${error instanceof Error ? error.message : String(error)}`,
2245
+ error
2246
+ );
2247
+ }
2248
+ }
2249
+ /**
2250
+ * Delete MCP server by ID
2251
+ */
2252
+ async delete(id) {
2253
+ try {
2254
+ const fullId = await this.resolveId(id);
2255
+ const result = await this.db.delete(mcpServers).where((0, import_drizzle_orm6.eq)(mcpServers.mcp_server_id, fullId)).run();
2256
+ if (result.rowsAffected === 0) {
2257
+ throw new EntityNotFoundError("MCPServer", id);
2258
+ }
2259
+ } catch (error) {
2260
+ if (error instanceof EntityNotFoundError) throw error;
2261
+ throw new RepositoryError(
2262
+ `Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
2263
+ error
2264
+ );
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Count total MCP servers
2269
+ */
2270
+ async count(filters) {
2271
+ try {
2272
+ const servers = await this.findAll(filters);
2273
+ return servers.length;
2274
+ } catch (error) {
2275
+ throw new RepositoryError(
2276
+ `Failed to count MCP servers: ${error instanceof Error ? error.message : String(error)}`,
2277
+ error
2278
+ );
2279
+ }
2280
+ }
2281
+ };
2282
+
2283
+ // src/db/repositories/messages.ts
2284
+ var import_drizzle_orm7 = require("drizzle-orm");
2285
+ var MessagesRepository = class {
2286
+ constructor(db) {
2287
+ this.db = db;
2288
+ }
2289
+ /**
2290
+ * Convert database row to Message type
2291
+ */
2292
+ rowToMessage(row) {
2293
+ return {
2294
+ message_id: row.message_id,
2295
+ session_id: row.session_id,
2296
+ task_id: row.task_id ? row.task_id : void 0,
2297
+ type: row.type,
2298
+ role: row.role,
2299
+ index: row.index,
2300
+ timestamp: new Date(row.timestamp).toISOString(),
2301
+ content_preview: row.content_preview || "",
2302
+ content: row.data.content,
2303
+ tool_uses: row.data.tool_uses,
2304
+ metadata: row.data.metadata
2305
+ };
2306
+ }
2307
+ /**
2308
+ * Convert Message to database row
2309
+ */
2310
+ messageToRow(message) {
2311
+ return {
2312
+ message_id: message.message_id,
2313
+ created_at: /* @__PURE__ */ new Date(),
2314
+ session_id: message.session_id,
2315
+ task_id: message.task_id,
2316
+ type: message.type,
2317
+ role: message.role,
2318
+ index: message.index,
2319
+ timestamp: new Date(message.timestamp),
2320
+ content_preview: message.content_preview,
2321
+ data: {
2322
+ content: message.content,
2323
+ tool_uses: message.tool_uses,
2324
+ metadata: message.metadata
2325
+ }
2326
+ };
2327
+ }
2328
+ /**
2329
+ * Create a single message
2330
+ */
2331
+ async create(message) {
2332
+ const row = this.messageToRow(message);
2333
+ const [inserted] = await this.db.insert(messages).values(row).returning();
2334
+ return this.rowToMessage(inserted);
2335
+ }
2336
+ /**
2337
+ * Bulk insert messages (optimized for session loading)
2338
+ */
2339
+ async createMany(messageList) {
2340
+ const rows = messageList.map((m) => this.messageToRow(m));
2341
+ const inserted = await this.db.insert(messages).values(rows).returning();
2342
+ return inserted.map((r) => this.rowToMessage(r));
2343
+ }
2344
+ /**
2345
+ * Get message by ID
2346
+ */
2347
+ async findById(messageId) {
2348
+ const rows = await this.db.select().from(messages).where((0, import_drizzle_orm7.eq)(messages.message_id, messageId)).limit(1);
2349
+ return rows[0] ? this.rowToMessage(rows[0]) : null;
2350
+ }
2351
+ /**
2352
+ * Get all messages (used by FeathersJS service adapter)
2353
+ */
2354
+ async findAll() {
2355
+ const rows = await this.db.select().from(messages).orderBy(messages.index);
2356
+ return rows.map((r) => this.rowToMessage(r));
2357
+ }
2358
+ /**
2359
+ * Get all messages for a session (ordered by index)
2360
+ */
2361
+ async findBySessionId(sessionId) {
2362
+ const rows = await this.db.select().from(messages).where((0, import_drizzle_orm7.eq)(messages.session_id, sessionId)).orderBy(messages.index);
2363
+ return rows.map((r) => this.rowToMessage(r));
2364
+ }
2365
+ /**
2366
+ * Get all messages for a task (ordered by index)
2367
+ */
2368
+ async findByTaskId(taskId) {
2369
+ const rows = await this.db.select().from(messages).where((0, import_drizzle_orm7.eq)(messages.task_id, taskId)).orderBy(messages.index);
2370
+ return rows.map((r) => this.rowToMessage(r));
2371
+ }
2372
+ /**
2373
+ * Get messages in a range for a session
2374
+ * Used for task message_range queries
2375
+ */
2376
+ async findByRange(sessionId, startIndex, endIndex) {
2377
+ const rows = await this.db.select().from(messages).where((0, import_drizzle_orm7.eq)(messages.session_id, sessionId)).orderBy(messages.index);
2378
+ return rows.filter((r) => r.index >= startIndex && r.index <= endIndex).map((r) => this.rowToMessage(r));
2379
+ }
2380
+ /**
2381
+ * Update message (used by FeathersJS service adapter)
2382
+ */
2383
+ async update(messageId, updates) {
2384
+ const existing = await this.findById(messageId);
2385
+ if (!existing) {
2386
+ throw new Error(`Message ${messageId} not found`);
2387
+ }
2388
+ const updated = { ...existing, ...updates };
2389
+ const row = this.messageToRow(updated);
2390
+ const [result] = await this.db.update(messages).set(row).where((0, import_drizzle_orm7.eq)(messages.message_id, messageId)).returning();
2391
+ return this.rowToMessage(result);
2392
+ }
2393
+ /**
2394
+ * Update message task assignment
2395
+ */
2396
+ async assignToTask(messageId, taskId) {
2397
+ const [updated] = await this.db.update(messages).set({ task_id: taskId }).where((0, import_drizzle_orm7.eq)(messages.message_id, messageId)).returning();
2398
+ return this.rowToMessage(updated);
2399
+ }
2400
+ /**
2401
+ * Delete all messages for a session (cascades automatically via FK)
2402
+ */
2403
+ async deleteBySessionId(sessionId) {
2404
+ await this.db.delete(messages).where((0, import_drizzle_orm7.eq)(messages.session_id, sessionId));
2405
+ }
2406
+ /**
2407
+ * Delete a single message
2408
+ */
2409
+ async delete(messageId) {
2410
+ await this.db.delete(messages).where((0, import_drizzle_orm7.eq)(messages.message_id, messageId));
2411
+ }
2412
+ };
2413
+
2414
+ // src/db/repositories/repos.ts
2415
+ var import_drizzle_orm8 = require("drizzle-orm");
2416
+ init_ids();
2417
+ var RepoRepository = class {
2418
+ constructor(db) {
2419
+ this.db = db;
2420
+ }
2421
+ /**
2422
+ * Convert database row to Repo type
2423
+ */
2424
+ rowToRepo(row) {
2425
+ return {
2426
+ repo_id: row.repo_id,
2427
+ slug: row.slug,
2428
+ created_at: new Date(row.created_at).toISOString(),
2429
+ last_updated: row.updated_at ? new Date(row.updated_at).toISOString() : new Date(row.created_at).toISOString(),
2430
+ ...row.data
2431
+ };
2432
+ }
2433
+ /**
2434
+ * Convert Repo to database insert format
2435
+ */
2436
+ repoToInsert(repo) {
2437
+ const now = Date.now();
2438
+ const repoId = repo.repo_id ?? generateId();
2439
+ if (!repo.slug) {
2440
+ throw new RepositoryError("slug is required when creating a repo");
2441
+ }
2442
+ if (!repo.remote_url) {
2443
+ throw new RepositoryError("Repo must have a remote_url");
2444
+ }
2445
+ return {
2446
+ repo_id: repoId,
2447
+ slug: repo.slug,
2448
+ created_at: new Date(repo.created_at ?? now),
2449
+ updated_at: repo.last_updated ? new Date(repo.last_updated) : new Date(now),
2450
+ data: {
2451
+ name: repo.name ?? repo.slug,
2452
+ remote_url: repo.remote_url,
2453
+ local_path: repo.local_path ?? "",
2454
+ default_branch: repo.default_branch,
2455
+ environment_config: repo.environment_config
2456
+ }
2457
+ };
2458
+ }
2459
+ /**
2460
+ * Resolve short ID to full ID
2461
+ */
2462
+ async resolveId(id) {
2463
+ if (id.length === 36 && id.includes("-")) {
2464
+ return id;
2465
+ }
2466
+ const normalized = id.replace(/-/g, "").toLowerCase();
2467
+ const pattern = `${normalized}%`;
2468
+ const results = await this.db.select({ repo_id: repos.repo_id }).from(repos).where((0, import_drizzle_orm8.like)(repos.repo_id, pattern)).all();
2469
+ if (results.length === 0) {
2470
+ throw new EntityNotFoundError("Repo", id);
2471
+ }
2472
+ if (results.length > 1) {
2473
+ throw new AmbiguousIdError(
2474
+ "Repo",
2475
+ id,
2476
+ results.map((r) => formatShortId(r.repo_id))
2477
+ );
2478
+ }
2479
+ return results[0].repo_id;
2480
+ }
2481
+ /**
2482
+ * Create a new repo
2483
+ */
2484
+ async create(data) {
2485
+ try {
2486
+ const insert = this.repoToInsert(data);
2487
+ await this.db.insert(repos).values(insert);
2488
+ const row = await this.db.select().from(repos).where((0, import_drizzle_orm8.eq)(repos.repo_id, insert.repo_id)).get();
2489
+ if (!row) {
2490
+ throw new RepositoryError("Failed to retrieve created repo");
2491
+ }
2492
+ return this.rowToRepo(row);
2493
+ } catch (error) {
2494
+ if (error instanceof RepositoryError) throw error;
2495
+ throw new RepositoryError(
2496
+ `Failed to create repo: ${error instanceof Error ? error.message : String(error)}`,
2497
+ error
2498
+ );
2499
+ }
2500
+ }
2501
+ /**
2502
+ * Find repo by ID (supports short ID)
2503
+ */
2504
+ async findById(id) {
2505
+ try {
2506
+ const fullId = await this.resolveId(id);
2507
+ const row = await this.db.select().from(repos).where((0, import_drizzle_orm8.eq)(repos.repo_id, fullId)).get();
2508
+ return row ? this.rowToRepo(row) : null;
2509
+ } catch (error) {
2510
+ if (error instanceof EntityNotFoundError) return null;
2511
+ if (error instanceof AmbiguousIdError) throw error;
2512
+ throw new RepositoryError(
2513
+ `Failed to find repo: ${error instanceof Error ? error.message : String(error)}`,
2514
+ error
2515
+ );
2516
+ }
2517
+ }
2518
+ /**
2519
+ * Find repo by slug (exact match)
2520
+ */
2521
+ async findBySlug(slug) {
2522
+ try {
2523
+ const row = await this.db.select().from(repos).where((0, import_drizzle_orm8.eq)(repos.slug, slug)).get();
2524
+ return row ? this.rowToRepo(row) : null;
2525
+ } catch (error) {
2526
+ throw new RepositoryError(
2527
+ `Failed to find repo by slug: ${error instanceof Error ? error.message : String(error)}`,
2528
+ error
2529
+ );
2530
+ }
2531
+ }
2532
+ /**
2533
+ * Find all repos
2534
+ */
2535
+ async findAll() {
2536
+ try {
2537
+ const rows = await this.db.select().from(repos).all();
2538
+ return rows.map((row) => this.rowToRepo(row));
2539
+ } catch (error) {
2540
+ throw new RepositoryError(
2541
+ `Failed to find all repos: ${error instanceof Error ? error.message : String(error)}`,
2542
+ error
2543
+ );
2544
+ }
2545
+ }
2546
+ /**
2547
+ * Find managed repos only (DEPRECATED: all repos are managed now)
2548
+ *
2549
+ * Kept for backwards compatibility - returns all repos.
2550
+ */
2551
+ async findManaged() {
2552
+ return this.findAll();
2553
+ }
2554
+ /**
2555
+ * Update repo by ID
2556
+ */
2557
+ async update(id, updates) {
2558
+ try {
2559
+ const fullId = await this.resolveId(id);
2560
+ const current = await this.findById(fullId);
2561
+ if (!current) {
2562
+ throw new EntityNotFoundError("Repo", id);
2563
+ }
2564
+ const merged = { ...current, ...updates };
2565
+ const insert = this.repoToInsert(merged);
2566
+ await this.db.update(repos).set({
2567
+ slug: insert.slug,
2568
+ updated_at: /* @__PURE__ */ new Date(),
2569
+ data: insert.data
2570
+ }).where((0, import_drizzle_orm8.eq)(repos.repo_id, fullId));
2571
+ const updated = await this.findById(fullId);
2572
+ if (!updated) {
2573
+ throw new RepositoryError("Failed to retrieve updated repo");
2574
+ }
2575
+ return updated;
2576
+ } catch (error) {
2577
+ if (error instanceof RepositoryError) throw error;
2578
+ if (error instanceof EntityNotFoundError) throw error;
2579
+ throw new RepositoryError(
2580
+ `Failed to update repo: ${error instanceof Error ? error.message : String(error)}`,
2581
+ error
2582
+ );
2583
+ }
2584
+ }
2585
+ /**
2586
+ * Delete repo by ID
2587
+ */
2588
+ async delete(id) {
2589
+ try {
2590
+ const fullId = await this.resolveId(id);
2591
+ const result = await this.db.delete(repos).where((0, import_drizzle_orm8.eq)(repos.repo_id, fullId)).run();
2592
+ if (result.rowsAffected === 0) {
2593
+ throw new EntityNotFoundError("Repo", id);
2594
+ }
2595
+ } catch (error) {
2596
+ if (error instanceof EntityNotFoundError) throw error;
2597
+ throw new RepositoryError(
2598
+ `Failed to delete repo: ${error instanceof Error ? error.message : String(error)}`,
2599
+ error
2600
+ );
2601
+ }
2602
+ }
2603
+ /**
2604
+ * @deprecated Worktrees are now first-class entities in their own table.
2605
+ * Use WorktreeRepository instead.
2606
+ */
2607
+ async addWorktree() {
2608
+ throw new Error("addWorktree is deprecated. Use WorktreeRepository.create() instead.");
2609
+ }
2610
+ /**
2611
+ * @deprecated Worktrees are now first-class entities in their own table.
2612
+ * Use WorktreeRepository instead.
2613
+ */
2614
+ async removeWorktree() {
2615
+ throw new Error("removeWorktree is deprecated. Use WorktreeRepository.delete() instead.");
2616
+ }
2617
+ /**
2618
+ * Count total repos
2619
+ */
2620
+ async count() {
2621
+ try {
2622
+ const result = await this.db.select({ count: import_drizzle_orm8.sql`count(*)` }).from(repos).get();
2623
+ return result?.count ?? 0;
2624
+ } catch (error) {
2625
+ throw new RepositoryError(
2626
+ `Failed to count repos: ${error instanceof Error ? error.message : String(error)}`,
2627
+ error
2628
+ );
2629
+ }
2630
+ }
2631
+ };
2632
+
2633
+ // src/db/repositories/session-mcp-servers.ts
2634
+ var import_drizzle_orm10 = require("drizzle-orm");
2635
+
2636
+ // src/types/session.ts
2637
+ var SessionStatus = {
2638
+ IDLE: "idle",
2639
+ RUNNING: "running",
2640
+ COMPLETED: "completed",
2641
+ FAILED: "failed"
2642
+ };
2643
+
2644
+ // src/types/task.ts
2645
+ var TaskStatus = {
2646
+ CREATED: "created",
2647
+ RUNNING: "running",
2648
+ STOPPING: "stopping",
2649
+ // Stop requested, waiting for SDK to halt
2650
+ AWAITING_PERMISSION: "awaiting_permission",
2651
+ COMPLETED: "completed",
2652
+ FAILED: "failed",
2653
+ STOPPED: "stopped"
2654
+ // User-requested stop (distinct from failed)
2655
+ };
2656
+
2657
+ // src/db/repositories/sessions.ts
2658
+ var import_drizzle_orm9 = require("drizzle-orm");
2659
+ init_ids();
2660
+ var SessionRepository = class {
2661
+ constructor(db) {
2662
+ this.db = db;
2663
+ }
2664
+ /**
2665
+ * Convert database row to Session type
2666
+ */
2667
+ rowToSession(row) {
2668
+ const genealogyData = row.data.genealogy || { children: [] };
2669
+ return {
2670
+ session_id: row.session_id,
2671
+ status: row.status,
2672
+ agentic_tool: row.agentic_tool,
2673
+ created_at: new Date(row.created_at).toISOString(),
2674
+ last_updated: row.updated_at ? new Date(row.updated_at).toISOString() : new Date(row.created_at).toISOString(),
2675
+ created_by: row.created_by,
2676
+ worktree_id: row.worktree_id,
2677
+ ...row.data,
2678
+ tasks: row.data.tasks.map((id) => id),
2679
+ genealogy: {
2680
+ parent_session_id: row.parent_session_id,
2681
+ forked_from_session_id: row.forked_from_session_id,
2682
+ fork_point_task_id: genealogyData.fork_point_task_id,
2683
+ spawn_point_task_id: genealogyData.spawn_point_task_id,
2684
+ children: genealogyData.children.map((id) => id)
2685
+ },
2686
+ permission_config: row.data.permission_config
2687
+ };
2688
+ }
2689
+ /**
2690
+ * Convert Session to database insert format
2691
+ */
2692
+ sessionToInsert(session) {
2693
+ const now = Date.now();
2694
+ const sessionId = session.session_id ?? generateId();
2695
+ if (!session.worktree_id) {
2696
+ throw new RepositoryError("Session must have a worktree_id");
2697
+ }
2698
+ return {
2699
+ session_id: sessionId,
2700
+ created_at: new Date(session.created_at ? session.created_at : now),
2701
+ updated_at: session.last_updated ? new Date(session.last_updated) : new Date(now),
2702
+ status: session.status ?? SessionStatus.IDLE,
2703
+ agentic_tool: session.agentic_tool ?? "claude-code",
2704
+ created_by: session.created_by ?? "anonymous",
2705
+ board_id: null,
2706
+ // Board ID tracked separately in boards.sessions array
2707
+ parent_session_id: session.genealogy?.parent_session_id ?? null,
2708
+ forked_from_session_id: session.genealogy?.forked_from_session_id ?? null,
2709
+ worktree_id: session.worktree_id,
2710
+ data: {
2711
+ agentic_tool_version: session.agentic_tool_version,
2712
+ sdk_session_id: session.sdk_session_id,
2713
+ // Preserve SDK session ID for conversation continuity
2714
+ mcp_token: session.mcp_token,
2715
+ // MCP authentication token for Agor self-access
2716
+ title: session.title,
2717
+ description: session.description,
2718
+ git_state: session.git_state ?? {
2719
+ ref: "main",
2720
+ base_sha: "",
2721
+ current_sha: ""
2722
+ },
2723
+ genealogy: session.genealogy ?? {
2724
+ children: []
2725
+ },
2726
+ contextFiles: session.contextFiles ?? [],
2727
+ tasks: session.tasks ?? [],
2728
+ message_count: session.message_count ?? 0,
2729
+ tool_use_count: session.tool_use_count ?? 0,
2730
+ permission_config: session.permission_config,
2731
+ model_config: session.model_config,
2732
+ custom_context: session.custom_context
2733
+ }
2734
+ };
2735
+ }
2736
+ /**
2737
+ * Resolve short ID to full ID
2738
+ */
2739
+ async resolveId(id) {
2740
+ if (id.length === 36 && id.includes("-")) {
2741
+ return id;
2742
+ }
2743
+ const normalized = id.replace(/-/g, "").toLowerCase();
2744
+ const pattern = `${normalized}%`;
2745
+ const results = await this.db.select({ session_id: sessions.session_id }).from(sessions).where((0, import_drizzle_orm9.like)(sessions.session_id, pattern)).all();
2746
+ if (results.length === 0) {
2747
+ throw new EntityNotFoundError("Session", id);
2748
+ }
2749
+ if (results.length > 1) {
2750
+ throw new AmbiguousIdError(
2751
+ "Session",
2752
+ id,
2753
+ results.map((r) => formatShortId(r.session_id))
2754
+ );
2755
+ }
2756
+ return results[0].session_id;
2757
+ }
2758
+ /**
2759
+ * Create a new session
2760
+ */
2761
+ async create(data) {
2762
+ try {
2763
+ const insert = this.sessionToInsert(data);
2764
+ await this.db.insert(sessions).values(insert);
2765
+ const row = await this.db.select().from(sessions).where((0, import_drizzle_orm9.eq)(sessions.session_id, insert.session_id)).get();
2766
+ if (!row) {
2767
+ throw new RepositoryError("Failed to retrieve created session");
2768
+ }
2769
+ return this.rowToSession(row);
2770
+ } catch (error) {
2771
+ if (error instanceof RepositoryError) throw error;
2772
+ throw new RepositoryError(
2773
+ `Failed to create session: ${error instanceof Error ? error.message : String(error)}`,
2774
+ error
2775
+ );
2776
+ }
2777
+ }
2778
+ /**
2779
+ * Find session by ID (supports short ID)
2780
+ */
2781
+ async findById(id) {
2782
+ try {
2783
+ const fullId = await this.resolveId(id);
2784
+ const row = await this.db.select().from(sessions).where((0, import_drizzle_orm9.eq)(sessions.session_id, fullId)).get();
2785
+ return row ? this.rowToSession(row) : null;
2786
+ } catch (error) {
2787
+ if (error instanceof EntityNotFoundError) return null;
2788
+ if (error instanceof AmbiguousIdError) throw error;
2789
+ throw new RepositoryError(
2790
+ `Failed to find session: ${error instanceof Error ? error.message : String(error)}`,
2791
+ error
2792
+ );
2793
+ }
2794
+ }
2795
+ /**
2796
+ * Find all sessions
2797
+ */
2798
+ async findAll() {
2799
+ try {
2800
+ const rows = await this.db.select().from(sessions).all();
2801
+ return rows.map((row) => this.rowToSession(row));
2802
+ } catch (error) {
2803
+ throw new RepositoryError(
2804
+ `Failed to find all sessions: ${error instanceof Error ? error.message : String(error)}`,
2805
+ error
2806
+ );
2807
+ }
2808
+ }
2809
+ /**
2810
+ * Find sessions by status
2811
+ */
2812
+ async findByStatus(status) {
2813
+ try {
2814
+ const rows = await this.db.select().from(sessions).where((0, import_drizzle_orm9.eq)(sessions.status, status)).all();
2815
+ return rows.map((row) => this.rowToSession(row));
2816
+ } catch (error) {
2817
+ throw new RepositoryError(
2818
+ `Failed to find sessions by status: ${error instanceof Error ? error.message : String(error)}`,
2819
+ error
2820
+ );
2821
+ }
2822
+ }
2823
+ /**
2824
+ * Find sessions by board ID
2825
+ */
2826
+ async findByBoard(_boardId) {
2827
+ try {
2828
+ const rows = await this.db.select().from(sessions).all();
2829
+ return rows.map((row) => this.rowToSession(row));
2830
+ } catch (error) {
2831
+ throw new RepositoryError(
2832
+ `Failed to find sessions by board: ${error instanceof Error ? error.message : String(error)}`,
2833
+ error
2834
+ );
2835
+ }
2836
+ }
2837
+ /**
2838
+ * Find child sessions (forked or spawned from this session)
2839
+ */
2840
+ async findChildren(sessionId) {
2841
+ try {
2842
+ const fullId = await this.resolveId(sessionId);
2843
+ const rows = await this.db.select().from(sessions).where(
2844
+ (0, import_drizzle_orm9.or)(
2845
+ import_drizzle_orm9.sql`json_extract(${sessions.data}, '$.genealogy.parent_session_id') = ${fullId}`,
2846
+ import_drizzle_orm9.sql`json_extract(${sessions.data}, '$.genealogy.forked_from_session_id') = ${fullId}`
2847
+ )
2848
+ ).all();
2849
+ return rows.map((row) => this.rowToSession(row));
2850
+ } catch (error) {
2851
+ throw new RepositoryError(
2852
+ `Failed to find child sessions: ${error instanceof Error ? error.message : String(error)}`,
2853
+ error
2854
+ );
2855
+ }
2856
+ }
2857
+ /**
2858
+ * Find ancestor sessions (parent chain)
2859
+ */
2860
+ async findAncestors(sessionId) {
2861
+ try {
2862
+ const fullId = await this.resolveId(sessionId);
2863
+ const ancestors = [];
2864
+ let currentSession = await this.findById(fullId);
2865
+ while (currentSession) {
2866
+ const parentId = currentSession.genealogy?.parent_session_id || currentSession.genealogy?.forked_from_session_id;
2867
+ if (!parentId) break;
2868
+ const parent = await this.findById(parentId);
2869
+ if (!parent) break;
2870
+ ancestors.push(parent);
2871
+ currentSession = parent;
2872
+ }
2873
+ return ancestors;
2874
+ } catch (error) {
2875
+ throw new RepositoryError(
2876
+ `Failed to find ancestor sessions: ${error instanceof Error ? error.message : String(error)}`,
2877
+ error
2878
+ );
2879
+ }
2880
+ }
2881
+ /**
2882
+ * Update session by ID
2883
+ */
2884
+ async update(id, updates) {
2885
+ try {
2886
+ const fullId = await this.resolveId(id);
2887
+ const current = await this.findById(fullId);
2888
+ if (!current) {
2889
+ throw new EntityNotFoundError("Session", id);
2890
+ }
2891
+ const merged = {
2892
+ ...current,
2893
+ ...updates
2894
+ };
2895
+ if (updates.permission_config) {
2896
+ console.log(`\u{1F4DD} [SessionRepository] Merging permission_config update`);
2897
+ console.log(
2898
+ ` Before merge - current.permission_config: ${JSON.stringify(current.permission_config)}`
2899
+ );
2900
+ console.log(` Update permission_config: ${JSON.stringify(updates.permission_config)}`);
2901
+ console.log(
2902
+ ` After merge - merged.permission_config: ${JSON.stringify(merged.permission_config)}`
2903
+ );
2904
+ }
2905
+ const insert = this.sessionToInsert(merged);
2906
+ console.log(`\u{1F5C4}\uFE0F [SessionRepository] Writing to DB:`);
2907
+ console.log(
2908
+ ` insert.data.permission_config: ${JSON.stringify(insert.data.permission_config)}`
2909
+ );
2910
+ await this.db.update(sessions).set({
2911
+ status: insert.status,
2912
+ updated_at: /* @__PURE__ */ new Date(),
2913
+ data: insert.data
2914
+ }).where((0, import_drizzle_orm9.eq)(sessions.session_id, fullId));
2915
+ console.log(`\u2705 [SessionRepository] DB update complete`);
2916
+ const updated = await this.findById(fullId);
2917
+ if (!updated) {
2918
+ throw new RepositoryError("Failed to retrieve updated session");
2919
+ }
2920
+ return updated;
2921
+ } catch (error) {
2922
+ if (error instanceof RepositoryError) throw error;
2923
+ if (error instanceof EntityNotFoundError) throw error;
2924
+ throw new RepositoryError(
2925
+ `Failed to update session: ${error instanceof Error ? error.message : String(error)}`,
2926
+ error
2927
+ );
2928
+ }
2929
+ }
2930
+ /**
2931
+ * Delete session by ID
2932
+ */
2933
+ async delete(id) {
2934
+ try {
2935
+ const fullId = await this.resolveId(id);
2936
+ const result = await this.db.delete(sessions).where((0, import_drizzle_orm9.eq)(sessions.session_id, fullId)).run();
2937
+ if (result.rowsAffected === 0) {
2938
+ throw new EntityNotFoundError("Session", id);
2939
+ }
2940
+ } catch (error) {
2941
+ if (error instanceof EntityNotFoundError) throw error;
2942
+ throw new RepositoryError(
2943
+ `Failed to delete session: ${error instanceof Error ? error.message : String(error)}`,
2944
+ error
2945
+ );
2946
+ }
2947
+ }
2948
+ /**
2949
+ * Find sessions with running tasks
2950
+ */
2951
+ async findRunning() {
2952
+ return this.findByStatus(SessionStatus.RUNNING);
2953
+ }
2954
+ /**
2955
+ * Count total sessions
2956
+ */
2957
+ async count() {
2958
+ try {
2959
+ const result = await this.db.select({ count: import_drizzle_orm9.sql`count(*)` }).from(sessions).get();
2960
+ return result?.count ?? 0;
2961
+ } catch (error) {
2962
+ throw new RepositoryError(
2963
+ `Failed to count sessions: ${error instanceof Error ? error.message : String(error)}`,
2964
+ error
2965
+ );
2966
+ }
2967
+ }
2968
+ };
2969
+
2970
+ // src/db/repositories/session-mcp-servers.ts
2971
+ var SessionMCPServerRepository = class {
2972
+ constructor(db) {
2973
+ this.db = db;
2974
+ this.sessionRepo = new SessionRepository(db);
2975
+ this.mcpServerRepo = new MCPServerRepository(db);
2976
+ }
2977
+ sessionRepo;
2978
+ mcpServerRepo;
2979
+ /**
2980
+ * Add MCP server to session
2981
+ */
2982
+ async addServer(sessionId, serverId) {
2983
+ try {
2984
+ const session = await this.sessionRepo.findById(sessionId);
2985
+ if (!session) {
2986
+ throw new EntityNotFoundError("Session", sessionId);
2987
+ }
2988
+ const server = await this.mcpServerRepo.findById(serverId);
2989
+ if (!server) {
2990
+ throw new EntityNotFoundError("MCPServer", serverId);
2991
+ }
2992
+ const existing = await this.db.select().from(sessionMcpServers).where(
2993
+ (0, import_drizzle_orm10.and)(
2994
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId),
2995
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.mcp_server_id, serverId)
2996
+ )
2997
+ ).get();
2998
+ if (existing) {
2999
+ await this.db.update(sessionMcpServers).set({ enabled: true }).where(
3000
+ (0, import_drizzle_orm10.and)(
3001
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId),
3002
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.mcp_server_id, serverId)
3003
+ )
3004
+ );
3005
+ return;
3006
+ }
3007
+ const insert = {
3008
+ session_id: sessionId,
3009
+ mcp_server_id: serverId,
3010
+ enabled: true,
3011
+ added_at: /* @__PURE__ */ new Date()
3012
+ };
3013
+ await this.db.insert(sessionMcpServers).values(insert);
3014
+ } catch (error) {
3015
+ if (error instanceof EntityNotFoundError) throw error;
3016
+ throw new RepositoryError(
3017
+ `Failed to add MCP server to session: ${error instanceof Error ? error.message : String(error)}`,
3018
+ error
3019
+ );
3020
+ }
3021
+ }
3022
+ /**
3023
+ * Remove MCP server from session
3024
+ */
3025
+ async removeServer(sessionId, serverId) {
3026
+ try {
3027
+ const result = await this.db.delete(sessionMcpServers).where(
3028
+ (0, import_drizzle_orm10.and)(
3029
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId),
3030
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.mcp_server_id, serverId)
3031
+ )
3032
+ ).run();
3033
+ if (result.rowsAffected === 0) {
3034
+ throw new EntityNotFoundError("SessionMCPServer", `${sessionId}/${serverId}`);
3035
+ }
3036
+ } catch (error) {
3037
+ if (error instanceof EntityNotFoundError) throw error;
3038
+ throw new RepositoryError(
3039
+ `Failed to remove MCP server from session: ${error instanceof Error ? error.message : String(error)}`,
3040
+ error
3041
+ );
3042
+ }
3043
+ }
3044
+ /**
3045
+ * Toggle MCP server enabled state for session
3046
+ */
3047
+ async toggleServer(sessionId, serverId, enabled) {
3048
+ try {
3049
+ const result = await this.db.update(sessionMcpServers).set({ enabled }).where(
3050
+ (0, import_drizzle_orm10.and)(
3051
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId),
3052
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.mcp_server_id, serverId)
3053
+ )
3054
+ ).run();
3055
+ if (result.rowsAffected === 0) {
3056
+ throw new EntityNotFoundError("SessionMCPServer", `${sessionId}/${serverId}`);
3057
+ }
3058
+ } catch (error) {
3059
+ if (error instanceof EntityNotFoundError) throw error;
3060
+ throw new RepositoryError(
3061
+ `Failed to toggle MCP server: ${error instanceof Error ? error.message : String(error)}`,
3062
+ error
3063
+ );
3064
+ }
3065
+ }
3066
+ /**
3067
+ * List MCP servers for a session
3068
+ */
3069
+ async listServers(sessionId, enabledOnly = false) {
3070
+ try {
3071
+ const conditions = [(0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId)];
3072
+ if (enabledOnly) {
3073
+ conditions.push((0, import_drizzle_orm10.eq)(sessionMcpServers.enabled, true));
3074
+ }
3075
+ const relationships = await this.db.select().from(sessionMcpServers).where((0, import_drizzle_orm10.and)(...conditions)).all();
3076
+ const servers = [];
3077
+ for (const rel of relationships) {
3078
+ const server = await this.mcpServerRepo.findById(rel.mcp_server_id);
3079
+ if (server) {
3080
+ servers.push(server);
3081
+ }
3082
+ }
3083
+ return servers;
3084
+ } catch (error) {
3085
+ throw new RepositoryError(
3086
+ `Failed to list MCP servers for session: ${error instanceof Error ? error.message : String(error)}`,
3087
+ error
3088
+ );
3089
+ }
3090
+ }
3091
+ /**
3092
+ * Set MCP servers for a session (bulk operation)
3093
+ * Replaces existing relationships with new ones
3094
+ */
3095
+ async setServers(sessionId, serverIds) {
3096
+ try {
3097
+ const session = await this.sessionRepo.findById(sessionId);
3098
+ if (!session) {
3099
+ throw new EntityNotFoundError("Session", sessionId);
3100
+ }
3101
+ await this.db.delete(sessionMcpServers).where((0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId));
3102
+ if (serverIds.length > 0) {
3103
+ const inserts = serverIds.map((serverId) => ({
3104
+ session_id: sessionId,
3105
+ mcp_server_id: serverId,
3106
+ enabled: true,
3107
+ added_at: /* @__PURE__ */ new Date()
3108
+ }));
3109
+ await this.db.insert(sessionMcpServers).values(inserts);
3110
+ }
3111
+ } catch (error) {
3112
+ if (error instanceof EntityNotFoundError) throw error;
3113
+ throw new RepositoryError(
3114
+ `Failed to set MCP servers for session: ${error instanceof Error ? error.message : String(error)}`,
3115
+ error
3116
+ );
3117
+ }
3118
+ }
3119
+ /**
3120
+ * Get relationship details
3121
+ */
3122
+ async getRelationship(sessionId, serverId) {
3123
+ try {
3124
+ const row = await this.db.select().from(sessionMcpServers).where(
3125
+ (0, import_drizzle_orm10.and)(
3126
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.session_id, sessionId),
3127
+ (0, import_drizzle_orm10.eq)(sessionMcpServers.mcp_server_id, serverId)
3128
+ )
3129
+ ).get();
3130
+ if (!row) {
3131
+ return null;
3132
+ }
3133
+ return {
3134
+ session_id: row.session_id,
3135
+ mcp_server_id: row.mcp_server_id,
3136
+ enabled: Boolean(row.enabled),
3137
+ added_at: new Date(row.added_at)
3138
+ };
3139
+ } catch (error) {
3140
+ throw new RepositoryError(
3141
+ `Failed to get relationship: ${error instanceof Error ? error.message : String(error)}`,
3142
+ error
3143
+ );
3144
+ }
3145
+ }
3146
+ /**
3147
+ * Count MCP servers for a session
3148
+ */
3149
+ async count(sessionId, enabledOnly = false) {
3150
+ try {
3151
+ const servers = await this.listServers(sessionId, enabledOnly);
3152
+ return servers.length;
3153
+ } catch (error) {
3154
+ throw new RepositoryError(
3155
+ `Failed to count MCP servers: ${error instanceof Error ? error.message : String(error)}`,
3156
+ error
3157
+ );
3158
+ }
3159
+ }
3160
+ };
3161
+
3162
+ // src/db/repositories/tasks.ts
3163
+ var import_drizzle_orm11 = require("drizzle-orm");
3164
+ init_ids();
3165
+ var TaskRepository = class {
3166
+ constructor(db) {
3167
+ this.db = db;
3168
+ }
3169
+ /**
3170
+ * Convert database row to Task type
3171
+ */
3172
+ rowToTask(row) {
3173
+ return {
3174
+ task_id: row.task_id,
3175
+ session_id: row.session_id,
3176
+ status: row.status,
3177
+ created_at: new Date(row.created_at).toISOString(),
3178
+ completed_at: row.completed_at ? new Date(row.completed_at).toISOString() : void 0,
3179
+ created_by: row.created_by,
3180
+ ...row.data
3181
+ };
3182
+ }
3183
+ /**
3184
+ * Convert Task to database insert format
3185
+ */
3186
+ taskToInsert(task) {
3187
+ const now = Date.now();
3188
+ const taskId = task.task_id ?? generateId();
3189
+ if (!task.session_id) {
3190
+ throw new RepositoryError("session_id is required when creating a task");
3191
+ }
3192
+ const git_state = task.git_state ?? {
3193
+ ref_at_start: "unknown",
3194
+ sha_at_start: "unknown"
3195
+ };
3196
+ return {
3197
+ task_id: taskId,
3198
+ session_id: task.session_id,
3199
+ created_at: new Date(now),
3200
+ // Always use server timestamp, ignore client-provided value
3201
+ completed_at: task.completed_at ? new Date(task.completed_at) : void 0,
3202
+ status: task.status ?? TaskStatus.CREATED,
3203
+ created_by: task.created_by ?? "anonymous",
3204
+ data: {
3205
+ description: task.description ?? "",
3206
+ full_prompt: task.full_prompt ?? task.description ?? "",
3207
+ message_range: task.message_range ?? {
3208
+ start_index: 0,
3209
+ end_index: 0,
3210
+ start_timestamp: new Date(now).toISOString()
3211
+ },
3212
+ git_state,
3213
+ model: task.model ?? "claude-sonnet-4-5",
3214
+ tool_use_count: task.tool_use_count ?? 0,
3215
+ usage: task.usage,
3216
+ // Token usage and cost tracking
3217
+ duration_ms: task.duration_ms,
3218
+ // Task execution duration
3219
+ agent_session_id: task.agent_session_id,
3220
+ // SDK session ID
3221
+ context_window: task.context_window,
3222
+ // Context window usage
3223
+ context_window_limit: task.context_window_limit,
3224
+ // Max context window
3225
+ report: task.report,
3226
+ permission_request: task.permission_request
3227
+ // Permission state for UI approval flow
3228
+ }
3229
+ };
3230
+ }
3231
+ /**
3232
+ * Resolve short ID to full ID
3233
+ */
3234
+ async resolveId(id) {
3235
+ if (id.length === 36 && id.includes("-")) {
3236
+ return id;
3237
+ }
3238
+ const normalized = id.replace(/-/g, "").toLowerCase();
3239
+ const pattern = `${normalized}%`;
3240
+ const results = await this.db.select({ task_id: tasks.task_id }).from(tasks).where((0, import_drizzle_orm11.like)(tasks.task_id, pattern)).all();
3241
+ if (results.length === 0) {
3242
+ throw new EntityNotFoundError("Task", id);
3243
+ }
3244
+ if (results.length > 1) {
3245
+ throw new AmbiguousIdError(
3246
+ "Task",
3247
+ id,
3248
+ results.map((r) => formatShortId(r.task_id))
3249
+ );
3250
+ }
3251
+ return results[0].task_id;
3252
+ }
3253
+ /**
3254
+ * Create a new task
3255
+ */
3256
+ async create(data) {
3257
+ try {
3258
+ const insert = this.taskToInsert(data);
3259
+ await this.db.insert(tasks).values(insert);
3260
+ const row = await this.db.select().from(tasks).where((0, import_drizzle_orm11.eq)(tasks.task_id, insert.task_id)).get();
3261
+ if (!row) {
3262
+ throw new RepositoryError("Failed to retrieve created task");
3263
+ }
3264
+ return this.rowToTask(row);
3265
+ } catch (error) {
3266
+ if (error instanceof RepositoryError) throw error;
3267
+ throw new RepositoryError(
3268
+ `Failed to create task: ${error instanceof Error ? error.message : String(error)}`,
3269
+ error
3270
+ );
3271
+ }
3272
+ }
3273
+ /**
3274
+ * Bulk create multiple tasks (for imports)
3275
+ */
3276
+ async createMany(taskList) {
3277
+ try {
3278
+ const inserts = taskList.map((task) => this.taskToInsert(task));
3279
+ await this.db.insert(tasks).values(inserts);
3280
+ const taskIds = inserts.map((t) => t.task_id);
3281
+ const rows = await this.db.select().from(tasks).where(import_drizzle_orm11.sql`${tasks.task_id} IN ${import_drizzle_orm11.sql.raw(`(${taskIds.map((id) => `'${id}'`).join(",")})`)}`);
3282
+ return rows.map((row) => this.rowToTask(row));
3283
+ } catch (error) {
3284
+ throw new RepositoryError(
3285
+ `Failed to bulk create tasks: ${error instanceof Error ? error.message : String(error)}`,
3286
+ error
3287
+ );
3288
+ }
3289
+ }
3290
+ /**
3291
+ * Find task by ID (supports short ID)
3292
+ */
3293
+ async findById(id) {
3294
+ try {
3295
+ const fullId = await this.resolveId(id);
3296
+ const row = await this.db.select().from(tasks).where((0, import_drizzle_orm11.eq)(tasks.task_id, fullId)).get();
3297
+ return row ? this.rowToTask(row) : null;
3298
+ } catch (error) {
3299
+ if (error instanceof EntityNotFoundError) return null;
3300
+ if (error instanceof AmbiguousIdError) throw error;
3301
+ throw new RepositoryError(
3302
+ `Failed to find task: ${error instanceof Error ? error.message : String(error)}`,
3303
+ error
3304
+ );
3305
+ }
3306
+ }
3307
+ /**
3308
+ * Find all tasks
3309
+ */
3310
+ async findAll() {
3311
+ try {
3312
+ const rows = await this.db.select().from(tasks).all();
3313
+ return rows.map((row) => this.rowToTask(row));
3314
+ } catch (error) {
3315
+ throw new RepositoryError(
3316
+ `Failed to find all tasks: ${error instanceof Error ? error.message : String(error)}`,
3317
+ error
3318
+ );
3319
+ }
3320
+ }
3321
+ /**
3322
+ * Find all tasks for a session
3323
+ */
3324
+ async findBySession(sessionId) {
3325
+ try {
3326
+ const rows = await this.db.select().from(tasks).where((0, import_drizzle_orm11.eq)(tasks.session_id, sessionId)).orderBy(tasks.created_at).all();
3327
+ return rows.map((row) => this.rowToTask(row));
3328
+ } catch (error) {
3329
+ throw new RepositoryError(
3330
+ `Failed to find tasks by session: ${error instanceof Error ? error.message : String(error)}`,
3331
+ error
3332
+ );
3333
+ }
3334
+ }
3335
+ /**
3336
+ * Find running tasks across all sessions
3337
+ */
3338
+ async findRunning() {
3339
+ try {
3340
+ const rows = await this.db.select().from(tasks).where((0, import_drizzle_orm11.eq)(tasks.status, TaskStatus.RUNNING)).all();
3341
+ return rows.map((row) => this.rowToTask(row));
3342
+ } catch (error) {
3343
+ throw new RepositoryError(
3344
+ `Failed to find running tasks: ${error instanceof Error ? error.message : String(error)}`,
3345
+ error
3346
+ );
3347
+ }
3348
+ }
3349
+ /**
3350
+ * Find tasks by status
3351
+ */
3352
+ async findByStatus(status) {
3353
+ try {
3354
+ const rows = await this.db.select().from(tasks).where((0, import_drizzle_orm11.eq)(tasks.status, status)).all();
3355
+ return rows.map((row) => this.rowToTask(row));
3356
+ } catch (error) {
3357
+ throw new RepositoryError(
3358
+ `Failed to find tasks by status: ${error instanceof Error ? error.message : String(error)}`,
3359
+ error
3360
+ );
3361
+ }
3362
+ }
3363
+ /**
3364
+ * Update task by ID
3365
+ */
3366
+ async update(id, updates) {
3367
+ try {
3368
+ const fullId = await this.resolveId(id);
3369
+ const current = await this.findById(fullId);
3370
+ if (!current) {
3371
+ throw new EntityNotFoundError("Task", id);
3372
+ }
3373
+ const merged = { ...current, ...updates };
3374
+ const insert = this.taskToInsert(merged);
3375
+ await this.db.update(tasks).set({
3376
+ status: insert.status,
3377
+ completed_at: insert.completed_at,
3378
+ data: insert.data
3379
+ }).where((0, import_drizzle_orm11.eq)(tasks.task_id, fullId));
3380
+ const updated = await this.findById(fullId);
3381
+ if (!updated) {
3382
+ throw new RepositoryError("Failed to retrieve updated task");
3383
+ }
3384
+ return updated;
3385
+ } catch (error) {
3386
+ if (error instanceof RepositoryError) throw error;
3387
+ if (error instanceof EntityNotFoundError) throw error;
3388
+ throw new RepositoryError(
3389
+ `Failed to update task: ${error instanceof Error ? error.message : String(error)}`,
3390
+ error
3391
+ );
3392
+ }
3393
+ }
3394
+ /**
3395
+ * Delete task by ID
3396
+ */
3397
+ async delete(id) {
3398
+ try {
3399
+ const fullId = await this.resolveId(id);
3400
+ const result = await this.db.delete(tasks).where((0, import_drizzle_orm11.eq)(tasks.task_id, fullId)).run();
3401
+ if (result.rowsAffected === 0) {
3402
+ throw new EntityNotFoundError("Task", id);
3403
+ }
3404
+ } catch (error) {
3405
+ if (error instanceof EntityNotFoundError) throw error;
3406
+ throw new RepositoryError(
3407
+ `Failed to delete task: ${error instanceof Error ? error.message : String(error)}`,
3408
+ error
3409
+ );
3410
+ }
3411
+ }
3412
+ /**
3413
+ * Count tasks for a session
3414
+ */
3415
+ async countBySession(sessionId) {
3416
+ try {
3417
+ const result = await this.db.select({ count: import_drizzle_orm11.sql`count(*)` }).from(tasks).where((0, import_drizzle_orm11.eq)(tasks.session_id, sessionId)).get();
3418
+ return result?.count ?? 0;
3419
+ } catch (error) {
3420
+ throw new RepositoryError(
3421
+ `Failed to count tasks: ${error instanceof Error ? error.message : String(error)}`,
3422
+ error
3423
+ );
3424
+ }
3425
+ }
3426
+ };
3427
+
3428
+ // src/db/repositories/worktrees.ts
3429
+ var import_drizzle_orm12 = require("drizzle-orm");
3430
+ init_ids();
3431
+ var WorktreeRepository = class {
3432
+ constructor(db) {
3433
+ this.db = db;
3434
+ }
3435
+ /**
3436
+ * Convert database row to Worktree type
3437
+ */
3438
+ rowToWorktree(row) {
3439
+ return {
3440
+ worktree_id: row.worktree_id,
3441
+ repo_id: row.repo_id,
3442
+ created_at: new Date(row.created_at).toISOString(),
3443
+ updated_at: row.updated_at ? new Date(row.updated_at).toISOString() : new Date(row.created_at).toISOString(),
3444
+ created_by: row.created_by,
3445
+ name: row.name,
3446
+ ref: row.ref,
3447
+ worktree_unique_id: row.worktree_unique_id,
3448
+ board_id: row.board_id ?? void 0,
3449
+ // Top-level column
3450
+ ...row.data,
3451
+ sessions: row.data.sessions || []
3452
+ };
3453
+ }
3454
+ /**
3455
+ * Convert Worktree to database insert format
3456
+ */
3457
+ worktreeToInsert(worktree) {
3458
+ const now = Date.now();
3459
+ const worktreeId = worktree.worktree_id ?? generateId();
3460
+ return {
3461
+ worktree_id: worktreeId,
3462
+ repo_id: worktree.repo_id,
3463
+ created_at: worktree.created_at ? new Date(worktree.created_at) : new Date(now),
3464
+ updated_at: new Date(now),
3465
+ created_by: worktree.created_by ?? "anonymous",
3466
+ name: worktree.name,
3467
+ ref: worktree.ref,
3468
+ worktree_unique_id: worktree.worktree_unique_id,
3469
+ // Required field
3470
+ // Explicitly convert undefined to null for Drizzle (undefined values are ignored in set())
3471
+ board_id: worktree.board_id === void 0 ? null : worktree.board_id || null,
3472
+ data: {
3473
+ path: worktree.path,
3474
+ base_ref: worktree.base_ref,
3475
+ base_sha: worktree.base_sha,
3476
+ last_commit_sha: worktree.last_commit_sha,
3477
+ tracking_branch: worktree.tracking_branch,
3478
+ new_branch: worktree.new_branch ?? false,
3479
+ issue_url: worktree.issue_url,
3480
+ pull_request_url: worktree.pull_request_url,
3481
+ notes: worktree.notes,
3482
+ environment_instance: worktree.environment_instance,
3483
+ sessions: worktree.sessions || [],
3484
+ last_used: worktree.last_used ?? new Date(now).toISOString(),
3485
+ custom_context: worktree.custom_context
3486
+ }
3487
+ };
3488
+ }
3489
+ /**
3490
+ * Create a new worktree
3491
+ */
3492
+ async create(worktree) {
3493
+ const insert = this.worktreeToInsert(worktree);
3494
+ const [row] = await this.db.insert(worktrees).values(insert).returning();
3495
+ return this.rowToWorktree(row);
3496
+ }
3497
+ /**
3498
+ * Find worktree by exact ID or short ID prefix
3499
+ */
3500
+ async findById(id) {
3501
+ if (id.length === 36 && id.includes("-")) {
3502
+ const [row] = await this.db.select().from(worktrees).where((0, import_drizzle_orm12.eq)(worktrees.worktree_id, id)).limit(1);
3503
+ return row ? this.rowToWorktree(row) : null;
3504
+ }
3505
+ const prefix = id.replace(/-/g, "").toLowerCase();
3506
+ const matches = await this.db.select().from(worktrees).where((0, import_drizzle_orm12.like)(worktrees.worktree_id, `${prefix}%`)).limit(2);
3507
+ if (matches.length === 0) return null;
3508
+ if (matches.length > 1) {
3509
+ throw new AmbiguousIdError(
3510
+ "Worktree",
3511
+ prefix,
3512
+ matches.map((m) => formatShortId(m.worktree_id))
3513
+ );
3514
+ }
3515
+ return this.rowToWorktree(matches[0]);
3516
+ }
3517
+ /**
3518
+ * Find all worktrees (with optional filters)
3519
+ */
3520
+ async findAll(filter) {
3521
+ if (filter?.repo_id) {
3522
+ const rows2 = await this.db.select().from(worktrees).where((0, import_drizzle_orm12.eq)(worktrees.repo_id, filter.repo_id));
3523
+ return rows2.map((row) => this.rowToWorktree(row));
3524
+ }
3525
+ const rows = await this.db.select().from(worktrees);
3526
+ return rows.map((row) => this.rowToWorktree(row));
3527
+ }
3528
+ /**
3529
+ * Update worktree by ID
3530
+ */
3531
+ async update(id, updates) {
3532
+ const existing = await this.findById(id);
3533
+ if (!existing) {
3534
+ throw new EntityNotFoundError("Worktree", id);
3535
+ }
3536
+ const merged = {
3537
+ ...existing,
3538
+ ...updates,
3539
+ worktree_id: existing.worktree_id,
3540
+ repo_id: existing.repo_id,
3541
+ created_at: existing.created_at,
3542
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3543
+ };
3544
+ const insert = this.worktreeToInsert(merged);
3545
+ const [row] = await this.db.update(worktrees).set(insert).where((0, import_drizzle_orm12.eq)(worktrees.worktree_id, existing.worktree_id)).returning();
3546
+ return this.rowToWorktree(row);
3547
+ }
3548
+ /**
3549
+ * Delete worktree by ID
3550
+ */
3551
+ async delete(id) {
3552
+ const existing = await this.findById(id);
3553
+ if (!existing) {
3554
+ throw new EntityNotFoundError("Worktree", id);
3555
+ }
3556
+ await this.db.delete(worktrees).where((0, import_drizzle_orm12.eq)(worktrees.worktree_id, existing.worktree_id));
3557
+ }
3558
+ /**
3559
+ * Find worktree by repo_id and name
3560
+ */
3561
+ async findByRepoAndName(repoId, name) {
3562
+ const [row] = await this.db.select().from(worktrees).where(import_drizzle_orm12.sql`${worktrees.repo_id} = ${repoId} AND ${worktrees.name} = ${name}`).limit(1);
3563
+ return row ? this.rowToWorktree(row) : null;
3564
+ }
3565
+ /**
3566
+ * Add session to worktree's sessions array
3567
+ */
3568
+ async addSession(worktreeId, sessionId) {
3569
+ const worktree = await this.findById(worktreeId);
3570
+ if (!worktree) {
3571
+ throw new EntityNotFoundError("Worktree", worktreeId);
3572
+ }
3573
+ const sessions2 = worktree.sessions || [];
3574
+ if (!sessions2.includes(sessionId)) {
3575
+ sessions2.push(sessionId);
3576
+ }
3577
+ return this.update(worktreeId, {
3578
+ sessions: sessions2,
3579
+ last_used: (/* @__PURE__ */ new Date()).toISOString()
3580
+ });
3581
+ }
3582
+ /**
3583
+ * Remove session from worktree's sessions array
3584
+ */
3585
+ async removeSession(worktreeId, sessionId) {
3586
+ const worktree = await this.findById(worktreeId);
3587
+ if (!worktree) {
3588
+ throw new EntityNotFoundError("Worktree", worktreeId);
3589
+ }
3590
+ const sessions2 = (worktree.sessions || []).filter((id) => id !== sessionId);
3591
+ return this.update(worktreeId, { sessions: sessions2 });
3592
+ }
3593
+ };
3594
+
3595
+ // src/db/user-utils.ts
3596
+ var import_bcryptjs = __toESM(require("bcryptjs"), 1);
3597
+ var import_drizzle_orm13 = require("drizzle-orm");
3598
+ init_ids();
3599
+ async function createUser(db, data) {
3600
+ const existing = await db.select().from(users).where((0, import_drizzle_orm13.eq)(users.email, data.email)).get();
3601
+ if (existing) {
3602
+ throw new Error(`User with email ${data.email} already exists`);
3603
+ }
3604
+ const hashedPassword = await import_bcryptjs.default.hash(data.password, 10);
3605
+ const now = /* @__PURE__ */ new Date();
3606
+ const user_id = generateId();
3607
+ const role = data.role || "member";
3608
+ const defaultEmoji = role === "admin" ? "\u2B50" : "\u{1F464}";
3609
+ const row = await db.insert(users).values({
3610
+ user_id,
3611
+ email: data.email,
3612
+ password: hashedPassword,
3613
+ name: data.name,
3614
+ emoji: defaultEmoji,
3615
+ role,
3616
+ created_at: now,
3617
+ updated_at: now,
3618
+ data: {
3619
+ preferences: {}
3620
+ }
3621
+ }).returning().get();
3622
+ const userData = row.data;
3623
+ return {
3624
+ user_id: row.user_id,
3625
+ email: row.email,
3626
+ name: row.name ?? void 0,
3627
+ role: row.role,
3628
+ avatar: userData.avatar,
3629
+ preferences: userData.preferences,
3630
+ onboarding_completed: !!row.onboarding_completed,
3631
+ created_at: row.created_at,
3632
+ updated_at: row.updated_at ?? void 0
3633
+ };
3634
+ }
3635
+ async function userExists(db, email) {
3636
+ const existing = await db.select().from(users).where((0, import_drizzle_orm13.eq)(users.email, email)).get();
3637
+ return !!existing;
3638
+ }
3639
+ async function getUserByEmail(db, email) {
3640
+ const row = await db.select().from(users).where((0, import_drizzle_orm13.eq)(users.email, email)).get();
3641
+ if (!row) {
3642
+ return null;
3643
+ }
3644
+ const userData = row.data;
3645
+ return {
3646
+ user_id: row.user_id,
3647
+ email: row.email,
3648
+ name: row.name ?? void 0,
3649
+ role: row.role,
3650
+ avatar: userData.avatar,
3651
+ preferences: userData.preferences,
3652
+ onboarding_completed: !!row.onboarding_completed,
3653
+ created_at: row.created_at,
3654
+ updated_at: row.updated_at ?? void 0
3655
+ };
3656
+ }
3657
+ var DEFAULT_ADMIN_USER = {
3658
+ email: "admin@agor.live",
3659
+ password: "admin",
3660
+ name: "Admin",
3661
+ role: "admin"
3662
+ };
3663
+ async function createDefaultAdminUser(db) {
3664
+ const existing = await getUserByEmail(db, DEFAULT_ADMIN_USER.email);
3665
+ if (existing) {
3666
+ throw new Error(`Admin user already exists (email: ${DEFAULT_ADMIN_USER.email})`);
3667
+ }
3668
+ return createUser(db, DEFAULT_ADMIN_USER);
3669
+ }
3670
+
3671
+ // src/db/index.ts
3672
+ var compare = import_bcryptjs2.default.compare;
3673
+ var hash = import_bcryptjs2.default.hash;
3674
+ // Annotate the CommonJS export names for ESM import in node:
3675
+ 0 && (module.exports = {
3676
+ AmbiguousIdError,
3677
+ BoardCommentsRepository,
3678
+ BoardObjectRepository,
3679
+ BoardRepository,
3680
+ DEFAULT_ADMIN_USER,
3681
+ DEFAULT_DB_PATH,
3682
+ DatabaseConnectionError,
3683
+ EntityNotFoundError,
3684
+ IdResolutionError,
3685
+ MCPServerRepository,
3686
+ MessagesRepository,
3687
+ MigrationError,
3688
+ RepoRepository,
3689
+ RepositoryError,
3690
+ SessionMCPServerRepository,
3691
+ SessionRepository,
3692
+ TaskRepository,
3693
+ WorktreeRepository,
3694
+ and,
3695
+ boardComments,
3696
+ boardObjects,
3697
+ boards,
3698
+ compare,
3699
+ createDatabase,
3700
+ createDefaultAdminUser,
3701
+ createLocalDatabase,
3702
+ createUser,
3703
+ desc,
3704
+ eq,
3705
+ formatShortId,
3706
+ generateId,
3707
+ getUserByEmail,
3708
+ hash,
3709
+ inArray,
3710
+ initializeDatabase,
3711
+ like,
3712
+ mcpServers,
3713
+ messages,
3714
+ or,
3715
+ repos,
3716
+ resolveShortId,
3717
+ runMigrations,
3718
+ seedInitialData,
3719
+ sessionMcpServers,
3720
+ sessions,
3721
+ sql,
3722
+ tasks,
3723
+ userExists,
3724
+ users,
3725
+ worktrees
3726
+ });