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