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