@basegrid_tech/mcp 0.6.5
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/dist/index.js +3461 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import "module";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// ../server/src/config-manager.ts
|
|
8
|
+
import fs4 from "fs";
|
|
9
|
+
|
|
10
|
+
// ../shared/src/types.ts
|
|
11
|
+
var CLAUDE_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
|
|
12
|
+
var CLAUDE_DEFAULT_EFFORT = "medium";
|
|
13
|
+
var CODEX_DEFAULT_MODEL_ID = "gpt-5.5";
|
|
14
|
+
var CODEX_OBSOLETE_MODEL_IDS = [
|
|
15
|
+
"gpt-5-codex",
|
|
16
|
+
"gpt-5.4-mini",
|
|
17
|
+
"gpt-5.2"
|
|
18
|
+
];
|
|
19
|
+
var CODEX_DEFAULT_EFFORT = "medium";
|
|
20
|
+
var DEFAULT_WORKSPACE = {
|
|
21
|
+
id: "personal",
|
|
22
|
+
name: "Personal",
|
|
23
|
+
icon: "User",
|
|
24
|
+
iconColor: "#6b7280",
|
|
25
|
+
order: 0
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ../shared/src/agent-defaults.ts
|
|
29
|
+
var DEFAULT_AGENT_COMMANDS = {
|
|
30
|
+
claude: {
|
|
31
|
+
command: "claude",
|
|
32
|
+
args: [],
|
|
33
|
+
headlessArgs: ["--dangerously-skip-permissions"]
|
|
34
|
+
},
|
|
35
|
+
copilot: {
|
|
36
|
+
command: "copilot",
|
|
37
|
+
args: [],
|
|
38
|
+
headlessArgs: ["--allow-all"]
|
|
39
|
+
},
|
|
40
|
+
codex: {
|
|
41
|
+
command: "codex",
|
|
42
|
+
args: [],
|
|
43
|
+
headlessArgs: ["-a", "never"]
|
|
44
|
+
},
|
|
45
|
+
opencode: { command: "opencode", args: [] },
|
|
46
|
+
gemini: {
|
|
47
|
+
command: "gemini",
|
|
48
|
+
args: [],
|
|
49
|
+
headlessArgs: ["-y"]
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ../server/src/database.ts
|
|
54
|
+
import Database from "libsql";
|
|
55
|
+
import path3 from "path";
|
|
56
|
+
import fs3 from "fs";
|
|
57
|
+
import { randomUUID } from "crypto";
|
|
58
|
+
|
|
59
|
+
// ../server/src/logger.ts
|
|
60
|
+
import pino from "pino";
|
|
61
|
+
var log = pino({ level: process.env.VITEST ? "silent" : "info" }, process.stderr);
|
|
62
|
+
var logger_default = log;
|
|
63
|
+
|
|
64
|
+
// ../server/src/basegrid-paths-fs.ts
|
|
65
|
+
import path from "path";
|
|
66
|
+
import os from "os";
|
|
67
|
+
import fs from "fs";
|
|
68
|
+
|
|
69
|
+
// ../shared/src/basegrid-paths.ts
|
|
70
|
+
var BASEGRID_DIR_NAME = ".basegrid";
|
|
71
|
+
var ATTACHMENTS_DIR_NAME = "attachments";
|
|
72
|
+
var LEGACY_ATTACHMENTS_DIR_NAME = "chat-attachments";
|
|
73
|
+
var WORKTREES_DIR_NAME = "worktrees";
|
|
74
|
+
var LEGACY_WORKTREES_MARKER = ".basegrid-worktrees";
|
|
75
|
+
var ATTACHMENT_MARKERS = [
|
|
76
|
+
`/${BASEGRID_DIR_NAME}/${ATTACHMENTS_DIR_NAME}/`,
|
|
77
|
+
`\\${BASEGRID_DIR_NAME}\\${ATTACHMENTS_DIR_NAME}\\`,
|
|
78
|
+
`/${BASEGRID_DIR_NAME}/${LEGACY_ATTACHMENTS_DIR_NAME}/`,
|
|
79
|
+
`\\${BASEGRID_DIR_NAME}\\${LEGACY_ATTACHMENTS_DIR_NAME}\\`
|
|
80
|
+
];
|
|
81
|
+
var MANAGED_WORKTREE_MARKERS = [
|
|
82
|
+
`/${BASEGRID_DIR_NAME}/${WORKTREES_DIR_NAME}/`,
|
|
83
|
+
`\\${BASEGRID_DIR_NAME}\\${WORKTREES_DIR_NAME}\\`,
|
|
84
|
+
`/${LEGACY_WORKTREES_MARKER}/`,
|
|
85
|
+
`\\${LEGACY_WORKTREES_MARKER}\\`
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// ../server/src/basegrid-paths-fs.ts
|
|
89
|
+
function getDataDir() {
|
|
90
|
+
return path.join(os.homedir(), BASEGRID_DIR_NAME);
|
|
91
|
+
}
|
|
92
|
+
function getDbPath() {
|
|
93
|
+
return path.join(getDataDir(), "basegrid.db");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ../server/src/process-utils.ts
|
|
97
|
+
import { execFileSync, execFile } from "child_process";
|
|
98
|
+
import fs2 from "fs";
|
|
99
|
+
import path2 from "path";
|
|
100
|
+
function getUserShellEnv() {
|
|
101
|
+
if (process.platform === "win32") return { ...process.env };
|
|
102
|
+
try {
|
|
103
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
104
|
+
const output = execFileSync(shell, ["-ilc", "env"], {
|
|
105
|
+
encoding: "utf-8",
|
|
106
|
+
timeout: 5e3,
|
|
107
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
108
|
+
});
|
|
109
|
+
const env = {};
|
|
110
|
+
for (const line of output.split("\n")) {
|
|
111
|
+
const idx = line.indexOf("=");
|
|
112
|
+
if (idx > 0) {
|
|
113
|
+
env[line.substring(0, idx)] = line.substring(idx + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return env;
|
|
117
|
+
} catch {
|
|
118
|
+
return { ...process.env };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
var resolvedEnv = getUserShellEnv();
|
|
122
|
+
var DEFAULT_NO_PROXY_HOSTS = [
|
|
123
|
+
"localhost",
|
|
124
|
+
"127.0.0.1",
|
|
125
|
+
"::1",
|
|
126
|
+
".local",
|
|
127
|
+
".ts.net",
|
|
128
|
+
"10.",
|
|
129
|
+
"192.168."
|
|
130
|
+
];
|
|
131
|
+
var DEFAULT_NO_PROXY = DEFAULT_NO_PROXY_HOSTS.join(",");
|
|
132
|
+
|
|
133
|
+
// ../server/src/default-workflows.ts
|
|
134
|
+
var DEFAULT_TASK_WORKFLOW_ID = "system:default-task-workflow";
|
|
135
|
+
function buildDefaultTaskWorkflow() {
|
|
136
|
+
const triggerConfig = {
|
|
137
|
+
triggerType: "taskStatusChanged",
|
|
138
|
+
fromStatus: "todo",
|
|
139
|
+
toStatus: "in_progress"
|
|
140
|
+
// projectFilter omitted → fires in every project
|
|
141
|
+
};
|
|
142
|
+
const launchConfig = {
|
|
143
|
+
agentType: "fromTask",
|
|
144
|
+
projectName: "",
|
|
145
|
+
projectPath: "",
|
|
146
|
+
headless: true
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
id: DEFAULT_TASK_WORKFLOW_ID,
|
|
150
|
+
name: "Default Task Workflow",
|
|
151
|
+
icon: "Play",
|
|
152
|
+
iconColor: "#10b981",
|
|
153
|
+
enabled: true,
|
|
154
|
+
workspaceId: "personal",
|
|
155
|
+
nodes: [
|
|
156
|
+
{
|
|
157
|
+
id: "trigger-1",
|
|
158
|
+
type: "trigger",
|
|
159
|
+
label: "When task moves to In Progress",
|
|
160
|
+
position: { x: 0, y: 0 },
|
|
161
|
+
config: triggerConfig
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "launch-1",
|
|
165
|
+
type: "launchAgent",
|
|
166
|
+
label: "Launch task agent",
|
|
167
|
+
position: { x: 0, y: 120 },
|
|
168
|
+
config: launchConfig
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
edges: [{ id: "e1", source: "trigger-1", target: "launch-1" }]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ../server/src/database.ts
|
|
176
|
+
var CONFIG_DIR = getDataDir();
|
|
177
|
+
var DB_PATH = getDbPath();
|
|
178
|
+
var db = null;
|
|
179
|
+
function getDb() {
|
|
180
|
+
if (!db) throw new Error("Database not initialized. Call initDatabase() first.");
|
|
181
|
+
return db;
|
|
182
|
+
}
|
|
183
|
+
function initDatabase() {
|
|
184
|
+
if (!fs3.existsSync(CONFIG_DIR)) {
|
|
185
|
+
fs3.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
db = new Database(DB_PATH);
|
|
189
|
+
db.pragma("journal_mode = WAL");
|
|
190
|
+
db.pragma("foreign_keys = ON");
|
|
191
|
+
createSchema();
|
|
192
|
+
seedSystemDefaults();
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger_default.error("[database] Failed to open database:", err);
|
|
195
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
196
|
+
const isCorrupt = /corrupt|notadb|malformed|not a database|file is not a database/i.test(
|
|
197
|
+
message
|
|
198
|
+
);
|
|
199
|
+
if (isCorrupt) {
|
|
200
|
+
logger_default.warn("[database] Database appears corrupt, attempting recovery...");
|
|
201
|
+
recoverCorruptDatabase();
|
|
202
|
+
} else {
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function seedSystemDefaults() {
|
|
208
|
+
const d = getDb();
|
|
209
|
+
const flagRow = d.prepare("SELECT value FROM defaults WHERE key = 'hasSeededDefaultTaskWorkflow'").get();
|
|
210
|
+
if (flagRow) {
|
|
211
|
+
try {
|
|
212
|
+
if (JSON.parse(flagRow.value) === true) return;
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const existing = d.prepare("SELECT id FROM workflows WHERE id = ?").get(DEFAULT_TASK_WORKFLOW_ID);
|
|
217
|
+
if (!existing) {
|
|
218
|
+
const w = buildDefaultTaskWorkflow();
|
|
219
|
+
d.prepare(
|
|
220
|
+
`INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
222
|
+
).run(
|
|
223
|
+
w.id,
|
|
224
|
+
w.name,
|
|
225
|
+
w.icon,
|
|
226
|
+
w.iconColor,
|
|
227
|
+
JSON.stringify(w.nodes),
|
|
228
|
+
JSON.stringify(w.edges),
|
|
229
|
+
w.enabled ? 1 : 0,
|
|
230
|
+
w.lastRunAt ?? null,
|
|
231
|
+
w.lastRunStatus ?? null,
|
|
232
|
+
w.staggerDelayMs ?? null,
|
|
233
|
+
w.workspaceId ?? "personal"
|
|
234
|
+
);
|
|
235
|
+
logger_default.info(`[database] Seeded default task workflow (${DEFAULT_TASK_WORKFLOW_ID})`);
|
|
236
|
+
}
|
|
237
|
+
d.prepare(
|
|
238
|
+
"INSERT OR REPLACE INTO defaults (key, value) VALUES ('hasSeededDefaultTaskWorkflow', ?)"
|
|
239
|
+
).run(JSON.stringify(true));
|
|
240
|
+
}
|
|
241
|
+
function recoverCorruptDatabase() {
|
|
242
|
+
try {
|
|
243
|
+
db?.close();
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
db = null;
|
|
247
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
248
|
+
const backupPath = `${DB_PATH}.corrupt-${timestamp}`;
|
|
249
|
+
try {
|
|
250
|
+
if (fs3.existsSync(DB_PATH)) {
|
|
251
|
+
fs3.copyFileSync(DB_PATH, backupPath);
|
|
252
|
+
logger_default.info(`[database] Backed up corrupt database to ${backupPath}`);
|
|
253
|
+
}
|
|
254
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
255
|
+
const file = DB_PATH + suffix;
|
|
256
|
+
if (fs3.existsSync(file)) fs3.unlinkSync(file);
|
|
257
|
+
}
|
|
258
|
+
} catch (backupErr) {
|
|
259
|
+
logger_default.error("[database] Failed to back up corrupt database:", backupErr);
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
db = new Database(DB_PATH);
|
|
263
|
+
db.pragma("journal_mode = WAL");
|
|
264
|
+
db.pragma("foreign_keys = ON");
|
|
265
|
+
createSchema();
|
|
266
|
+
seedSystemDefaults();
|
|
267
|
+
logger_default.info("[database] Successfully created fresh database after corruption recovery");
|
|
268
|
+
} catch (freshErr) {
|
|
269
|
+
logger_default.error("[database] Failed to create fresh database after corruption:", freshErr);
|
|
270
|
+
throw freshErr;
|
|
271
|
+
}
|
|
272
|
+
logger_default.warn(`[database] Database was corrupted and has been reset. Backup saved to: ${backupPath}`);
|
|
273
|
+
}
|
|
274
|
+
function dbSignalChange() {
|
|
275
|
+
try {
|
|
276
|
+
const signalPath = path3.join(CONFIG_DIR, ".db-signal");
|
|
277
|
+
fs3.writeFileSync(signalPath, Date.now().toString());
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function closeDatabase() {
|
|
282
|
+
if (db) {
|
|
283
|
+
db.close();
|
|
284
|
+
db = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function factoryResetDatabase() {
|
|
288
|
+
if (!db) initDatabase();
|
|
289
|
+
const d = getDb();
|
|
290
|
+
try {
|
|
291
|
+
d.pragma("wal_checkpoint(TRUNCATE)");
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
const run = d.transaction(() => {
|
|
295
|
+
const rows = d.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
296
|
+
d.pragma("foreign_keys = OFF");
|
|
297
|
+
for (const { name } of rows) {
|
|
298
|
+
try {
|
|
299
|
+
d.exec(`DROP TABLE IF EXISTS "${name}"`);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
logger_default.warn(`[database] factory-reset: drop ${name} failed: ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
d.pragma("foreign_keys = ON");
|
|
305
|
+
});
|
|
306
|
+
run();
|
|
307
|
+
createSchema();
|
|
308
|
+
seedSystemDefaults();
|
|
309
|
+
closeDatabase();
|
|
310
|
+
for (const f of [DB_PATH, `${DB_PATH}-wal`, `${DB_PATH}-shm`, `${DB_PATH}-journal`]) {
|
|
311
|
+
try {
|
|
312
|
+
if (fs3.existsSync(f)) fs3.rmSync(f, { force: true });
|
|
313
|
+
} catch (err) {
|
|
314
|
+
logger_default.warn(`[database] factory-reset: rm ${f} failed: ${err.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
initDatabase();
|
|
318
|
+
}
|
|
319
|
+
function createSchema() {
|
|
320
|
+
const d = getDb();
|
|
321
|
+
const cols = d.prepare("PRAGMA table_info(workflows)").all();
|
|
322
|
+
if (cols.some((c) => c.name === "actions")) {
|
|
323
|
+
d.exec("ALTER TABLE workflows RENAME TO workflows_backup_old_format");
|
|
324
|
+
logger_default.warn("[database] migrated old-format workflows table to workflows_backup_old_format");
|
|
325
|
+
}
|
|
326
|
+
d.exec(`
|
|
327
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
328
|
+
key TEXT PRIMARY KEY,
|
|
329
|
+
value TEXT NOT NULL
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
CREATE TABLE IF NOT EXISTS basegrid_script_statuses (
|
|
333
|
+
id TEXT PRIMARY KEY,
|
|
334
|
+
project_path TEXT NOT NULL,
|
|
335
|
+
worktree_path TEXT,
|
|
336
|
+
role TEXT NOT NULL,
|
|
337
|
+
command TEXT NOT NULL,
|
|
338
|
+
phase TEXT NOT NULL,
|
|
339
|
+
exit_code INTEGER,
|
|
340
|
+
started_at INTEGER NOT NULL,
|
|
341
|
+
finished_at INTEGER,
|
|
342
|
+
terminal_id TEXT,
|
|
343
|
+
output TEXT,
|
|
344
|
+
error TEXT,
|
|
345
|
+
updated_at INTEGER NOT NULL
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
CREATE INDEX IF NOT EXISTS idx_basegrid_script_statuses_project
|
|
349
|
+
ON basegrid_script_statuses(project_path);
|
|
350
|
+
|
|
351
|
+
CREATE TABLE IF NOT EXISTS defaults (
|
|
352
|
+
key TEXT PRIMARY KEY,
|
|
353
|
+
value TEXT NOT NULL
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
357
|
+
name TEXT PRIMARY KEY,
|
|
358
|
+
path TEXT NOT NULL,
|
|
359
|
+
preferred_agents TEXT NOT NULL DEFAULT '[]',
|
|
360
|
+
icon TEXT,
|
|
361
|
+
icon_color TEXT,
|
|
362
|
+
host_ids TEXT,
|
|
363
|
+
project_id TEXT
|
|
364
|
+
);
|
|
365
|
+
-- UNIQUE INDEX \u043F\u043E project_id \u0441\u043E\u0437\u0434\u0430\u0451\u0442\u0441\u044F \u0432 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 v17 (\u0441\u043C. migrateSchema):
|
|
366
|
+
-- \u043D\u0430 upgrade-\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0438 \u043A\u043E\u043B\u043E\u043D\u043A\u0438 project_id \u0435\u0449\u0451 \u043D\u0435\u0442, \u0438 CREATE INDEX \u0437\u0434\u0435\u0441\u044C \u043F\u0430\u0434\u0430\u043B \u0431\u044B.
|
|
367
|
+
|
|
368
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
369
|
+
id TEXT PRIMARY KEY,
|
|
370
|
+
name TEXT NOT NULL,
|
|
371
|
+
icon TEXT NOT NULL,
|
|
372
|
+
icon_color TEXT NOT NULL,
|
|
373
|
+
nodes TEXT NOT NULL DEFAULT '[]',
|
|
374
|
+
edges TEXT NOT NULL DEFAULT '[]',
|
|
375
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
376
|
+
last_run_at TEXT,
|
|
377
|
+
last_run_status TEXT,
|
|
378
|
+
stagger_delay_ms INTEGER
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
CREATE TABLE IF NOT EXISTS agent_commands (
|
|
382
|
+
agent_type TEXT PRIMARY KEY,
|
|
383
|
+
command TEXT NOT NULL,
|
|
384
|
+
args TEXT NOT NULL DEFAULT '[]',
|
|
385
|
+
headless_args TEXT,
|
|
386
|
+
fallback_command TEXT,
|
|
387
|
+
fallback_args TEXT
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
CREATE TABLE IF NOT EXISTS remote_hosts (
|
|
391
|
+
id TEXT PRIMARY KEY,
|
|
392
|
+
label TEXT NOT NULL,
|
|
393
|
+
hostname TEXT NOT NULL,
|
|
394
|
+
user TEXT NOT NULL,
|
|
395
|
+
port INTEGER NOT NULL DEFAULT 22,
|
|
396
|
+
auth_method TEXT DEFAULT 'agent',
|
|
397
|
+
ssh_key_path TEXT,
|
|
398
|
+
credential_id TEXT,
|
|
399
|
+
encrypted_password TEXT,
|
|
400
|
+
ssh_options TEXT
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
CREATE TABLE IF NOT EXISTS ssh_keys (
|
|
404
|
+
id TEXT PRIMARY KEY,
|
|
405
|
+
label TEXT NOT NULL,
|
|
406
|
+
encrypted_private_key TEXT NOT NULL,
|
|
407
|
+
public_key TEXT,
|
|
408
|
+
certificate TEXT,
|
|
409
|
+
key_type TEXT,
|
|
410
|
+
created_at TEXT NOT NULL
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
CREATE TABLE IF NOT EXISTS account (
|
|
414
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
415
|
+
user_id TEXT NOT NULL,
|
|
416
|
+
email TEXT NOT NULL,
|
|
417
|
+
plan TEXT NOT NULL DEFAULT 'free',
|
|
418
|
+
plan_expires_at TEXT,
|
|
419
|
+
encrypted_access_token TEXT NOT NULL,
|
|
420
|
+
encrypted_refresh_token TEXT NOT NULL,
|
|
421
|
+
token_expires_at INTEGER NOT NULL,
|
|
422
|
+
updated_at TEXT NOT NULL
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
CREATE TABLE IF NOT EXISTS install_meta (
|
|
426
|
+
key TEXT PRIMARY KEY,
|
|
427
|
+
value TEXT NOT NULL
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
431
|
+
id TEXT PRIMARY KEY,
|
|
432
|
+
project_name TEXT NOT NULL,
|
|
433
|
+
title TEXT NOT NULL,
|
|
434
|
+
description TEXT NOT NULL DEFAULT '',
|
|
435
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
436
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
437
|
+
assigned_session_id TEXT,
|
|
438
|
+
assigned_agent TEXT,
|
|
439
|
+
agent_session_id TEXT,
|
|
440
|
+
branch TEXT,
|
|
441
|
+
use_worktree INTEGER DEFAULT 0,
|
|
442
|
+
created_at TEXT NOT NULL,
|
|
443
|
+
updated_at TEXT NOT NULL,
|
|
444
|
+
completed_at TEXT
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
448
|
+
id TEXT PRIMARY KEY,
|
|
449
|
+
agent_type TEXT NOT NULL,
|
|
450
|
+
project_name TEXT NOT NULL,
|
|
451
|
+
project_path TEXT NOT NULL,
|
|
452
|
+
status TEXT NOT NULL,
|
|
453
|
+
created_at INTEGER NOT NULL,
|
|
454
|
+
pid INTEGER NOT NULL,
|
|
455
|
+
display_name TEXT,
|
|
456
|
+
branch TEXT,
|
|
457
|
+
worktree_path TEXT,
|
|
458
|
+
is_worktree INTEGER DEFAULT 0,
|
|
459
|
+
remote_host_id TEXT,
|
|
460
|
+
remote_host_label TEXT,
|
|
461
|
+
hook_session_id TEXT,
|
|
462
|
+
status_source TEXT,
|
|
463
|
+
saved_at INTEGER,
|
|
464
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
465
|
+
worktree_name TEXT,
|
|
466
|
+
agent_session_id TEXT,
|
|
467
|
+
docked INTEGER NOT NULL DEFAULT 0,
|
|
468
|
+
basegrid_role TEXT,
|
|
469
|
+
shell_cwd TEXT
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
CREATE TABLE IF NOT EXISTS schedule_log (
|
|
473
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
474
|
+
workflow_id TEXT NOT NULL,
|
|
475
|
+
workflow_name TEXT NOT NULL,
|
|
476
|
+
executed_at TEXT NOT NULL,
|
|
477
|
+
status TEXT NOT NULL,
|
|
478
|
+
sessions_launched INTEGER NOT NULL DEFAULT 0,
|
|
479
|
+
error TEXT
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_schedule_log_workflow_id ON schedule_log(workflow_id);
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_name, status);
|
|
484
|
+
|
|
485
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
486
|
+
id TEXT PRIMARY KEY,
|
|
487
|
+
name TEXT NOT NULL,
|
|
488
|
+
icon TEXT,
|
|
489
|
+
icon_color TEXT,
|
|
490
|
+
"order" INTEGER NOT NULL DEFAULT 0
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
494
|
+
id TEXT PRIMARY KEY,
|
|
495
|
+
workflow_id TEXT NOT NULL,
|
|
496
|
+
started_at TEXT NOT NULL,
|
|
497
|
+
completed_at TEXT,
|
|
498
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
499
|
+
trigger_task_id TEXT
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
CREATE TABLE IF NOT EXISTS workflow_run_nodes (
|
|
503
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
504
|
+
run_id TEXT NOT NULL,
|
|
505
|
+
node_id TEXT NOT NULL,
|
|
506
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
507
|
+
started_at TEXT,
|
|
508
|
+
completed_at TEXT,
|
|
509
|
+
session_id TEXT,
|
|
510
|
+
error TEXT,
|
|
511
|
+
logs TEXT,
|
|
512
|
+
task_id TEXT,
|
|
513
|
+
agent_session_id TEXT,
|
|
514
|
+
agent_type TEXT,
|
|
515
|
+
project_name TEXT,
|
|
516
|
+
project_path TEXT,
|
|
517
|
+
approved_at TEXT,
|
|
518
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow ON workflow_runs(workflow_id);
|
|
522
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_task ON workflow_runs(trigger_task_id);
|
|
523
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_run ON workflow_run_nodes(run_id);
|
|
524
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_task ON workflow_run_nodes(task_id);
|
|
525
|
+
|
|
526
|
+
CREATE TABLE IF NOT EXISTS session_logs (
|
|
527
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
528
|
+
task_id TEXT NOT NULL,
|
|
529
|
+
session_id TEXT NOT NULL,
|
|
530
|
+
agent_type TEXT,
|
|
531
|
+
branch TEXT,
|
|
532
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
533
|
+
started_at TEXT NOT NULL,
|
|
534
|
+
completed_at TEXT,
|
|
535
|
+
exit_code INTEGER,
|
|
536
|
+
logs TEXT,
|
|
537
|
+
project_name TEXT
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
CREATE INDEX IF NOT EXISTS idx_session_logs_task ON session_logs(task_id);
|
|
541
|
+
CREATE INDEX IF NOT EXISTS idx_session_logs_session ON session_logs(session_id);
|
|
542
|
+
|
|
543
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
544
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
545
|
+
session_id TEXT NOT NULL,
|
|
546
|
+
event_type TEXT NOT NULL,
|
|
547
|
+
timestamp TEXT NOT NULL,
|
|
548
|
+
metadata TEXT
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp DESC);
|
|
552
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_type ON session_events(event_type, timestamp DESC);
|
|
553
|
+
|
|
554
|
+
CREATE TABLE IF NOT EXISTS session_activity (
|
|
555
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
556
|
+
terminal_id TEXT NOT NULL,
|
|
557
|
+
agent_session_id TEXT,
|
|
558
|
+
seq INTEGER NOT NULL,
|
|
559
|
+
kind TEXT NOT NULL,
|
|
560
|
+
timestamp INTEGER NOT NULL,
|
|
561
|
+
tool_name TEXT,
|
|
562
|
+
tool_use_id TEXT,
|
|
563
|
+
parent_tool_use_id TEXT,
|
|
564
|
+
summary TEXT,
|
|
565
|
+
status TEXT,
|
|
566
|
+
detail_json TEXT
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
CREATE INDEX IF NOT EXISTS idx_session_activity_agent
|
|
570
|
+
ON session_activity(agent_session_id, id);
|
|
571
|
+
CREATE INDEX IF NOT EXISTS idx_session_activity_terminal
|
|
572
|
+
ON session_activity(terminal_id, id);
|
|
573
|
+
|
|
574
|
+
CREATE TABLE IF NOT EXISTS worktree_diff_cache (
|
|
575
|
+
worktree_path TEXT PRIMARY KEY,
|
|
576
|
+
insertions INTEGER NOT NULL,
|
|
577
|
+
deletions INTEGER NOT NULL,
|
|
578
|
+
files_changed INTEGER NOT NULL,
|
|
579
|
+
updated_at INTEGER NOT NULL
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
CREATE TABLE IF NOT EXISTS worktree_list_cache (
|
|
583
|
+
project_path TEXT NOT NULL,
|
|
584
|
+
worktree_path TEXT NOT NULL,
|
|
585
|
+
branch TEXT,
|
|
586
|
+
is_main INTEGER NOT NULL,
|
|
587
|
+
name TEXT,
|
|
588
|
+
alias TEXT,
|
|
589
|
+
alias_generated_at INTEGER,
|
|
590
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
591
|
+
updated_at INTEGER NOT NULL,
|
|
592
|
+
PRIMARY KEY (project_path, worktree_path)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
CREATE INDEX IF NOT EXISTS idx_worktree_list_project
|
|
596
|
+
ON worktree_list_cache(project_path);
|
|
597
|
+
|
|
598
|
+
CREATE TABLE IF NOT EXISTS usage_metrics (
|
|
599
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
600
|
+
terminal_id TEXT NOT NULL,
|
|
601
|
+
agent_session_id TEXT,
|
|
602
|
+
project_path TEXT,
|
|
603
|
+
timestamp INTEGER NOT NULL,
|
|
604
|
+
model TEXT,
|
|
605
|
+
input_tokens INTEGER NOT NULL,
|
|
606
|
+
output_tokens INTEGER NOT NULL,
|
|
607
|
+
cache_creation_tokens INTEGER NOT NULL,
|
|
608
|
+
cache_read_tokens INTEGER NOT NULL
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
CREATE INDEX IF NOT EXISTS idx_usage_metrics_agent
|
|
612
|
+
ON usage_metrics(agent_session_id, timestamp);
|
|
613
|
+
CREATE INDEX IF NOT EXISTS idx_usage_metrics_project
|
|
614
|
+
ON usage_metrics(project_path, timestamp);
|
|
615
|
+
CREATE INDEX IF NOT EXISTS idx_usage_metrics_ts
|
|
616
|
+
ON usage_metrics(timestamp);
|
|
617
|
+
`);
|
|
618
|
+
migrateSchema(d);
|
|
619
|
+
verifySchema(d);
|
|
620
|
+
}
|
|
621
|
+
function migrateSchema(d) {
|
|
622
|
+
const row = d.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
|
|
623
|
+
const version = row ? parseInt(row.value, 10) : 0;
|
|
624
|
+
if (version < 1) {
|
|
625
|
+
d.transaction(() => {
|
|
626
|
+
const projectCols = d.prepare("PRAGMA table_info(projects)").all();
|
|
627
|
+
if (!projectCols.some((c) => c.name === "workspace_id")) {
|
|
628
|
+
d.exec("ALTER TABLE projects ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'");
|
|
629
|
+
}
|
|
630
|
+
const workflowCols = d.prepare("PRAGMA table_info(workflows)").all();
|
|
631
|
+
if (!workflowCols.some((c) => c.name === "workspace_id")) {
|
|
632
|
+
d.exec("ALTER TABLE workflows ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'");
|
|
633
|
+
}
|
|
634
|
+
d.prepare(
|
|
635
|
+
`INSERT OR IGNORE INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
|
|
636
|
+
).run(
|
|
637
|
+
DEFAULT_WORKSPACE.id,
|
|
638
|
+
DEFAULT_WORKSPACE.name,
|
|
639
|
+
DEFAULT_WORKSPACE.icon ?? null,
|
|
640
|
+
DEFAULT_WORKSPACE.iconColor ?? null,
|
|
641
|
+
DEFAULT_WORKSPACE.order
|
|
642
|
+
);
|
|
643
|
+
d.prepare(
|
|
644
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '1')"
|
|
645
|
+
).run();
|
|
646
|
+
})();
|
|
647
|
+
logger_default.info("[database] migrated schema to version 1 (workspaces)");
|
|
648
|
+
}
|
|
649
|
+
if (version < 2) {
|
|
650
|
+
d.transaction(() => {
|
|
651
|
+
const hostCols = d.prepare("PRAGMA table_info(remote_hosts)").all();
|
|
652
|
+
if (!hostCols.some((c) => c.name === "auth_method")) {
|
|
653
|
+
d.exec("ALTER TABLE remote_hosts ADD COLUMN auth_method TEXT");
|
|
654
|
+
d.exec("ALTER TABLE remote_hosts ADD COLUMN credential_id TEXT");
|
|
655
|
+
d.exec("ALTER TABLE remote_hosts ADD COLUMN encrypted_password TEXT");
|
|
656
|
+
d.exec(
|
|
657
|
+
"UPDATE remote_hosts SET auth_method = CASE WHEN ssh_key_path IS NOT NULL AND ssh_key_path != '' THEN 'key-file' ELSE 'agent' END"
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
d.exec(`
|
|
661
|
+
CREATE TABLE IF NOT EXISTS ssh_keys (
|
|
662
|
+
id TEXT PRIMARY KEY,
|
|
663
|
+
label TEXT NOT NULL,
|
|
664
|
+
encrypted_private_key TEXT NOT NULL,
|
|
665
|
+
public_key TEXT,
|
|
666
|
+
certificate TEXT,
|
|
667
|
+
key_type TEXT,
|
|
668
|
+
created_at TEXT NOT NULL
|
|
669
|
+
)
|
|
670
|
+
`);
|
|
671
|
+
d.prepare(
|
|
672
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '2')"
|
|
673
|
+
).run();
|
|
674
|
+
})();
|
|
675
|
+
logger_default.info("[database] migrated schema to version 2 (ssh credential vault)");
|
|
676
|
+
}
|
|
677
|
+
if (version < 3) {
|
|
678
|
+
d.transaction(() => {
|
|
679
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
680
|
+
if (!sessionCols.some((c) => c.name === "sort_order")) {
|
|
681
|
+
d.exec("ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0");
|
|
682
|
+
}
|
|
683
|
+
d.prepare(
|
|
684
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '3')"
|
|
685
|
+
).run();
|
|
686
|
+
})();
|
|
687
|
+
logger_default.info("[database] migrated schema to version 3 (session sort order)");
|
|
688
|
+
}
|
|
689
|
+
if (version < 4) {
|
|
690
|
+
d.transaction(() => {
|
|
691
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
692
|
+
if (!sessionCols.some((c) => c.name === "worktree_name")) {
|
|
693
|
+
d.exec("ALTER TABLE sessions ADD COLUMN worktree_name TEXT");
|
|
694
|
+
}
|
|
695
|
+
d.prepare(
|
|
696
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '4')"
|
|
697
|
+
).run();
|
|
698
|
+
})();
|
|
699
|
+
logger_default.info("[database] migrated schema to version 4 (worktree name)");
|
|
700
|
+
}
|
|
701
|
+
if (version < 5) {
|
|
702
|
+
d.transaction(() => {
|
|
703
|
+
const agentCols = d.prepare("PRAGMA table_info(agent_commands)").all();
|
|
704
|
+
if (!agentCols.some((c) => c.name === "headless_args")) {
|
|
705
|
+
d.exec("ALTER TABLE agent_commands ADD COLUMN headless_args TEXT");
|
|
706
|
+
}
|
|
707
|
+
d.prepare(
|
|
708
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '5')"
|
|
709
|
+
).run();
|
|
710
|
+
})();
|
|
711
|
+
logger_default.info("[database] migrated schema to version 5 (headless args)");
|
|
712
|
+
}
|
|
713
|
+
if (version < 6) {
|
|
714
|
+
d.transaction(() => {
|
|
715
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
716
|
+
if (!sessionCols.some((c) => c.name === "claude_session_id") && !sessionCols.some((c) => c.name === "agent_session_id")) {
|
|
717
|
+
d.exec("ALTER TABLE sessions ADD COLUMN claude_session_id TEXT");
|
|
718
|
+
}
|
|
719
|
+
d.prepare(
|
|
720
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '6')"
|
|
721
|
+
).run();
|
|
722
|
+
})();
|
|
723
|
+
logger_default.info("[database] migrated schema to version 6 (claude session id)");
|
|
724
|
+
}
|
|
725
|
+
if (version < 7) {
|
|
726
|
+
d.transaction(() => {
|
|
727
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
728
|
+
const hasOld = sessionCols.some((c) => c.name === "claude_session_id");
|
|
729
|
+
const hasNew = sessionCols.some((c) => c.name === "agent_session_id");
|
|
730
|
+
if (hasOld && !hasNew) {
|
|
731
|
+
try {
|
|
732
|
+
d.exec("ALTER TABLE sessions RENAME COLUMN claude_session_id TO agent_session_id");
|
|
733
|
+
} catch {
|
|
734
|
+
d.exec("ALTER TABLE sessions ADD COLUMN agent_session_id TEXT");
|
|
735
|
+
d.exec("UPDATE sessions SET agent_session_id = claude_session_id");
|
|
736
|
+
}
|
|
737
|
+
} else if (hasOld && hasNew) {
|
|
738
|
+
d.exec(
|
|
739
|
+
"UPDATE sessions SET agent_session_id = claude_session_id WHERE agent_session_id IS NULL AND claude_session_id IS NOT NULL"
|
|
740
|
+
);
|
|
741
|
+
} else if (!hasNew) {
|
|
742
|
+
d.exec("ALTER TABLE sessions ADD COLUMN agent_session_id TEXT");
|
|
743
|
+
}
|
|
744
|
+
d.prepare(
|
|
745
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '7')"
|
|
746
|
+
).run();
|
|
747
|
+
})();
|
|
748
|
+
logger_default.info(
|
|
749
|
+
"[database] migrated schema to version 7 (rename claude_session_id \u2192 agent_session_id)"
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
if (version < 8) {
|
|
753
|
+
d.transaction(() => {
|
|
754
|
+
const cols = d.prepare("PRAGMA table_info(workflow_run_nodes)").all();
|
|
755
|
+
if (!cols.some((c) => c.name === "approved_at")) {
|
|
756
|
+
d.exec("ALTER TABLE workflow_run_nodes ADD COLUMN approved_at TEXT");
|
|
757
|
+
}
|
|
758
|
+
d.prepare(
|
|
759
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '8')"
|
|
760
|
+
).run();
|
|
761
|
+
})();
|
|
762
|
+
logger_default.info("[database] migrated schema to version 8 (approval gate timestamp)");
|
|
763
|
+
}
|
|
764
|
+
if (version < 9) {
|
|
765
|
+
d.transaction(() => {
|
|
766
|
+
d.exec(`
|
|
767
|
+
CREATE TABLE IF NOT EXISTS session_activity (
|
|
768
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
769
|
+
terminal_id TEXT NOT NULL,
|
|
770
|
+
agent_session_id TEXT,
|
|
771
|
+
seq INTEGER NOT NULL,
|
|
772
|
+
kind TEXT NOT NULL,
|
|
773
|
+
timestamp INTEGER NOT NULL,
|
|
774
|
+
tool_name TEXT,
|
|
775
|
+
tool_use_id TEXT,
|
|
776
|
+
summary TEXT,
|
|
777
|
+
status TEXT,
|
|
778
|
+
detail_json TEXT
|
|
779
|
+
);
|
|
780
|
+
CREATE INDEX IF NOT EXISTS idx_session_activity_agent
|
|
781
|
+
ON session_activity(agent_session_id, id);
|
|
782
|
+
CREATE INDEX IF NOT EXISTS idx_session_activity_terminal
|
|
783
|
+
ON session_activity(terminal_id, id);
|
|
784
|
+
`);
|
|
785
|
+
d.prepare(
|
|
786
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '9')"
|
|
787
|
+
).run();
|
|
788
|
+
})();
|
|
789
|
+
logger_default.info("[database] migrated schema to version 9 (session_activity)");
|
|
790
|
+
}
|
|
791
|
+
if (version < 10) {
|
|
792
|
+
d.transaction(() => {
|
|
793
|
+
d.exec(`
|
|
794
|
+
CREATE TABLE IF NOT EXISTS worktree_diff_cache (
|
|
795
|
+
worktree_path TEXT PRIMARY KEY,
|
|
796
|
+
insertions INTEGER NOT NULL,
|
|
797
|
+
deletions INTEGER NOT NULL,
|
|
798
|
+
files_changed INTEGER NOT NULL,
|
|
799
|
+
updated_at INTEGER NOT NULL
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
CREATE TABLE IF NOT EXISTS worktree_list_cache (
|
|
803
|
+
project_path TEXT NOT NULL,
|
|
804
|
+
worktree_path TEXT NOT NULL,
|
|
805
|
+
branch TEXT,
|
|
806
|
+
is_main INTEGER NOT NULL,
|
|
807
|
+
name TEXT,
|
|
808
|
+
updated_at INTEGER NOT NULL,
|
|
809
|
+
PRIMARY KEY (project_path, worktree_path)
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
CREATE INDEX IF NOT EXISTS idx_worktree_list_project
|
|
813
|
+
ON worktree_list_cache(project_path);
|
|
814
|
+
`);
|
|
815
|
+
d.prepare(
|
|
816
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '10')"
|
|
817
|
+
).run();
|
|
818
|
+
})();
|
|
819
|
+
logger_default.info("[database] migrated schema to version 10 (worktree caches)");
|
|
820
|
+
}
|
|
821
|
+
if (version < 11) {
|
|
822
|
+
d.transaction(() => {
|
|
823
|
+
d.exec(`
|
|
824
|
+
CREATE TABLE IF NOT EXISTS usage_metrics (
|
|
825
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
826
|
+
terminal_id TEXT NOT NULL,
|
|
827
|
+
agent_session_id TEXT,
|
|
828
|
+
project_path TEXT,
|
|
829
|
+
timestamp INTEGER NOT NULL,
|
|
830
|
+
model TEXT,
|
|
831
|
+
input_tokens INTEGER NOT NULL,
|
|
832
|
+
output_tokens INTEGER NOT NULL,
|
|
833
|
+
cache_creation_tokens INTEGER NOT NULL,
|
|
834
|
+
cache_read_tokens INTEGER NOT NULL
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
CREATE INDEX IF NOT EXISTS idx_usage_metrics_agent
|
|
838
|
+
ON usage_metrics(agent_session_id, timestamp);
|
|
839
|
+
CREATE INDEX IF NOT EXISTS idx_usage_metrics_project
|
|
840
|
+
ON usage_metrics(project_path, timestamp);
|
|
841
|
+
CREATE INDEX IF NOT EXISTS idx_usage_metrics_ts
|
|
842
|
+
ON usage_metrics(timestamp);
|
|
843
|
+
`);
|
|
844
|
+
d.prepare(
|
|
845
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '11')"
|
|
846
|
+
).run();
|
|
847
|
+
})();
|
|
848
|
+
logger_default.info("[database] migrated schema to version 11 (usage_metrics)");
|
|
849
|
+
}
|
|
850
|
+
if (version < 12) {
|
|
851
|
+
d.transaction(() => {
|
|
852
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
853
|
+
if (!sessionCols.some((c) => c.name === "docked")) {
|
|
854
|
+
d.exec("ALTER TABLE sessions ADD COLUMN docked INTEGER NOT NULL DEFAULT 0");
|
|
855
|
+
}
|
|
856
|
+
if (!sessionCols.some((c) => c.name === "basegrid_role")) {
|
|
857
|
+
d.exec("ALTER TABLE sessions ADD COLUMN basegrid_role TEXT");
|
|
858
|
+
}
|
|
859
|
+
if (!sessionCols.some((c) => c.name === "shell_cwd")) {
|
|
860
|
+
d.exec("ALTER TABLE sessions ADD COLUMN shell_cwd TEXT");
|
|
861
|
+
}
|
|
862
|
+
d.prepare(
|
|
863
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '12')"
|
|
864
|
+
).run();
|
|
865
|
+
})();
|
|
866
|
+
logger_default.info("[database] migrated schema to version 12 (docked terminals + basegrid roles)");
|
|
867
|
+
}
|
|
868
|
+
if (version < 13) {
|
|
869
|
+
d.transaction(() => {
|
|
870
|
+
const result = d.prepare("DELETE FROM sessions WHERE agent_type = 'shell'").run();
|
|
871
|
+
if (result.changes > 0) {
|
|
872
|
+
logger_default.info(`[database] migration v13: removed ${result.changes} stale shell session(s)`);
|
|
873
|
+
}
|
|
874
|
+
d.prepare(
|
|
875
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '13')"
|
|
876
|
+
).run();
|
|
877
|
+
})();
|
|
878
|
+
logger_default.info("[database] migrated schema to version 13 (purge shell zombies)");
|
|
879
|
+
}
|
|
880
|
+
if (version < 14) {
|
|
881
|
+
d.transaction(() => {
|
|
882
|
+
const cols = d.prepare("PRAGMA table_info(session_activity)").all();
|
|
883
|
+
if (!cols.some((c) => c.name === "parent_tool_use_id")) {
|
|
884
|
+
d.exec("ALTER TABLE session_activity ADD COLUMN parent_tool_use_id TEXT");
|
|
885
|
+
}
|
|
886
|
+
d.prepare(
|
|
887
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '14')"
|
|
888
|
+
).run();
|
|
889
|
+
})();
|
|
890
|
+
logger_default.info("[database] migrated schema to version 14 (parent_tool_use_id for subagent nesting)");
|
|
891
|
+
}
|
|
892
|
+
if (version < 15) {
|
|
893
|
+
d.transaction(() => {
|
|
894
|
+
const cols = d.prepare("PRAGMA table_info(worktree_list_cache)").all();
|
|
895
|
+
if (!cols.some((c) => c.name === "alias")) {
|
|
896
|
+
d.exec("ALTER TABLE worktree_list_cache ADD COLUMN alias TEXT");
|
|
897
|
+
}
|
|
898
|
+
if (!cols.some((c) => c.name === "alias_generated_at")) {
|
|
899
|
+
d.exec("ALTER TABLE worktree_list_cache ADD COLUMN alias_generated_at INTEGER");
|
|
900
|
+
}
|
|
901
|
+
d.prepare(
|
|
902
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '15')"
|
|
903
|
+
).run();
|
|
904
|
+
})();
|
|
905
|
+
logger_default.info("[database] migrated schema to version 15 (worktree alias from LLM)");
|
|
906
|
+
}
|
|
907
|
+
if (version < 16) {
|
|
908
|
+
d.transaction(() => {
|
|
909
|
+
const cols = d.prepare("PRAGMA table_info(worktree_list_cache)").all();
|
|
910
|
+
if (!cols.some((c) => c.name === "sort_order")) {
|
|
911
|
+
d.exec("ALTER TABLE worktree_list_cache ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0");
|
|
912
|
+
}
|
|
913
|
+
const projects = d.prepare("SELECT DISTINCT project_path FROM worktree_list_cache").all();
|
|
914
|
+
const updateOrder = d.prepare(
|
|
915
|
+
"UPDATE worktree_list_cache SET sort_order = ? WHERE project_path = ? AND worktree_path = ?"
|
|
916
|
+
);
|
|
917
|
+
for (const { project_path } of projects) {
|
|
918
|
+
const rows = d.prepare(
|
|
919
|
+
"SELECT worktree_path FROM worktree_list_cache WHERE project_path = ? ORDER BY is_main DESC, name ASC"
|
|
920
|
+
).all(project_path);
|
|
921
|
+
rows.forEach((r, i) => updateOrder.run(i + 1, project_path, r.worktree_path));
|
|
922
|
+
}
|
|
923
|
+
d.prepare(
|
|
924
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '16')"
|
|
925
|
+
).run();
|
|
926
|
+
})();
|
|
927
|
+
logger_default.info("[database] migrated schema to version 16 (worktree sort_order)");
|
|
928
|
+
}
|
|
929
|
+
if (version < 17) {
|
|
930
|
+
d.transaction(() => {
|
|
931
|
+
const cols = d.prepare("PRAGMA table_info(projects)").all();
|
|
932
|
+
if (!cols.some((c) => c.name === "project_id")) {
|
|
933
|
+
d.exec("ALTER TABLE projects ADD COLUMN project_id TEXT");
|
|
934
|
+
}
|
|
935
|
+
const rows = d.prepare("SELECT name FROM projects WHERE project_id IS NULL OR project_id = ''").all();
|
|
936
|
+
const upd = d.prepare("UPDATE projects SET project_id = ? WHERE name = ?");
|
|
937
|
+
for (const r of rows) {
|
|
938
|
+
const id = randomUUID();
|
|
939
|
+
upd.run(id, r.name);
|
|
940
|
+
}
|
|
941
|
+
d.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_project_id ON projects(project_id)");
|
|
942
|
+
d.prepare(
|
|
943
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '17')"
|
|
944
|
+
).run();
|
|
945
|
+
})();
|
|
946
|
+
logger_default.info("[database] migrated schema to version 17 (projects.project_id UUID)");
|
|
947
|
+
}
|
|
948
|
+
if (version < 18) {
|
|
949
|
+
d.transaction(() => {
|
|
950
|
+
d.exec(`
|
|
951
|
+
CREATE TABLE IF NOT EXISTS source_connections (
|
|
952
|
+
id TEXT PRIMARY KEY,
|
|
953
|
+
connector_id TEXT NOT NULL,
|
|
954
|
+
name TEXT NOT NULL,
|
|
955
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
956
|
+
sync_interval_minutes INTEGER NOT NULL DEFAULT 5,
|
|
957
|
+
status_mapping TEXT NOT NULL DEFAULT '{}',
|
|
958
|
+
execution_project TEXT,
|
|
959
|
+
last_sync_at TEXT,
|
|
960
|
+
last_sync_error TEXT,
|
|
961
|
+
sync_cursor TEXT,
|
|
962
|
+
created_at TEXT NOT NULL
|
|
963
|
+
)
|
|
964
|
+
`);
|
|
965
|
+
d.exec(`
|
|
966
|
+
CREATE TABLE IF NOT EXISTS task_source_links (
|
|
967
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
968
|
+
connection_id TEXT NOT NULL REFERENCES source_connections(id) ON DELETE CASCADE,
|
|
969
|
+
connector_id TEXT NOT NULL,
|
|
970
|
+
external_id TEXT NOT NULL,
|
|
971
|
+
external_url TEXT NOT NULL,
|
|
972
|
+
source_status_raw TEXT NOT NULL,
|
|
973
|
+
source_updated_at TEXT NOT NULL,
|
|
974
|
+
last_synced_at TEXT NOT NULL,
|
|
975
|
+
conflict_state TEXT NOT NULL DEFAULT 'none',
|
|
976
|
+
PRIMARY KEY (task_id),
|
|
977
|
+
UNIQUE (connection_id, external_id)
|
|
978
|
+
)
|
|
979
|
+
`);
|
|
980
|
+
const taskCols = d.prepare("PRAGMA table_info(tasks)").all();
|
|
981
|
+
if (!taskCols.some((c) => c.name === "source_connector_id")) {
|
|
982
|
+
d.exec("ALTER TABLE tasks ADD COLUMN source_connector_id TEXT");
|
|
983
|
+
}
|
|
984
|
+
if (!taskCols.some((c) => c.name === "source_external_url")) {
|
|
985
|
+
d.exec("ALTER TABLE tasks ADD COLUMN source_external_url TEXT");
|
|
986
|
+
}
|
|
987
|
+
if (!taskCols.some((c) => c.name === "source_external_id")) {
|
|
988
|
+
d.exec("ALTER TABLE tasks ADD COLUMN source_external_id TEXT");
|
|
989
|
+
}
|
|
990
|
+
d.prepare(
|
|
991
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '18')"
|
|
992
|
+
).run();
|
|
993
|
+
})();
|
|
994
|
+
logger_default.info("[database] migrated schema to version 18 (connector source connections)");
|
|
995
|
+
}
|
|
996
|
+
if (version < 19) {
|
|
997
|
+
d.transaction(() => {
|
|
998
|
+
const result = d.prepare("DELETE FROM workflows WHERE id LIKE 'connector:%'").run();
|
|
999
|
+
d.prepare(
|
|
1000
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '19')"
|
|
1001
|
+
).run();
|
|
1002
|
+
logger_default.info(
|
|
1003
|
+
`[database] migrated schema to version 19 (removed ${result.changes} seeded connector workflow(s))`
|
|
1004
|
+
);
|
|
1005
|
+
})();
|
|
1006
|
+
}
|
|
1007
|
+
if (version < 20) {
|
|
1008
|
+
d.transaction(() => {
|
|
1009
|
+
d.exec(`
|
|
1010
|
+
CREATE TABLE IF NOT EXISTS sync_history (
|
|
1011
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1012
|
+
connection_id TEXT NOT NULL REFERENCES source_connections(id) ON DELETE CASCADE,
|
|
1013
|
+
kind TEXT NOT NULL,
|
|
1014
|
+
started_at TEXT NOT NULL,
|
|
1015
|
+
finished_at TEXT NOT NULL,
|
|
1016
|
+
duration_ms INTEGER NOT NULL,
|
|
1017
|
+
imported INTEGER NOT NULL DEFAULT 0,
|
|
1018
|
+
updated INTEGER NOT NULL DEFAULT 0,
|
|
1019
|
+
status TEXT NOT NULL,
|
|
1020
|
+
error_message TEXT
|
|
1021
|
+
);
|
|
1022
|
+
CREATE INDEX IF NOT EXISTS idx_sync_history_conn
|
|
1023
|
+
ON sync_history(connection_id, id DESC);
|
|
1024
|
+
`);
|
|
1025
|
+
d.prepare(
|
|
1026
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '20')"
|
|
1027
|
+
).run();
|
|
1028
|
+
})();
|
|
1029
|
+
logger_default.info("[database] migrated schema to version 20 (sync_history)");
|
|
1030
|
+
}
|
|
1031
|
+
if (version < 21) {
|
|
1032
|
+
d.transaction(() => {
|
|
1033
|
+
const tasks = d.prepare(
|
|
1034
|
+
`SELECT t.id, t.source_connector_id, t.source_external_id, t.source_external_url, t.updated_at
|
|
1035
|
+
FROM tasks t
|
|
1036
|
+
LEFT JOIN task_source_links l ON l.task_id = t.id
|
|
1037
|
+
WHERE t.source_connector_id IS NOT NULL
|
|
1038
|
+
AND t.source_external_id IS NOT NULL
|
|
1039
|
+
AND l.task_id IS NULL`
|
|
1040
|
+
).all();
|
|
1041
|
+
const insertLink = d.prepare(
|
|
1042
|
+
`INSERT OR IGNORE INTO task_source_links
|
|
1043
|
+
(task_id, connection_id, connector_id, external_id, external_url, source_status_raw, source_updated_at, last_synced_at, conflict_state)
|
|
1044
|
+
VALUES (?, ?, ?, ?, ?, '', ?, ?, 'none')`
|
|
1045
|
+
);
|
|
1046
|
+
let restored = 0;
|
|
1047
|
+
for (const t of tasks) {
|
|
1048
|
+
const conn = d.prepare(
|
|
1049
|
+
"SELECT id FROM source_connections WHERE connector_id = ? ORDER BY created_at DESC LIMIT 1"
|
|
1050
|
+
).get(t.source_connector_id);
|
|
1051
|
+
if (!conn) continue;
|
|
1052
|
+
insertLink.run(
|
|
1053
|
+
t.id,
|
|
1054
|
+
conn.id,
|
|
1055
|
+
t.source_connector_id,
|
|
1056
|
+
t.source_external_id,
|
|
1057
|
+
t.source_external_url ?? "",
|
|
1058
|
+
t.updated_at,
|
|
1059
|
+
t.updated_at
|
|
1060
|
+
);
|
|
1061
|
+
restored++;
|
|
1062
|
+
}
|
|
1063
|
+
d.prepare(
|
|
1064
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '21')"
|
|
1065
|
+
).run();
|
|
1066
|
+
logger_default.info(`[database] migrated schema to version 21 (restored ${restored} task_source_links)`);
|
|
1067
|
+
})();
|
|
1068
|
+
}
|
|
1069
|
+
if (version < 22) {
|
|
1070
|
+
d.transaction(() => {
|
|
1071
|
+
const cols = d.prepare("PRAGMA table_info(task_source_links)").all();
|
|
1072
|
+
if (!cols.some((c) => c.name === "last_error")) {
|
|
1073
|
+
d.exec("ALTER TABLE task_source_links ADD COLUMN last_error TEXT");
|
|
1074
|
+
}
|
|
1075
|
+
if (!cols.some((c) => c.name === "last_error_at")) {
|
|
1076
|
+
d.exec("ALTER TABLE task_source_links ADD COLUMN last_error_at TEXT");
|
|
1077
|
+
}
|
|
1078
|
+
d.prepare(
|
|
1079
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '22')"
|
|
1080
|
+
).run();
|
|
1081
|
+
})();
|
|
1082
|
+
logger_default.info("[database] migrated schema to version 22 (task_source_links.last_error)");
|
|
1083
|
+
}
|
|
1084
|
+
if (version < 23) {
|
|
1085
|
+
d.transaction(() => {
|
|
1086
|
+
const result = d.prepare(
|
|
1087
|
+
"UPDATE source_connections SET sync_interval_minutes = 30 WHERE sync_interval_minutes < 30"
|
|
1088
|
+
).run();
|
|
1089
|
+
d.prepare(
|
|
1090
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '23')"
|
|
1091
|
+
).run();
|
|
1092
|
+
logger_default.info(
|
|
1093
|
+
`[database] migrated schema to version 23 (raised ${result.changes} connection(s) to 30min sync interval)`
|
|
1094
|
+
);
|
|
1095
|
+
})();
|
|
1096
|
+
}
|
|
1097
|
+
if (version < 24) {
|
|
1098
|
+
d.transaction(() => {
|
|
1099
|
+
d.exec(`
|
|
1100
|
+
CREATE TABLE IF NOT EXISTS account (
|
|
1101
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
1102
|
+
user_id TEXT NOT NULL,
|
|
1103
|
+
email TEXT NOT NULL,
|
|
1104
|
+
plan TEXT NOT NULL DEFAULT 'free',
|
|
1105
|
+
plan_expires_at TEXT,
|
|
1106
|
+
encrypted_access_token TEXT NOT NULL,
|
|
1107
|
+
encrypted_refresh_token TEXT NOT NULL,
|
|
1108
|
+
token_expires_at INTEGER NOT NULL,
|
|
1109
|
+
updated_at TEXT NOT NULL
|
|
1110
|
+
)
|
|
1111
|
+
`);
|
|
1112
|
+
d.exec(`
|
|
1113
|
+
CREATE TABLE IF NOT EXISTS install_meta (
|
|
1114
|
+
key TEXT PRIMARY KEY,
|
|
1115
|
+
value TEXT NOT NULL
|
|
1116
|
+
)
|
|
1117
|
+
`);
|
|
1118
|
+
const existing = d.prepare("SELECT value FROM install_meta WHERE key = 'install_id'").get();
|
|
1119
|
+
if (!existing) {
|
|
1120
|
+
d.prepare("INSERT INTO install_meta (key, value) VALUES ('install_id', ?)").run(
|
|
1121
|
+
randomUUID()
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
d.prepare(
|
|
1125
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '24')"
|
|
1126
|
+
).run();
|
|
1127
|
+
})();
|
|
1128
|
+
logger_default.info("[database] migrated schema to version 24 (account cache + install_id)");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function verifySchema(d) {
|
|
1132
|
+
const expectedTables = [
|
|
1133
|
+
{
|
|
1134
|
+
name: "source_connections",
|
|
1135
|
+
ddl: `CREATE TABLE IF NOT EXISTS source_connections (
|
|
1136
|
+
id TEXT PRIMARY KEY,
|
|
1137
|
+
connector_id TEXT NOT NULL,
|
|
1138
|
+
name TEXT NOT NULL,
|
|
1139
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1140
|
+
sync_interval_minutes INTEGER NOT NULL DEFAULT 30,
|
|
1141
|
+
status_mapping TEXT NOT NULL DEFAULT '{}',
|
|
1142
|
+
execution_project TEXT,
|
|
1143
|
+
last_sync_at TEXT,
|
|
1144
|
+
last_sync_error TEXT,
|
|
1145
|
+
sync_cursor TEXT,
|
|
1146
|
+
created_at TEXT NOT NULL
|
|
1147
|
+
)`
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: "sync_history",
|
|
1151
|
+
ddl: `CREATE TABLE IF NOT EXISTS sync_history (
|
|
1152
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1153
|
+
connection_id TEXT NOT NULL REFERENCES source_connections(id) ON DELETE CASCADE,
|
|
1154
|
+
kind TEXT NOT NULL,
|
|
1155
|
+
started_at TEXT NOT NULL,
|
|
1156
|
+
finished_at TEXT NOT NULL,
|
|
1157
|
+
duration_ms INTEGER NOT NULL,
|
|
1158
|
+
imported INTEGER NOT NULL DEFAULT 0,
|
|
1159
|
+
updated INTEGER NOT NULL DEFAULT 0,
|
|
1160
|
+
status TEXT NOT NULL,
|
|
1161
|
+
error_message TEXT
|
|
1162
|
+
)`
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
name: "task_source_links",
|
|
1166
|
+
ddl: `CREATE TABLE IF NOT EXISTS task_source_links (
|
|
1167
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1168
|
+
connection_id TEXT NOT NULL REFERENCES source_connections(id) ON DELETE CASCADE,
|
|
1169
|
+
connector_id TEXT NOT NULL,
|
|
1170
|
+
external_id TEXT NOT NULL,
|
|
1171
|
+
external_url TEXT NOT NULL,
|
|
1172
|
+
source_status_raw TEXT NOT NULL,
|
|
1173
|
+
source_updated_at TEXT NOT NULL,
|
|
1174
|
+
last_synced_at TEXT NOT NULL,
|
|
1175
|
+
conflict_state TEXT NOT NULL DEFAULT 'none',
|
|
1176
|
+
PRIMARY KEY (task_id),
|
|
1177
|
+
UNIQUE (connection_id, external_id)
|
|
1178
|
+
)`
|
|
1179
|
+
}
|
|
1180
|
+
];
|
|
1181
|
+
for (const { name, ddl } of expectedTables) {
|
|
1182
|
+
const row = d.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
|
|
1183
|
+
if (row) continue;
|
|
1184
|
+
try {
|
|
1185
|
+
d.exec(ddl);
|
|
1186
|
+
logger_default.warn(`[database] self-heal: created missing table ${name}`);
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
logger_default.error(`[database] self-heal: failed to create table ${name}:`, err);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const expectedByTable = {
|
|
1192
|
+
projects: [
|
|
1193
|
+
{
|
|
1194
|
+
column: "workspace_id",
|
|
1195
|
+
ddl: "ALTER TABLE projects ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'"
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
column: "project_id",
|
|
1199
|
+
ddl: "ALTER TABLE projects ADD COLUMN project_id TEXT"
|
|
1200
|
+
}
|
|
1201
|
+
],
|
|
1202
|
+
workflows: [
|
|
1203
|
+
{
|
|
1204
|
+
column: "workspace_id",
|
|
1205
|
+
ddl: "ALTER TABLE workflows ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'"
|
|
1206
|
+
}
|
|
1207
|
+
],
|
|
1208
|
+
remote_hosts: [
|
|
1209
|
+
{ column: "auth_method", ddl: "ALTER TABLE remote_hosts ADD COLUMN auth_method TEXT" },
|
|
1210
|
+
{ column: "credential_id", ddl: "ALTER TABLE remote_hosts ADD COLUMN credential_id TEXT" },
|
|
1211
|
+
{
|
|
1212
|
+
column: "encrypted_password",
|
|
1213
|
+
ddl: "ALTER TABLE remote_hosts ADD COLUMN encrypted_password TEXT"
|
|
1214
|
+
}
|
|
1215
|
+
],
|
|
1216
|
+
sessions: [
|
|
1217
|
+
{
|
|
1218
|
+
column: "sort_order",
|
|
1219
|
+
ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
|
|
1220
|
+
},
|
|
1221
|
+
{ column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
|
|
1222
|
+
{ column: "agent_session_id", ddl: "ALTER TABLE sessions ADD COLUMN agent_session_id TEXT" },
|
|
1223
|
+
{
|
|
1224
|
+
column: "docked",
|
|
1225
|
+
ddl: "ALTER TABLE sessions ADD COLUMN docked INTEGER NOT NULL DEFAULT 0"
|
|
1226
|
+
},
|
|
1227
|
+
{ column: "basegrid_role", ddl: "ALTER TABLE sessions ADD COLUMN basegrid_role TEXT" },
|
|
1228
|
+
{ column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd TEXT" }
|
|
1229
|
+
],
|
|
1230
|
+
agent_commands: [
|
|
1231
|
+
{
|
|
1232
|
+
column: "headless_args",
|
|
1233
|
+
ddl: "ALTER TABLE agent_commands ADD COLUMN headless_args TEXT"
|
|
1234
|
+
}
|
|
1235
|
+
],
|
|
1236
|
+
workflow_run_nodes: [
|
|
1237
|
+
{ column: "agent_type", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN agent_type TEXT" },
|
|
1238
|
+
{
|
|
1239
|
+
column: "project_name",
|
|
1240
|
+
ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN project_name TEXT"
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
column: "project_path",
|
|
1244
|
+
ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN project_path TEXT"
|
|
1245
|
+
},
|
|
1246
|
+
{ column: "approved_at", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN approved_at TEXT" }
|
|
1247
|
+
],
|
|
1248
|
+
tasks: [
|
|
1249
|
+
{
|
|
1250
|
+
column: "source_connector_id",
|
|
1251
|
+
ddl: "ALTER TABLE tasks ADD COLUMN source_connector_id TEXT"
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
column: "source_external_url",
|
|
1255
|
+
ddl: "ALTER TABLE tasks ADD COLUMN source_external_url TEXT"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
column: "source_external_id",
|
|
1259
|
+
ddl: "ALTER TABLE tasks ADD COLUMN source_external_id TEXT"
|
|
1260
|
+
}
|
|
1261
|
+
],
|
|
1262
|
+
task_source_links: [
|
|
1263
|
+
{
|
|
1264
|
+
column: "last_error",
|
|
1265
|
+
ddl: "ALTER TABLE task_source_links ADD COLUMN last_error TEXT"
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
column: "last_error_at",
|
|
1269
|
+
ddl: "ALTER TABLE task_source_links ADD COLUMN last_error_at TEXT"
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1272
|
+
worktree_list_cache: [
|
|
1273
|
+
{ column: "alias", ddl: "ALTER TABLE worktree_list_cache ADD COLUMN alias TEXT" },
|
|
1274
|
+
{
|
|
1275
|
+
column: "alias_generated_at",
|
|
1276
|
+
ddl: "ALTER TABLE worktree_list_cache ADD COLUMN alias_generated_at INTEGER"
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
column: "sort_order",
|
|
1280
|
+
ddl: "ALTER TABLE worktree_list_cache ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
|
|
1281
|
+
}
|
|
1282
|
+
]
|
|
1283
|
+
};
|
|
1284
|
+
for (const [table, columns] of Object.entries(expectedByTable)) {
|
|
1285
|
+
const existing = new Set(
|
|
1286
|
+
d.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name)
|
|
1287
|
+
);
|
|
1288
|
+
for (const { column, ddl } of columns) {
|
|
1289
|
+
if (existing.has(column)) continue;
|
|
1290
|
+
try {
|
|
1291
|
+
d.exec(ddl);
|
|
1292
|
+
logger_default.warn(`[database] self-heal: added missing column ${table}.${column}`);
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
logger_default.error(`[database] self-heal: failed to add ${table}.${column}:`, err);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function loadConfig() {
|
|
1300
|
+
const d = getDb();
|
|
1301
|
+
const { defaults, unknownKeys } = loadDefaults(d);
|
|
1302
|
+
const projects = loadProjects(d);
|
|
1303
|
+
const agentCommands = loadAgentCommands(d);
|
|
1304
|
+
const workflows = loadWorkflows(d);
|
|
1305
|
+
const remoteHosts = loadRemoteHosts(d);
|
|
1306
|
+
const tasks = loadTasks(d);
|
|
1307
|
+
const workspaces = loadWorkspaces(d);
|
|
1308
|
+
return {
|
|
1309
|
+
version: 1,
|
|
1310
|
+
defaults,
|
|
1311
|
+
projects,
|
|
1312
|
+
agentCommands: Object.keys(agentCommands).length > 0 ? agentCommands : { ...DEFAULT_AGENT_COMMANDS },
|
|
1313
|
+
workflows,
|
|
1314
|
+
remoteHosts,
|
|
1315
|
+
tasks,
|
|
1316
|
+
workspaces,
|
|
1317
|
+
...unknownKeys.length > 0 ? { loadWarnings: { unknownDefaults: unknownKeys } } : {}
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
var DEFAULTS_KEYS_MAP = {
|
|
1321
|
+
shell: true,
|
|
1322
|
+
fontSize: true,
|
|
1323
|
+
theme: true,
|
|
1324
|
+
rowHeight: true,
|
|
1325
|
+
defaultAgent: true,
|
|
1326
|
+
notifications: true,
|
|
1327
|
+
hasSeenOnboarding: true,
|
|
1328
|
+
widgetEnabled: true,
|
|
1329
|
+
minimizeToTray: true,
|
|
1330
|
+
dashboardEnabled: true,
|
|
1331
|
+
slashCommandFallbackDescription: true,
|
|
1332
|
+
taskViewMode: true,
|
|
1333
|
+
taskListCollapsedStatuses: true,
|
|
1334
|
+
layoutMode: true,
|
|
1335
|
+
mainViewMode: true,
|
|
1336
|
+
activeWorkspace: true,
|
|
1337
|
+
updateChannel: true,
|
|
1338
|
+
webAccessEnabled: true,
|
|
1339
|
+
mobileAccessEnabled: true,
|
|
1340
|
+
networkAccessEnabled: true,
|
|
1341
|
+
proxy: true,
|
|
1342
|
+
proxySource: true,
|
|
1343
|
+
showHeadlessAgents: true,
|
|
1344
|
+
headlessRetentionMinutes: true,
|
|
1345
|
+
autoSwitchSuppressSeconds: true,
|
|
1346
|
+
claudeDefaultModel: true,
|
|
1347
|
+
claudeDefaultEffort: true,
|
|
1348
|
+
codexDefaultModel: true,
|
|
1349
|
+
codexDefaultEffort: true,
|
|
1350
|
+
language: true,
|
|
1351
|
+
hasSeededDefaultTaskWorkflow: true,
|
|
1352
|
+
defaultSpinnerVariant: true,
|
|
1353
|
+
dragFilesFromChanges: true,
|
|
1354
|
+
taskGithubIssueEnabled: true,
|
|
1355
|
+
morningBriefingEnabled: true,
|
|
1356
|
+
lastBriefingShownDate: true,
|
|
1357
|
+
toolHeatmapEnabled: true,
|
|
1358
|
+
dashboardCollapsedColumns: true,
|
|
1359
|
+
perfOverlayEnabled: true,
|
|
1360
|
+
sidebarSnapPointsEnabled: true,
|
|
1361
|
+
sidebarWidth: true,
|
|
1362
|
+
navigationHistoryEnabled: true,
|
|
1363
|
+
experimentalClaudeStreamRuntime: true,
|
|
1364
|
+
costForecastEnabled: true,
|
|
1365
|
+
sendAnonymousUsageData: true
|
|
1366
|
+
};
|
|
1367
|
+
var DEFAULTS_KEYS = Object.keys(DEFAULTS_KEYS_MAP);
|
|
1368
|
+
var INTERNAL_DEFAULTS_KEYS = /* @__PURE__ */ new Set([
|
|
1369
|
+
"storageMigrationV1",
|
|
1370
|
+
"storageMigrationV1HealSessions",
|
|
1371
|
+
"lastStorageCleanupAt",
|
|
1372
|
+
"lastStorageCleanupBytes",
|
|
1373
|
+
"ui:activeWorktreePath"
|
|
1374
|
+
]);
|
|
1375
|
+
function loadDefaults(d) {
|
|
1376
|
+
const rows = d.prepare("SELECT key, value FROM defaults").all();
|
|
1377
|
+
const map = {};
|
|
1378
|
+
for (const row of rows) {
|
|
1379
|
+
map[row.key] = JSON.parse(row.value);
|
|
1380
|
+
}
|
|
1381
|
+
const whitelist = new Set(DEFAULTS_KEYS);
|
|
1382
|
+
const result = {};
|
|
1383
|
+
const unknownKeys = [];
|
|
1384
|
+
for (const dbKey of Object.keys(map)) {
|
|
1385
|
+
if (whitelist.has(dbKey)) {
|
|
1386
|
+
result[dbKey] = map[dbKey];
|
|
1387
|
+
} else if (INTERNAL_DEFAULTS_KEYS.has(dbKey)) {
|
|
1388
|
+
} else {
|
|
1389
|
+
unknownKeys.push(dbKey);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (unknownKeys.length > 0) {
|
|
1393
|
+
logger_default.warn(
|
|
1394
|
+
`[database] loadDefaults: ${unknownKeys.length} unknown key(s) in defaults table will be ignored: ${unknownKeys.join(", ")}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const defaultShell = process.platform === "win32" ? process.env.COMSPEC || "powershell.exe" : process.env.SHELL || "/bin/zsh";
|
|
1398
|
+
return {
|
|
1399
|
+
defaults: {
|
|
1400
|
+
...result,
|
|
1401
|
+
shell: result.shell ?? defaultShell,
|
|
1402
|
+
fontSize: result.fontSize ?? 12,
|
|
1403
|
+
theme: result.theme ?? "dark"
|
|
1404
|
+
},
|
|
1405
|
+
unknownKeys
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function loadProjects(d) {
|
|
1409
|
+
const rows = d.prepare("SELECT * FROM projects").all();
|
|
1410
|
+
return rows.map(rowToProject);
|
|
1411
|
+
}
|
|
1412
|
+
function loadWorkflows(d) {
|
|
1413
|
+
const rows = d.prepare("SELECT * FROM workflows").all();
|
|
1414
|
+
return rows.map(rowToWorkflow);
|
|
1415
|
+
}
|
|
1416
|
+
function loadAgentCommands(d) {
|
|
1417
|
+
const rows = d.prepare("SELECT * FROM agent_commands").all();
|
|
1418
|
+
const result = {};
|
|
1419
|
+
for (const r of rows) {
|
|
1420
|
+
result[r.agent_type] = {
|
|
1421
|
+
command: r.command,
|
|
1422
|
+
args: JSON.parse(r.args),
|
|
1423
|
+
...r.headless_args != null && { headlessArgs: JSON.parse(r.headless_args) },
|
|
1424
|
+
...r.fallback_command != null && { fallbackCommand: r.fallback_command },
|
|
1425
|
+
...r.fallback_args != null && { fallbackArgs: JSON.parse(r.fallback_args) }
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
return result;
|
|
1429
|
+
}
|
|
1430
|
+
function loadRemoteHosts(d) {
|
|
1431
|
+
const rows = d.prepare("SELECT * FROM remote_hosts").all();
|
|
1432
|
+
return rows.map((r) => ({
|
|
1433
|
+
id: r.id,
|
|
1434
|
+
label: r.label,
|
|
1435
|
+
hostname: r.hostname,
|
|
1436
|
+
user: r.user,
|
|
1437
|
+
port: r.port,
|
|
1438
|
+
...r.auth_method != null && { authMethod: r.auth_method },
|
|
1439
|
+
...r.ssh_key_path != null && { sshKeyPath: r.ssh_key_path },
|
|
1440
|
+
...r.credential_id != null && { credentialId: r.credential_id },
|
|
1441
|
+
...r.encrypted_password != null && { encryptedPassword: r.encrypted_password },
|
|
1442
|
+
...r.ssh_options != null && { sshOptions: r.ssh_options }
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
function loadTasks(d) {
|
|
1446
|
+
const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
|
|
1447
|
+
return rows.map(rowToTask);
|
|
1448
|
+
}
|
|
1449
|
+
function loadWorkspaces(d) {
|
|
1450
|
+
const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
|
|
1451
|
+
return rows.map(rowToWorkspace);
|
|
1452
|
+
}
|
|
1453
|
+
function saveConfig(config) {
|
|
1454
|
+
const d = getDb();
|
|
1455
|
+
const run = d.transaction(() => {
|
|
1456
|
+
d.prepare("DELETE FROM defaults").run();
|
|
1457
|
+
const insertDefault = d.prepare("INSERT INTO defaults (key, value) VALUES (?, ?)");
|
|
1458
|
+
for (const [key, value] of Object.entries(config.defaults)) {
|
|
1459
|
+
if (value !== void 0) {
|
|
1460
|
+
insertDefault.run(key, JSON.stringify(value));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const existingIds = /* @__PURE__ */ new Map();
|
|
1464
|
+
for (const r of d.prepare("SELECT name, project_id FROM projects").all()) {
|
|
1465
|
+
if (r.project_id) existingIds.set(r.name, r.project_id);
|
|
1466
|
+
}
|
|
1467
|
+
d.prepare("DELETE FROM projects").run();
|
|
1468
|
+
const insertProject = d.prepare(
|
|
1469
|
+
"INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1470
|
+
);
|
|
1471
|
+
for (const p of config.projects) {
|
|
1472
|
+
const projectId = p.projectId ?? existingIds.get(p.name) ?? randomUUID();
|
|
1473
|
+
insertProject.run(
|
|
1474
|
+
p.name,
|
|
1475
|
+
p.path,
|
|
1476
|
+
JSON.stringify(p.preferredAgents),
|
|
1477
|
+
p.icon ?? null,
|
|
1478
|
+
p.iconColor ?? null,
|
|
1479
|
+
p.hostIds ? JSON.stringify(p.hostIds) : null,
|
|
1480
|
+
p.workspaceId ?? "personal",
|
|
1481
|
+
projectId
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
d.prepare("DELETE FROM workflows").run();
|
|
1485
|
+
const insertWorkflow = d.prepare(
|
|
1486
|
+
`INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
|
|
1487
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1488
|
+
);
|
|
1489
|
+
for (const w of config.workflows ?? []) {
|
|
1490
|
+
insertWorkflow.run(
|
|
1491
|
+
w.id,
|
|
1492
|
+
w.name,
|
|
1493
|
+
w.icon,
|
|
1494
|
+
w.iconColor,
|
|
1495
|
+
JSON.stringify(w.nodes),
|
|
1496
|
+
JSON.stringify(w.edges),
|
|
1497
|
+
w.enabled ? 1 : 0,
|
|
1498
|
+
w.lastRunAt ?? null,
|
|
1499
|
+
w.lastRunStatus ?? null,
|
|
1500
|
+
w.staggerDelayMs ?? null,
|
|
1501
|
+
w.workspaceId ?? "personal"
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
d.prepare("DELETE FROM agent_commands").run();
|
|
1505
|
+
const insertAgent = d.prepare(
|
|
1506
|
+
"INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1507
|
+
);
|
|
1508
|
+
if (config.agentCommands) {
|
|
1509
|
+
for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
|
|
1510
|
+
if (cmd) {
|
|
1511
|
+
insertAgent.run(
|
|
1512
|
+
agentType,
|
|
1513
|
+
cmd.command,
|
|
1514
|
+
JSON.stringify(cmd.args),
|
|
1515
|
+
cmd.headlessArgs ? JSON.stringify(cmd.headlessArgs) : null,
|
|
1516
|
+
cmd.fallbackCommand ?? null,
|
|
1517
|
+
cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
d.prepare("DELETE FROM remote_hosts").run();
|
|
1523
|
+
const insertHost = d.prepare(
|
|
1524
|
+
"INSERT INTO remote_hosts (id, label, hostname, user, port, auth_method, ssh_key_path, credential_id, encrypted_password, ssh_options) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1525
|
+
);
|
|
1526
|
+
for (const h of config.remoteHosts ?? []) {
|
|
1527
|
+
insertHost.run(
|
|
1528
|
+
h.id,
|
|
1529
|
+
h.label,
|
|
1530
|
+
h.hostname,
|
|
1531
|
+
h.user,
|
|
1532
|
+
h.port,
|
|
1533
|
+
h.authMethod ?? "agent",
|
|
1534
|
+
h.sshKeyPath ?? null,
|
|
1535
|
+
h.credentialId ?? null,
|
|
1536
|
+
h.encryptedPassword ?? null,
|
|
1537
|
+
h.sshOptions ?? null
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
d.prepare("DELETE FROM tasks").run();
|
|
1541
|
+
const insertTask = d.prepare(
|
|
1542
|
+
`INSERT INTO tasks (id, project_name, title, description, status, "order", assigned_session_id, assigned_agent, agent_session_id, branch, use_worktree, created_at, updated_at, completed_at, source_connector_id, source_external_url, source_external_id)
|
|
1543
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1544
|
+
);
|
|
1545
|
+
for (const t of config.tasks ?? []) {
|
|
1546
|
+
insertTask.run(
|
|
1547
|
+
t.id,
|
|
1548
|
+
t.projectName,
|
|
1549
|
+
t.title,
|
|
1550
|
+
t.description,
|
|
1551
|
+
t.status,
|
|
1552
|
+
t.order,
|
|
1553
|
+
t.assignedSessionId ?? null,
|
|
1554
|
+
t.assignedAgent ?? null,
|
|
1555
|
+
t.agentSessionId ?? null,
|
|
1556
|
+
t.branch ?? null,
|
|
1557
|
+
t.useWorktree ? 1 : 0,
|
|
1558
|
+
t.createdAt,
|
|
1559
|
+
t.updatedAt,
|
|
1560
|
+
t.completedAt ?? null,
|
|
1561
|
+
t.sourceConnectorId ?? null,
|
|
1562
|
+
t.sourceExternalUrl ?? null,
|
|
1563
|
+
t.sourceExternalId ?? null
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
d.prepare("DELETE FROM workspaces").run();
|
|
1567
|
+
const insertWorkspace = d.prepare(
|
|
1568
|
+
`INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
|
|
1569
|
+
);
|
|
1570
|
+
for (const ws of config.workspaces ?? [DEFAULT_WORKSPACE]) {
|
|
1571
|
+
insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order);
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
run();
|
|
1575
|
+
}
|
|
1576
|
+
function dbListTasks(projectName, status) {
|
|
1577
|
+
const d = getDb();
|
|
1578
|
+
let sql = "SELECT * FROM tasks";
|
|
1579
|
+
const params = [];
|
|
1580
|
+
const clauses = [];
|
|
1581
|
+
if (projectName) {
|
|
1582
|
+
clauses.push("project_name = ?");
|
|
1583
|
+
params.push(projectName);
|
|
1584
|
+
}
|
|
1585
|
+
if (status) {
|
|
1586
|
+
clauses.push("status = ?");
|
|
1587
|
+
params.push(status);
|
|
1588
|
+
}
|
|
1589
|
+
if (clauses.length) sql += " WHERE " + clauses.join(" AND ");
|
|
1590
|
+
sql += ' ORDER BY "order"';
|
|
1591
|
+
const rows = d.prepare(sql).all(...params);
|
|
1592
|
+
return rows.map(rowToTask);
|
|
1593
|
+
}
|
|
1594
|
+
function dbGetTask(id) {
|
|
1595
|
+
const row = getDb().prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
1596
|
+
return row ? rowToTask(row) : null;
|
|
1597
|
+
}
|
|
1598
|
+
function dbInsertTask(task) {
|
|
1599
|
+
getDb().prepare(
|
|
1600
|
+
`INSERT INTO tasks (id, project_name, title, description, status, "order", assigned_session_id, assigned_agent, agent_session_id, branch, use_worktree, created_at, updated_at, completed_at, source_connector_id, source_external_url, source_external_id)
|
|
1601
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1602
|
+
).run(
|
|
1603
|
+
task.id,
|
|
1604
|
+
task.projectName,
|
|
1605
|
+
task.title,
|
|
1606
|
+
task.description,
|
|
1607
|
+
task.status,
|
|
1608
|
+
task.order,
|
|
1609
|
+
task.assignedSessionId ?? null,
|
|
1610
|
+
task.assignedAgent ?? null,
|
|
1611
|
+
task.agentSessionId ?? null,
|
|
1612
|
+
task.branch ?? null,
|
|
1613
|
+
task.useWorktree ? 1 : 0,
|
|
1614
|
+
task.createdAt,
|
|
1615
|
+
task.updatedAt,
|
|
1616
|
+
task.completedAt ?? null,
|
|
1617
|
+
task.sourceConnectorId ?? null,
|
|
1618
|
+
task.sourceExternalUrl ?? null,
|
|
1619
|
+
task.sourceExternalId ?? null
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
function dbUpdateTask(id, updates) {
|
|
1623
|
+
const sets = [];
|
|
1624
|
+
const params = [];
|
|
1625
|
+
if (updates.title !== void 0) {
|
|
1626
|
+
sets.push("title = ?");
|
|
1627
|
+
params.push(updates.title);
|
|
1628
|
+
}
|
|
1629
|
+
if (updates.description !== void 0) {
|
|
1630
|
+
sets.push("description = ?");
|
|
1631
|
+
params.push(updates.description);
|
|
1632
|
+
}
|
|
1633
|
+
if (updates.status !== void 0) {
|
|
1634
|
+
sets.push("status = ?");
|
|
1635
|
+
params.push(updates.status);
|
|
1636
|
+
}
|
|
1637
|
+
if (updates.order !== void 0) {
|
|
1638
|
+
sets.push('"order" = ?');
|
|
1639
|
+
params.push(updates.order);
|
|
1640
|
+
}
|
|
1641
|
+
if (updates.branch !== void 0) {
|
|
1642
|
+
sets.push("branch = ?");
|
|
1643
|
+
params.push(updates.branch);
|
|
1644
|
+
}
|
|
1645
|
+
if (updates.useWorktree !== void 0) {
|
|
1646
|
+
sets.push("use_worktree = ?");
|
|
1647
|
+
params.push(updates.useWorktree ? 1 : 0);
|
|
1648
|
+
}
|
|
1649
|
+
if (updates.assignedAgent !== void 0) {
|
|
1650
|
+
sets.push("assigned_agent = ?");
|
|
1651
|
+
params.push(updates.assignedAgent);
|
|
1652
|
+
}
|
|
1653
|
+
if (updates.assignedSessionId !== void 0) {
|
|
1654
|
+
sets.push("assigned_session_id = ?");
|
|
1655
|
+
params.push(updates.assignedSessionId);
|
|
1656
|
+
}
|
|
1657
|
+
if (updates.agentSessionId !== void 0) {
|
|
1658
|
+
sets.push("agent_session_id = ?");
|
|
1659
|
+
params.push(updates.agentSessionId);
|
|
1660
|
+
}
|
|
1661
|
+
if (updates.updatedAt !== void 0) {
|
|
1662
|
+
sets.push("updated_at = ?");
|
|
1663
|
+
params.push(updates.updatedAt);
|
|
1664
|
+
}
|
|
1665
|
+
if ("completedAt" in updates) {
|
|
1666
|
+
sets.push("completed_at = ?");
|
|
1667
|
+
params.push(updates.completedAt ?? null);
|
|
1668
|
+
}
|
|
1669
|
+
if (updates.sourceConnectorId !== void 0) {
|
|
1670
|
+
sets.push("source_connector_id = ?");
|
|
1671
|
+
params.push(updates.sourceConnectorId);
|
|
1672
|
+
}
|
|
1673
|
+
if (updates.sourceExternalUrl !== void 0) {
|
|
1674
|
+
sets.push("source_external_url = ?");
|
|
1675
|
+
params.push(updates.sourceExternalUrl);
|
|
1676
|
+
}
|
|
1677
|
+
if (updates.sourceExternalId !== void 0) {
|
|
1678
|
+
sets.push("source_external_id = ?");
|
|
1679
|
+
params.push(updates.sourceExternalId);
|
|
1680
|
+
}
|
|
1681
|
+
if (sets.length === 0) return;
|
|
1682
|
+
params.push(id);
|
|
1683
|
+
getDb().prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
1684
|
+
}
|
|
1685
|
+
function dbDeleteTask(id) {
|
|
1686
|
+
getDb().prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
1687
|
+
}
|
|
1688
|
+
function dbGetMaxTaskOrder(projectName) {
|
|
1689
|
+
const row = getDb().prepare('SELECT MAX("order") as m FROM tasks WHERE project_name = ?').get(projectName);
|
|
1690
|
+
return row.m ?? -1;
|
|
1691
|
+
}
|
|
1692
|
+
function dbListProjects() {
|
|
1693
|
+
const rows = getDb().prepare("SELECT * FROM projects").all();
|
|
1694
|
+
return rows.map(rowToProject);
|
|
1695
|
+
}
|
|
1696
|
+
function dbGetProject(name) {
|
|
1697
|
+
const row = getDb().prepare("SELECT * FROM projects WHERE name = ?").get(name);
|
|
1698
|
+
return row ? rowToProject(row) : null;
|
|
1699
|
+
}
|
|
1700
|
+
function dbInsertProject(project) {
|
|
1701
|
+
const projectId = project.projectId ?? randomUUID();
|
|
1702
|
+
getDb().prepare(
|
|
1703
|
+
"INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1704
|
+
).run(
|
|
1705
|
+
project.name,
|
|
1706
|
+
project.path,
|
|
1707
|
+
JSON.stringify(project.preferredAgents),
|
|
1708
|
+
project.icon ?? null,
|
|
1709
|
+
project.iconColor ?? null,
|
|
1710
|
+
project.hostIds ? JSON.stringify(project.hostIds) : null,
|
|
1711
|
+
project.workspaceId ?? "personal",
|
|
1712
|
+
projectId
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
function dbUpdateProject(name, updates) {
|
|
1716
|
+
const sets = [];
|
|
1717
|
+
const params = [];
|
|
1718
|
+
if (updates.path !== void 0) {
|
|
1719
|
+
sets.push("path = ?");
|
|
1720
|
+
params.push(updates.path);
|
|
1721
|
+
}
|
|
1722
|
+
if (updates.preferredAgents !== void 0) {
|
|
1723
|
+
sets.push("preferred_agents = ?");
|
|
1724
|
+
params.push(JSON.stringify(updates.preferredAgents));
|
|
1725
|
+
}
|
|
1726
|
+
if (updates.icon !== void 0) {
|
|
1727
|
+
sets.push("icon = ?");
|
|
1728
|
+
params.push(updates.icon);
|
|
1729
|
+
}
|
|
1730
|
+
if (updates.iconColor !== void 0) {
|
|
1731
|
+
sets.push("icon_color = ?");
|
|
1732
|
+
params.push(updates.iconColor);
|
|
1733
|
+
}
|
|
1734
|
+
if (updates.hostIds !== void 0) {
|
|
1735
|
+
sets.push("host_ids = ?");
|
|
1736
|
+
params.push(JSON.stringify(updates.hostIds));
|
|
1737
|
+
}
|
|
1738
|
+
if (updates.workspaceId !== void 0) {
|
|
1739
|
+
sets.push("workspace_id = ?");
|
|
1740
|
+
params.push(updates.workspaceId);
|
|
1741
|
+
}
|
|
1742
|
+
if (sets.length === 0) return;
|
|
1743
|
+
params.push(name);
|
|
1744
|
+
getDb().prepare(`UPDATE projects SET ${sets.join(", ")} WHERE name = ?`).run(...params);
|
|
1745
|
+
}
|
|
1746
|
+
function dbDeleteProject(name) {
|
|
1747
|
+
const d = getDb();
|
|
1748
|
+
d.transaction(() => {
|
|
1749
|
+
d.prepare("DELETE FROM tasks WHERE project_name = ?").run(name);
|
|
1750
|
+
d.prepare("DELETE FROM projects WHERE name = ?").run(name);
|
|
1751
|
+
})();
|
|
1752
|
+
}
|
|
1753
|
+
function dbListWorkflows() {
|
|
1754
|
+
const rows = getDb().prepare("SELECT * FROM workflows").all();
|
|
1755
|
+
return rows.map(rowToWorkflow);
|
|
1756
|
+
}
|
|
1757
|
+
function dbInsertWorkflow(workflow) {
|
|
1758
|
+
getDb().prepare(
|
|
1759
|
+
`INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
|
|
1760
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1761
|
+
).run(
|
|
1762
|
+
workflow.id,
|
|
1763
|
+
workflow.name,
|
|
1764
|
+
workflow.icon,
|
|
1765
|
+
workflow.iconColor,
|
|
1766
|
+
JSON.stringify(workflow.nodes),
|
|
1767
|
+
JSON.stringify(workflow.edges),
|
|
1768
|
+
workflow.enabled ? 1 : 0,
|
|
1769
|
+
workflow.lastRunAt ?? null,
|
|
1770
|
+
workflow.lastRunStatus ?? null,
|
|
1771
|
+
workflow.staggerDelayMs ?? null,
|
|
1772
|
+
workflow.workspaceId ?? "personal"
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
function dbUpdateWorkflow(id, updates) {
|
|
1776
|
+
const sets = [];
|
|
1777
|
+
const params = [];
|
|
1778
|
+
if (updates.name !== void 0) {
|
|
1779
|
+
sets.push("name = ?");
|
|
1780
|
+
params.push(updates.name);
|
|
1781
|
+
}
|
|
1782
|
+
if (updates.nodes !== void 0) {
|
|
1783
|
+
sets.push("nodes = ?");
|
|
1784
|
+
params.push(JSON.stringify(updates.nodes));
|
|
1785
|
+
}
|
|
1786
|
+
if (updates.edges !== void 0) {
|
|
1787
|
+
sets.push("edges = ?");
|
|
1788
|
+
params.push(JSON.stringify(updates.edges));
|
|
1789
|
+
}
|
|
1790
|
+
if (updates.icon !== void 0) {
|
|
1791
|
+
sets.push("icon = ?");
|
|
1792
|
+
params.push(updates.icon);
|
|
1793
|
+
}
|
|
1794
|
+
if (updates.iconColor !== void 0) {
|
|
1795
|
+
sets.push("icon_color = ?");
|
|
1796
|
+
params.push(updates.iconColor);
|
|
1797
|
+
}
|
|
1798
|
+
if (updates.enabled !== void 0) {
|
|
1799
|
+
sets.push("enabled = ?");
|
|
1800
|
+
params.push(updates.enabled ? 1 : 0);
|
|
1801
|
+
}
|
|
1802
|
+
if (updates.staggerDelayMs !== void 0) {
|
|
1803
|
+
sets.push("stagger_delay_ms = ?");
|
|
1804
|
+
params.push(updates.staggerDelayMs);
|
|
1805
|
+
}
|
|
1806
|
+
if (updates.workspaceId !== void 0) {
|
|
1807
|
+
sets.push("workspace_id = ?");
|
|
1808
|
+
params.push(updates.workspaceId);
|
|
1809
|
+
}
|
|
1810
|
+
if (sets.length === 0) return;
|
|
1811
|
+
params.push(id);
|
|
1812
|
+
getDb().prepare(`UPDATE workflows SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
1813
|
+
}
|
|
1814
|
+
function dbDeleteWorkflow(id) {
|
|
1815
|
+
getDb().prepare("DELETE FROM workflows WHERE id = ?").run(id);
|
|
1816
|
+
}
|
|
1817
|
+
function dbListWorkspaces() {
|
|
1818
|
+
const rows = getDb().prepare('SELECT * FROM workspaces ORDER BY "order"').all();
|
|
1819
|
+
return rows.map(rowToWorkspace);
|
|
1820
|
+
}
|
|
1821
|
+
function dbInsertWorkspace(workspace) {
|
|
1822
|
+
getDb().prepare(`INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`).run(
|
|
1823
|
+
workspace.id,
|
|
1824
|
+
workspace.name,
|
|
1825
|
+
workspace.icon ?? null,
|
|
1826
|
+
workspace.iconColor ?? null,
|
|
1827
|
+
workspace.order
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
function dbUpdateWorkspace(id, updates) {
|
|
1831
|
+
const sets = [];
|
|
1832
|
+
const params = [];
|
|
1833
|
+
if (updates.name !== void 0) {
|
|
1834
|
+
sets.push("name = ?");
|
|
1835
|
+
params.push(updates.name);
|
|
1836
|
+
}
|
|
1837
|
+
if (updates.icon !== void 0) {
|
|
1838
|
+
sets.push("icon = ?");
|
|
1839
|
+
params.push(updates.icon);
|
|
1840
|
+
}
|
|
1841
|
+
if (updates.iconColor !== void 0) {
|
|
1842
|
+
sets.push("icon_color = ?");
|
|
1843
|
+
params.push(updates.iconColor);
|
|
1844
|
+
}
|
|
1845
|
+
if (updates.order !== void 0) {
|
|
1846
|
+
sets.push('"order" = ?');
|
|
1847
|
+
params.push(updates.order);
|
|
1848
|
+
}
|
|
1849
|
+
if (sets.length === 0) return;
|
|
1850
|
+
params.push(id);
|
|
1851
|
+
getDb().prepare(`UPDATE workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
1852
|
+
}
|
|
1853
|
+
function dbDeleteWorkspace(id) {
|
|
1854
|
+
const d = getDb();
|
|
1855
|
+
d.transaction(() => {
|
|
1856
|
+
d.prepare("UPDATE projects SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
|
|
1857
|
+
d.prepare("UPDATE workflows SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
|
|
1858
|
+
d.prepare("DELETE FROM workspaces WHERE id = ?").run(id);
|
|
1859
|
+
})();
|
|
1860
|
+
}
|
|
1861
|
+
function rowToTask(r) {
|
|
1862
|
+
return {
|
|
1863
|
+
id: r.id,
|
|
1864
|
+
projectName: r.project_name,
|
|
1865
|
+
title: r.title,
|
|
1866
|
+
description: r.description,
|
|
1867
|
+
status: r.status,
|
|
1868
|
+
order: r.order,
|
|
1869
|
+
...r.assigned_session_id != null && { assignedSessionId: r.assigned_session_id },
|
|
1870
|
+
...r.assigned_agent != null && { assignedAgent: r.assigned_agent },
|
|
1871
|
+
...r.agent_session_id != null && { agentSessionId: r.agent_session_id },
|
|
1872
|
+
...r.branch != null && { branch: r.branch },
|
|
1873
|
+
...r.use_worktree != null && r.use_worktree !== 0 && { useWorktree: true },
|
|
1874
|
+
createdAt: r.created_at,
|
|
1875
|
+
updatedAt: r.updated_at,
|
|
1876
|
+
...r.completed_at != null && { completedAt: r.completed_at },
|
|
1877
|
+
...r.source_connector_id != null && { sourceConnectorId: r.source_connector_id },
|
|
1878
|
+
...r.source_external_url != null && { sourceExternalUrl: r.source_external_url },
|
|
1879
|
+
...r.source_external_id != null && {
|
|
1880
|
+
sourceExternalId: r.source_external_id
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
function rowToProject(r) {
|
|
1885
|
+
return {
|
|
1886
|
+
name: r.name,
|
|
1887
|
+
path: r.path,
|
|
1888
|
+
preferredAgents: JSON.parse(r.preferred_agents),
|
|
1889
|
+
...r.icon != null && { icon: r.icon },
|
|
1890
|
+
...r.icon_color != null && { iconColor: r.icon_color },
|
|
1891
|
+
...r.host_ids != null && { hostIds: JSON.parse(r.host_ids) },
|
|
1892
|
+
workspaceId: r.workspace_id ?? "personal",
|
|
1893
|
+
...r.project_id != null && r.project_id !== "" && { projectId: r.project_id }
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
function rowToWorkflow(r) {
|
|
1897
|
+
return {
|
|
1898
|
+
id: r.id,
|
|
1899
|
+
name: r.name,
|
|
1900
|
+
icon: r.icon,
|
|
1901
|
+
iconColor: r.icon_color,
|
|
1902
|
+
nodes: JSON.parse(r.nodes),
|
|
1903
|
+
edges: JSON.parse(r.edges),
|
|
1904
|
+
enabled: r.enabled === 1,
|
|
1905
|
+
...r.last_run_at != null && { lastRunAt: r.last_run_at },
|
|
1906
|
+
...r.last_run_status != null && { lastRunStatus: r.last_run_status },
|
|
1907
|
+
...r.stagger_delay_ms != null && { staggerDelayMs: r.stagger_delay_ms },
|
|
1908
|
+
workspaceId: r.workspace_id ?? "personal"
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
function rowToWorkspace(r) {
|
|
1912
|
+
return {
|
|
1913
|
+
id: r.id,
|
|
1914
|
+
name: r.name,
|
|
1915
|
+
...r.icon != null && { icon: r.icon },
|
|
1916
|
+
...r.icon_color != null && { iconColor: r.icon_color },
|
|
1917
|
+
order: r.order
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function mapWorkflowNodeRow(n) {
|
|
1921
|
+
return {
|
|
1922
|
+
nodeId: n.node_id,
|
|
1923
|
+
status: n.status,
|
|
1924
|
+
...n.started_at != null && { startedAt: n.started_at },
|
|
1925
|
+
...n.completed_at != null && { completedAt: n.completed_at },
|
|
1926
|
+
...n.session_id != null && { sessionId: n.session_id },
|
|
1927
|
+
...n.error != null && { error: n.error },
|
|
1928
|
+
...n.logs != null && { logs: n.logs },
|
|
1929
|
+
...n.task_id != null && { taskId: n.task_id },
|
|
1930
|
+
...n.agent_session_id != null && { agentSessionId: n.agent_session_id },
|
|
1931
|
+
...n.agent_type != null && { agentType: n.agent_type },
|
|
1932
|
+
...n.project_name != null && { projectName: n.project_name },
|
|
1933
|
+
...n.project_path != null && { projectPath: n.project_path },
|
|
1934
|
+
...n.approved_at != null && { approvedAt: n.approved_at }
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function loadWorkflowRunNodes(d, runIds) {
|
|
1938
|
+
if (runIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1939
|
+
const placeholders = runIds.map(() => "?").join(",");
|
|
1940
|
+
const rows = d.prepare(`SELECT * FROM workflow_run_nodes WHERE run_id IN (${placeholders})`).all(...runIds);
|
|
1941
|
+
const byRunId = /* @__PURE__ */ new Map();
|
|
1942
|
+
for (const row of rows) {
|
|
1943
|
+
const arr = byRunId.get(row.run_id);
|
|
1944
|
+
if (arr) arr.push(mapWorkflowNodeRow(row));
|
|
1945
|
+
else byRunId.set(row.run_id, [mapWorkflowNodeRow(row)]);
|
|
1946
|
+
}
|
|
1947
|
+
return byRunId;
|
|
1948
|
+
}
|
|
1949
|
+
function listWorkflowRuns(workflowId, limit = 20) {
|
|
1950
|
+
const d = getDb();
|
|
1951
|
+
const rows = d.prepare("SELECT * FROM workflow_runs WHERE workflow_id = ? ORDER BY started_at DESC LIMIT ?").all(workflowId, limit);
|
|
1952
|
+
const nodesByRunId = loadWorkflowRunNodes(
|
|
1953
|
+
d,
|
|
1954
|
+
rows.map((r) => r.id)
|
|
1955
|
+
);
|
|
1956
|
+
return rows.map((r) => ({
|
|
1957
|
+
workflowId: r.workflow_id,
|
|
1958
|
+
startedAt: r.started_at,
|
|
1959
|
+
...r.completed_at != null && { completedAt: r.completed_at },
|
|
1960
|
+
status: r.status,
|
|
1961
|
+
...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
|
|
1962
|
+
nodeStates: nodesByRunId.get(r.id) ?? []
|
|
1963
|
+
}));
|
|
1964
|
+
}
|
|
1965
|
+
function listWorkflowRunsByTask(taskId, limit = 20) {
|
|
1966
|
+
const d = getDb();
|
|
1967
|
+
const rows = d.prepare(
|
|
1968
|
+
`
|
|
1969
|
+
SELECT DISTINCT wr.*, w.name as workflow_name
|
|
1970
|
+
FROM workflow_runs wr
|
|
1971
|
+
LEFT JOIN workflows w ON w.id = wr.workflow_id
|
|
1972
|
+
WHERE wr.trigger_task_id = ?
|
|
1973
|
+
OR wr.id IN (SELECT run_id FROM workflow_run_nodes WHERE task_id = ?)
|
|
1974
|
+
ORDER BY wr.started_at DESC
|
|
1975
|
+
LIMIT ?
|
|
1976
|
+
`
|
|
1977
|
+
).all(taskId, taskId, limit);
|
|
1978
|
+
const nodesByRunId = loadWorkflowRunNodes(
|
|
1979
|
+
d,
|
|
1980
|
+
rows.map((r) => r.id)
|
|
1981
|
+
);
|
|
1982
|
+
return rows.map((r) => ({
|
|
1983
|
+
workflowId: r.workflow_id,
|
|
1984
|
+
startedAt: r.started_at,
|
|
1985
|
+
...r.completed_at != null && { completedAt: r.completed_at },
|
|
1986
|
+
status: r.status,
|
|
1987
|
+
...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
|
|
1988
|
+
...r.workflow_name != null && { workflowName: r.workflow_name },
|
|
1989
|
+
nodeStates: nodesByRunId.get(r.id) ?? []
|
|
1990
|
+
}));
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// ../server/src/config-manager.ts
|
|
1994
|
+
var DB_DIR = getDataDir();
|
|
1995
|
+
var ConfigManager = class {
|
|
1996
|
+
changeCallbacks = [];
|
|
1997
|
+
dbWatcher = null;
|
|
1998
|
+
debounceTimer = null;
|
|
1999
|
+
cachedConfig = null;
|
|
2000
|
+
init() {
|
|
2001
|
+
initDatabase();
|
|
2002
|
+
}
|
|
2003
|
+
close() {
|
|
2004
|
+
this.stopWatchingDb();
|
|
2005
|
+
closeDatabase();
|
|
2006
|
+
}
|
|
2007
|
+
/** Полный сброс — удаляет БД и переинициализирует пустую схему.
|
|
2008
|
+
* Инвалидирует кэш и будит подписчиков, чтобы UI увидел пустой конфиг. */
|
|
2009
|
+
factoryReset() {
|
|
2010
|
+
this.stopWatchingDb();
|
|
2011
|
+
this.cachedConfig = null;
|
|
2012
|
+
factoryResetDatabase();
|
|
2013
|
+
this.notifyChanged();
|
|
2014
|
+
}
|
|
2015
|
+
loadConfig() {
|
|
2016
|
+
if (this.cachedConfig) return this.cachedConfig;
|
|
2017
|
+
try {
|
|
2018
|
+
const config = loadConfig();
|
|
2019
|
+
const seeded = this.seedCodexDefaults(this.seedClaudeDefaults(config));
|
|
2020
|
+
this.cachedConfig = seeded;
|
|
2021
|
+
return seeded;
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
|
|
2024
|
+
return {
|
|
2025
|
+
version: 1,
|
|
2026
|
+
defaults: {
|
|
2027
|
+
shell: process.platform === "win32" ? process.env.COMSPEC || "powershell.exe" : process.env.SHELL || "/bin/zsh",
|
|
2028
|
+
fontSize: 13,
|
|
2029
|
+
theme: "dark"
|
|
2030
|
+
},
|
|
2031
|
+
projects: [],
|
|
2032
|
+
agentCommands: { ...DEFAULT_AGENT_COMMANDS },
|
|
2033
|
+
workflows: [],
|
|
2034
|
+
tasks: []
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
/** При первом запуске (или после factory-reset) в DB нет ни
|
|
2039
|
+
* `claudeDefaultModel`, ни `claudeDefaultEffort`, и пользователь видит в
|
|
2040
|
+
* пикерах настроек fallback-значения через `?? CLAUDE_DEFAULT_MODEL`. До
|
|
2041
|
+
* фикса это приводило к расхождению: UI показывал «Sonnet 4.6 / medium»,
|
|
2042
|
+
* а сервер при создании сессии не находил дефолтов и не слал ни `/model`,
|
|
2043
|
+
* ни `/effort`. Теперь записываем стандартные значения в DB сразу, чтобы
|
|
2044
|
+
* источник правды был один. */
|
|
2045
|
+
seedClaudeDefaults(config) {
|
|
2046
|
+
const needsModel = config.defaults.claudeDefaultModel == null;
|
|
2047
|
+
const needsEffort = config.defaults.claudeDefaultEffort == null;
|
|
2048
|
+
if (!needsModel && !needsEffort) return config;
|
|
2049
|
+
const seeded = {
|
|
2050
|
+
...config,
|
|
2051
|
+
defaults: {
|
|
2052
|
+
...config.defaults,
|
|
2053
|
+
...needsModel ? { claudeDefaultModel: CLAUDE_DEFAULT_MODEL_ID } : {},
|
|
2054
|
+
...needsEffort ? { claudeDefaultEffort: CLAUDE_DEFAULT_EFFORT } : {}
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
try {
|
|
2058
|
+
saveConfig(seeded);
|
|
2059
|
+
logger_default.info(
|
|
2060
|
+
`[config-manager] seeded claude defaults: model=${needsModel ? CLAUDE_DEFAULT_MODEL_ID : "(kept)"} effort=${needsEffort ? CLAUDE_DEFAULT_EFFORT : "(kept)"}`
|
|
2061
|
+
);
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
logger_default.warn(
|
|
2064
|
+
`[config-manager] failed to persist seeded claude defaults: ${err.message}`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
return seeded;
|
|
2068
|
+
}
|
|
2069
|
+
/** Аналог seedClaudeDefaults для codex: при первом запуске прописывает в DB
|
|
2070
|
+
* значения по умолчанию, чтобы UI и сервер видели одно и то же. Заодно
|
|
2071
|
+
* мигрирует устаревшие id (см. CODEX_OBSOLETE_MODEL_IDS) на актуальный
|
|
2072
|
+
* дефолт — иначе пикер в UI показывал бы пустую опцию, а сервер слал бы в
|
|
2073
|
+
* codex CLI несуществующий --model и сессия падала бы при старте. */
|
|
2074
|
+
seedCodexDefaults(config) {
|
|
2075
|
+
const stored = config.defaults.codexDefaultModel;
|
|
2076
|
+
const needsModel = stored == null || typeof stored === "string" && CODEX_OBSOLETE_MODEL_IDS.includes(stored);
|
|
2077
|
+
const needsEffort = config.defaults.codexDefaultEffort == null;
|
|
2078
|
+
if (!needsModel && !needsEffort) return config;
|
|
2079
|
+
const seeded = {
|
|
2080
|
+
...config,
|
|
2081
|
+
defaults: {
|
|
2082
|
+
...config.defaults,
|
|
2083
|
+
...needsModel ? { codexDefaultModel: CODEX_DEFAULT_MODEL_ID } : {},
|
|
2084
|
+
...needsEffort ? { codexDefaultEffort: CODEX_DEFAULT_EFFORT } : {}
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
try {
|
|
2088
|
+
saveConfig(seeded);
|
|
2089
|
+
logger_default.info(
|
|
2090
|
+
`[config-manager] seeded codex defaults: model=${needsModel ? CODEX_DEFAULT_MODEL_ID : "(kept)"} effort=${needsEffort ? CODEX_DEFAULT_EFFORT : "(kept)"}`
|
|
2091
|
+
);
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
logger_default.warn(
|
|
2094
|
+
`[config-manager] failed to persist seeded codex defaults: ${err.message}`
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
return seeded;
|
|
2098
|
+
}
|
|
2099
|
+
saveConfig(config) {
|
|
2100
|
+
try {
|
|
2101
|
+
saveConfig(config);
|
|
2102
|
+
this.cachedConfig = null;
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
logger_default.error("[config-manager] saveConfig failed:", err);
|
|
2105
|
+
throw err;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
/** Register a callback for when config changes from within the main process */
|
|
2109
|
+
onConfigChanged(callback) {
|
|
2110
|
+
this.changeCallbacks.push(callback);
|
|
2111
|
+
}
|
|
2112
|
+
/** Notify all registered callbacks (call after main-process config mutations) */
|
|
2113
|
+
notifyChanged() {
|
|
2114
|
+
this.cachedConfig = null;
|
|
2115
|
+
const config = this.loadConfig();
|
|
2116
|
+
for (const cb of this.changeCallbacks) {
|
|
2117
|
+
cb(config);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Watch for external DB writes (e.g. MCP stdio process).
|
|
2122
|
+
* Слушаем ТОЛЬКО `.db-signal` — явный сигнальный файл, который трогают
|
|
2123
|
+
* внешние писатели через `dbSignalChange()`. Внутренние мутации сервера
|
|
2124
|
+
* и так зовут `notifyChanged()` напрямую (см. saveConfig/notifyChanged
|
|
2125
|
+
* call-sites). Раньше watcher следил ещё за `.db` и `.db-wal`, и любая
|
|
2126
|
+
* запись в БД (session-persistence, tasks, etc.) → менялся WAL-файл →
|
|
2127
|
+
* срабатывал debounce 300мс → notifyChanged → реконфигурация всех
|
|
2128
|
+
* менеджеров + broadcast клиентам. На активной сессии это давало
|
|
2129
|
+
* непрекращающийся спам в логах и лишние IPC-броадкасты.
|
|
2130
|
+
*/
|
|
2131
|
+
watchDb() {
|
|
2132
|
+
if (this.dbWatcher) return;
|
|
2133
|
+
try {
|
|
2134
|
+
this.dbWatcher = fs4.watch(DB_DIR, (eventType, filename) => {
|
|
2135
|
+
if (!filename || !filename.endsWith(".db-signal")) return;
|
|
2136
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
2137
|
+
this.debounceTimer = setTimeout(() => {
|
|
2138
|
+
this.notifyChanged();
|
|
2139
|
+
}, 300);
|
|
2140
|
+
});
|
|
2141
|
+
} catch {
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
stopWatchingDb() {
|
|
2145
|
+
if (this.dbWatcher) {
|
|
2146
|
+
this.dbWatcher.close();
|
|
2147
|
+
this.dbWatcher = null;
|
|
2148
|
+
}
|
|
2149
|
+
if (this.debounceTimer) {
|
|
2150
|
+
clearTimeout(this.debounceTimer);
|
|
2151
|
+
this.debounceTimer = null;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
// No-ops -- retained for API compatibility during transition
|
|
2155
|
+
watchConfig(_callback) {
|
|
2156
|
+
}
|
|
2157
|
+
stopWatching() {
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
var configManager = new ConfigManager();
|
|
2161
|
+
|
|
2162
|
+
// src/server.ts
|
|
2163
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2164
|
+
|
|
2165
|
+
// src/tools/tasks.ts
|
|
2166
|
+
import crypto from "crypto";
|
|
2167
|
+
import path4 from "path";
|
|
2168
|
+
import { z as z2 } from "zod";
|
|
2169
|
+
|
|
2170
|
+
// src/validation.ts
|
|
2171
|
+
import { z } from "zod";
|
|
2172
|
+
var safeName = z.string().min(1, "Name must not be empty").max(200, "Name must be 200 characters or less").refine((s) => !s.includes("..") && !s.includes("/") && !s.includes("\\"), {
|
|
2173
|
+
message: "Name must not contain path traversal characters (.. / \\)"
|
|
2174
|
+
});
|
|
2175
|
+
var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
|
|
2176
|
+
var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
|
|
2177
|
+
var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
|
|
2178
|
+
var safeShortText = z.string().max(200, "Value must be 200 characters or less");
|
|
2179
|
+
var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
|
|
2180
|
+
var safeAbsolutePath = z.string().min(1, "Path must not be empty").max(1e3, "Path must be 1000 characters or less").refine((s) => s.startsWith("/"), { message: "Path must be absolute (start with /)" });
|
|
2181
|
+
var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
|
|
2182
|
+
var V = {
|
|
2183
|
+
name: safeName,
|
|
2184
|
+
id: safeId,
|
|
2185
|
+
title: safeTitle,
|
|
2186
|
+
description: safeDescription,
|
|
2187
|
+
shortText: safeShortText,
|
|
2188
|
+
prompt: safePrompt,
|
|
2189
|
+
absolutePath: safeAbsolutePath,
|
|
2190
|
+
hexColor: safeHexColor
|
|
2191
|
+
};
|
|
2192
|
+
|
|
2193
|
+
// src/tools/tasks.ts
|
|
2194
|
+
var TASK_STATUSES = ["todo", "in_progress", "in_review", "done", "cancelled"];
|
|
2195
|
+
var AGENT_TYPES = [
|
|
2196
|
+
"claude",
|
|
2197
|
+
"copilot",
|
|
2198
|
+
"codex",
|
|
2199
|
+
"opencode",
|
|
2200
|
+
"gemini"
|
|
2201
|
+
];
|
|
2202
|
+
function registerTaskTools(server) {
|
|
2203
|
+
server.tool(
|
|
2204
|
+
"list_tasks",
|
|
2205
|
+
"List tasks, optionally filtered by project, status, assigned agent, or workspace",
|
|
2206
|
+
{
|
|
2207
|
+
project_name: V.name.optional().describe("Filter by project name"),
|
|
2208
|
+
status: z2.enum(TASK_STATUSES).optional().describe("Filter by status"),
|
|
2209
|
+
assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Filter by assigned agent type"),
|
|
2210
|
+
workspace_id: V.id.optional().describe("Filter by workspace ID (returns tasks from all projects in that workspace)")
|
|
2211
|
+
},
|
|
2212
|
+
async (args) => {
|
|
2213
|
+
let tasks = dbListTasks(args.project_name, args.status);
|
|
2214
|
+
if (args.workspace_id) {
|
|
2215
|
+
const projects = dbListProjects();
|
|
2216
|
+
const wsProjectNames = new Set(
|
|
2217
|
+
projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id).map((p) => p.name)
|
|
2218
|
+
);
|
|
2219
|
+
tasks = tasks.filter((t) => wsProjectNames.has(t.projectName));
|
|
2220
|
+
}
|
|
2221
|
+
if (args.assigned_agent) {
|
|
2222
|
+
tasks = tasks.filter((t) => t.assignedAgent === args.assigned_agent);
|
|
2223
|
+
}
|
|
2224
|
+
return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
|
|
2225
|
+
}
|
|
2226
|
+
);
|
|
2227
|
+
server.tool(
|
|
2228
|
+
"create_task",
|
|
2229
|
+
"Create a new task in a project",
|
|
2230
|
+
{
|
|
2231
|
+
project_name: V.name.describe("Project name (must match existing project)"),
|
|
2232
|
+
title: V.title.describe("Task title"),
|
|
2233
|
+
description: V.description.optional().describe("Task description (markdown)"),
|
|
2234
|
+
status: z2.enum(TASK_STATUSES).optional().describe("Task status (default: todo)"),
|
|
2235
|
+
branch: V.shortText.optional().describe("Git branch for this task"),
|
|
2236
|
+
use_worktree: z2.boolean().optional().describe("Create a git worktree for this task"),
|
|
2237
|
+
assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assign to an agent type")
|
|
2238
|
+
},
|
|
2239
|
+
async (args) => {
|
|
2240
|
+
const project = dbGetProject(args.project_name);
|
|
2241
|
+
if (!project) {
|
|
2242
|
+
return {
|
|
2243
|
+
content: [{ type: "text", text: `Error: project "${args.project_name}" not found` }],
|
|
2244
|
+
isError: true
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
const maxOrder = dbGetMaxTaskOrder(args.project_name);
|
|
2248
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2249
|
+
const status = args.status ?? "todo";
|
|
2250
|
+
const task = {
|
|
2251
|
+
id: crypto.randomUUID(),
|
|
2252
|
+
projectName: args.project_name,
|
|
2253
|
+
title: args.title,
|
|
2254
|
+
description: args.description ?? "",
|
|
2255
|
+
status,
|
|
2256
|
+
order: maxOrder + 1,
|
|
2257
|
+
createdAt: now,
|
|
2258
|
+
updatedAt: now,
|
|
2259
|
+
...args.branch && { branch: args.branch },
|
|
2260
|
+
...args.use_worktree && { useWorktree: args.use_worktree },
|
|
2261
|
+
...args.assigned_agent && { assignedAgent: args.assigned_agent },
|
|
2262
|
+
...(status === "done" || status === "cancelled") && { completedAt: now }
|
|
2263
|
+
};
|
|
2264
|
+
dbInsertTask(task);
|
|
2265
|
+
dbSignalChange();
|
|
2266
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
2267
|
+
}
|
|
2268
|
+
);
|
|
2269
|
+
server.tool("get_task", "Get a task by ID", { id: V.id.describe("Task ID") }, async (args) => {
|
|
2270
|
+
const task = dbGetTask(args.id);
|
|
2271
|
+
if (!task) {
|
|
2272
|
+
return {
|
|
2273
|
+
content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
|
|
2274
|
+
isError: true
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
2278
|
+
});
|
|
2279
|
+
server.tool(
|
|
2280
|
+
"update_task",
|
|
2281
|
+
"Update a task's properties",
|
|
2282
|
+
{
|
|
2283
|
+
id: V.id.describe("Task ID"),
|
|
2284
|
+
title: V.title.optional().describe("New title"),
|
|
2285
|
+
description: V.description.optional().describe("New description"),
|
|
2286
|
+
status: z2.enum(TASK_STATUSES).optional().describe("New status"),
|
|
2287
|
+
branch: V.shortText.optional().describe("Git branch"),
|
|
2288
|
+
use_worktree: z2.boolean().optional().describe("Use git worktree"),
|
|
2289
|
+
assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assigned agent type"),
|
|
2290
|
+
order: z2.number().optional().describe("Queue order")
|
|
2291
|
+
},
|
|
2292
|
+
async (args) => {
|
|
2293
|
+
const task = dbGetTask(args.id);
|
|
2294
|
+
if (!task) {
|
|
2295
|
+
return {
|
|
2296
|
+
content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
|
|
2297
|
+
isError: true
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
const updates = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2301
|
+
if (args.title !== void 0) updates.title = args.title;
|
|
2302
|
+
if (args.description !== void 0) updates.description = args.description;
|
|
2303
|
+
if (args.branch !== void 0) updates.branch = args.branch;
|
|
2304
|
+
if (args.use_worktree !== void 0) updates.useWorktree = args.use_worktree;
|
|
2305
|
+
if (args.assigned_agent !== void 0)
|
|
2306
|
+
updates.assignedAgent = args.assigned_agent;
|
|
2307
|
+
if (args.order !== void 0) updates.order = args.order;
|
|
2308
|
+
if (args.status !== void 0) {
|
|
2309
|
+
const newStatus = args.status;
|
|
2310
|
+
const wasDone = task.status === "done" || task.status === "cancelled";
|
|
2311
|
+
const isDone = newStatus === "done" || newStatus === "cancelled";
|
|
2312
|
+
updates.status = newStatus;
|
|
2313
|
+
if (isDone && !wasDone) updates.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2314
|
+
if (!isDone && wasDone) updates.completedAt = void 0;
|
|
2315
|
+
}
|
|
2316
|
+
dbUpdateTask(args.id, updates);
|
|
2317
|
+
dbSignalChange();
|
|
2318
|
+
const updated = dbGetTask(args.id);
|
|
2319
|
+
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
2320
|
+
}
|
|
2321
|
+
);
|
|
2322
|
+
server.tool(
|
|
2323
|
+
"delete_task",
|
|
2324
|
+
"Delete a task by ID",
|
|
2325
|
+
{ id: V.id.describe("Task ID") },
|
|
2326
|
+
async (args) => {
|
|
2327
|
+
const task = dbGetTask(args.id);
|
|
2328
|
+
if (!task) {
|
|
2329
|
+
return {
|
|
2330
|
+
content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
|
|
2331
|
+
isError: true
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
dbDeleteTask(args.id);
|
|
2335
|
+
dbSignalChange();
|
|
2336
|
+
return { content: [{ type: "text", text: `Deleted task: ${task.title}` }] };
|
|
2337
|
+
}
|
|
2338
|
+
);
|
|
2339
|
+
server.tool(
|
|
2340
|
+
"get_my_context",
|
|
2341
|
+
"Get your current task and project context. Auto-detects based on your working directory. Call this at the start of a session to understand what you are working on.",
|
|
2342
|
+
{
|
|
2343
|
+
cwd: V.absolutePath.optional().describe(
|
|
2344
|
+
"Your current working directory (auto-detected if omitted). Used to match against known projects and task worktrees."
|
|
2345
|
+
),
|
|
2346
|
+
task_id: V.id.optional().describe("Specific task ID to get context for (overrides auto-detection)")
|
|
2347
|
+
},
|
|
2348
|
+
async (args) => {
|
|
2349
|
+
if (args.task_id) {
|
|
2350
|
+
const task = dbGetTask(args.task_id);
|
|
2351
|
+
if (!task) {
|
|
2352
|
+
return {
|
|
2353
|
+
content: [{ type: "text", text: `Error: task "${args.task_id}" not found` }],
|
|
2354
|
+
isError: true
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
const project = dbGetProject(task.projectName);
|
|
2358
|
+
const siblingTasks = dbListTasks(task.projectName);
|
|
2359
|
+
return {
|
|
2360
|
+
content: [
|
|
2361
|
+
{
|
|
2362
|
+
type: "text",
|
|
2363
|
+
text: JSON.stringify(
|
|
2364
|
+
{
|
|
2365
|
+
task,
|
|
2366
|
+
project: project ?? void 0,
|
|
2367
|
+
siblingTasks: siblingTasks.filter((t) => t.id !== task.id).map((t) => ({
|
|
2368
|
+
id: t.id,
|
|
2369
|
+
title: t.title,
|
|
2370
|
+
status: t.status,
|
|
2371
|
+
branch: t.branch
|
|
2372
|
+
}))
|
|
2373
|
+
},
|
|
2374
|
+
null,
|
|
2375
|
+
2
|
|
2376
|
+
)
|
|
2377
|
+
}
|
|
2378
|
+
]
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
const cwd = args.cwd || process.cwd();
|
|
2382
|
+
const normalizedCwd = path4.resolve(cwd);
|
|
2383
|
+
const projects = dbListProjects();
|
|
2384
|
+
let matchedProject = null;
|
|
2385
|
+
let matchLen = 0;
|
|
2386
|
+
for (const p of projects) {
|
|
2387
|
+
const normalizedPath = path4.resolve(p.path);
|
|
2388
|
+
if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
|
|
2389
|
+
matchedProject = p;
|
|
2390
|
+
matchLen = normalizedPath.length;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (!matchedProject) {
|
|
2394
|
+
return {
|
|
2395
|
+
content: [
|
|
2396
|
+
{
|
|
2397
|
+
type: "text",
|
|
2398
|
+
text: JSON.stringify(
|
|
2399
|
+
{
|
|
2400
|
+
message: "No matching project found for current directory.",
|
|
2401
|
+
cwd: normalizedCwd,
|
|
2402
|
+
hint: "Use list_projects to see available projects, or pass a task_id directly."
|
|
2403
|
+
},
|
|
2404
|
+
null,
|
|
2405
|
+
2
|
|
2406
|
+
)
|
|
2407
|
+
}
|
|
2408
|
+
]
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const projectTasks = dbListTasks(matchedProject.name);
|
|
2412
|
+
let matchedTask = null;
|
|
2413
|
+
for (const t of projectTasks) {
|
|
2414
|
+
if (t.worktreePath) {
|
|
2415
|
+
const normalizedWorktree = path4.resolve(t.worktreePath);
|
|
2416
|
+
if (normalizedCwd.startsWith(normalizedWorktree)) {
|
|
2417
|
+
matchedTask = t;
|
|
2418
|
+
break;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (!matchedTask) {
|
|
2423
|
+
matchedTask = projectTasks.find((t) => t.status === "in_progress") ?? null;
|
|
2424
|
+
}
|
|
2425
|
+
const result = {
|
|
2426
|
+
project: {
|
|
2427
|
+
name: matchedProject.name,
|
|
2428
|
+
path: matchedProject.path,
|
|
2429
|
+
preferredAgents: matchedProject.preferredAgents
|
|
2430
|
+
},
|
|
2431
|
+
cwd: normalizedCwd
|
|
2432
|
+
};
|
|
2433
|
+
if (matchedTask) {
|
|
2434
|
+
result.task = matchedTask;
|
|
2435
|
+
result.siblingTasks = projectTasks.filter((t) => t.id !== matchedTask.id).map((t) => ({ id: t.id, title: t.title, status: t.status, branch: t.branch }));
|
|
2436
|
+
} else {
|
|
2437
|
+
result.message = "No specific task matched. Showing all project tasks.";
|
|
2438
|
+
result.tasks = projectTasks.map((t) => ({
|
|
2439
|
+
id: t.id,
|
|
2440
|
+
title: t.title,
|
|
2441
|
+
status: t.status,
|
|
2442
|
+
branch: t.branch
|
|
2443
|
+
}));
|
|
2444
|
+
}
|
|
2445
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2446
|
+
}
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/tools/projects.ts
|
|
2451
|
+
import { z as z3 } from "zod";
|
|
2452
|
+
var AGENT_TYPES2 = [
|
|
2453
|
+
"claude",
|
|
2454
|
+
"copilot",
|
|
2455
|
+
"codex",
|
|
2456
|
+
"opencode",
|
|
2457
|
+
"gemini"
|
|
2458
|
+
];
|
|
2459
|
+
function registerProjectTools(server) {
|
|
2460
|
+
server.tool(
|
|
2461
|
+
"list_projects",
|
|
2462
|
+
"List all projects, optionally filtered by workspace",
|
|
2463
|
+
{
|
|
2464
|
+
workspace_id: V.id.optional().describe('Filter by workspace ID (e.g. "personal")')
|
|
2465
|
+
},
|
|
2466
|
+
async (args) => {
|
|
2467
|
+
let projects = dbListProjects();
|
|
2468
|
+
if (args.workspace_id) {
|
|
2469
|
+
projects = projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id);
|
|
2470
|
+
}
|
|
2471
|
+
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
2472
|
+
}
|
|
2473
|
+
);
|
|
2474
|
+
server.tool(
|
|
2475
|
+
"create_project",
|
|
2476
|
+
"Create a new project",
|
|
2477
|
+
{
|
|
2478
|
+
name: V.name.describe("Project name (unique identifier)"),
|
|
2479
|
+
path: V.absolutePath.describe("Absolute path to project directory"),
|
|
2480
|
+
preferred_agents: z3.array(z3.enum(AGENT_TYPES2)).optional().describe("Preferred agent types"),
|
|
2481
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
2482
|
+
icon_color: V.hexColor.optional().describe("Hex color for icon")
|
|
2483
|
+
},
|
|
2484
|
+
async (args) => {
|
|
2485
|
+
if (dbGetProject(args.name)) {
|
|
2486
|
+
return {
|
|
2487
|
+
content: [{ type: "text", text: `Error: project "${args.name}" already exists` }],
|
|
2488
|
+
isError: true
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
const project = {
|
|
2492
|
+
name: args.name,
|
|
2493
|
+
path: args.path,
|
|
2494
|
+
preferredAgents: args.preferred_agents ?? [],
|
|
2495
|
+
...args.icon && { icon: args.icon },
|
|
2496
|
+
...args.icon_color && { iconColor: args.icon_color }
|
|
2497
|
+
};
|
|
2498
|
+
dbInsertProject(project);
|
|
2499
|
+
dbSignalChange();
|
|
2500
|
+
return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }] };
|
|
2501
|
+
}
|
|
2502
|
+
);
|
|
2503
|
+
server.tool(
|
|
2504
|
+
"update_project",
|
|
2505
|
+
"Update a project's properties",
|
|
2506
|
+
{
|
|
2507
|
+
name: V.name.describe("Project name (identifier, cannot be changed)"),
|
|
2508
|
+
path: V.absolutePath.optional().describe("New project path"),
|
|
2509
|
+
preferred_agents: z3.array(z3.enum(AGENT_TYPES2)).optional().describe("Preferred agent types"),
|
|
2510
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
2511
|
+
icon_color: V.hexColor.optional().describe("Hex color for icon")
|
|
2512
|
+
},
|
|
2513
|
+
async (args) => {
|
|
2514
|
+
if (!dbGetProject(args.name)) {
|
|
2515
|
+
return {
|
|
2516
|
+
content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
|
|
2517
|
+
isError: true
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
const updates = {};
|
|
2521
|
+
if (args.path !== void 0) updates.path = args.path;
|
|
2522
|
+
if (args.preferred_agents !== void 0)
|
|
2523
|
+
updates.preferredAgents = args.preferred_agents;
|
|
2524
|
+
if (args.icon !== void 0) updates.icon = args.icon;
|
|
2525
|
+
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
2526
|
+
dbUpdateProject(args.name, updates);
|
|
2527
|
+
dbSignalChange();
|
|
2528
|
+
const updated = dbGetProject(args.name);
|
|
2529
|
+
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
2530
|
+
}
|
|
2531
|
+
);
|
|
2532
|
+
server.tool(
|
|
2533
|
+
"delete_project",
|
|
2534
|
+
"Delete a project and all its tasks",
|
|
2535
|
+
{ name: V.name.describe("Project name") },
|
|
2536
|
+
async (args) => {
|
|
2537
|
+
if (!dbGetProject(args.name)) {
|
|
2538
|
+
return {
|
|
2539
|
+
content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
|
|
2540
|
+
isError: true
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
dbDeleteProject(args.name);
|
|
2544
|
+
dbSignalChange();
|
|
2545
|
+
return { content: [{ type: "text", text: `Deleted project: ${args.name}` }] };
|
|
2546
|
+
}
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// src/tools/sessions.ts
|
|
2551
|
+
import { z as z4 } from "zod";
|
|
2552
|
+
|
|
2553
|
+
// src/ws-client.ts
|
|
2554
|
+
import fs5 from "fs";
|
|
2555
|
+
import path5 from "path";
|
|
2556
|
+
import os2 from "os";
|
|
2557
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2558
|
+
import { WebSocket } from "ws";
|
|
2559
|
+
var PORT_FILE = path5.join(os2.homedir(), BASEGRID_DIR_NAME, "ws-port");
|
|
2560
|
+
var TIMEOUT_MS = 1e4;
|
|
2561
|
+
var IS_WIN = process.platform === "win32";
|
|
2562
|
+
var PORT_FILE_MISSING_MSG = IS_WIN ? `BaseGrid port file not found (~/.basegrid/ws-port).
|
|
2563
|
+
The app may be running but the port file was deleted (e.g. by another instance shutting down).
|
|
2564
|
+
To fix, find the BaseGrid process and its listening port:
|
|
2565
|
+
powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process BaseGrid).Id | Select LocalPort"
|
|
2566
|
+
Then write the WS port to the file:
|
|
2567
|
+
echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.basegrid\\ws-port
|
|
2568
|
+
Or restart BaseGrid to regenerate it.` : `BaseGrid port file not found (~/.basegrid/ws-port).
|
|
2569
|
+
The app may be running but the port file was deleted (e.g. by another instance shutting down).
|
|
2570
|
+
To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep BaseGrid
|
|
2571
|
+
Then write the WS port (the one on *:<port>) to the file:
|
|
2572
|
+
echo '{"port":<PORT>,"pid":<PID>}' > ~/.basegrid/ws-port
|
|
2573
|
+
Or restart BaseGrid to regenerate it.`;
|
|
2574
|
+
var PORT_FILE_INVALID_MSG = `BaseGrid port file exists but contains invalid data (~/.basegrid/ws-port).
|
|
2575
|
+
Delete it and restart BaseGrid, or overwrite it with the correct port:
|
|
2576
|
+
${IS_WIN ? "del %USERPROFILE%\\.basegrid\\ws-port" : "rm ~/.basegrid/ws-port"}`;
|
|
2577
|
+
var rpcId = 0;
|
|
2578
|
+
var cachedPort = null;
|
|
2579
|
+
var cacheTimestamp = 0;
|
|
2580
|
+
var CACHE_TTL_MS = 5e3;
|
|
2581
|
+
var EXEC_OPTS = {
|
|
2582
|
+
encoding: "utf-8",
|
|
2583
|
+
timeout: 5e3,
|
|
2584
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2585
|
+
};
|
|
2586
|
+
function discoverPort() {
|
|
2587
|
+
try {
|
|
2588
|
+
if (IS_WIN) {
|
|
2589
|
+
const taskOut = execFileSync2(
|
|
2590
|
+
"tasklist",
|
|
2591
|
+
["/FI", "IMAGENAME eq BaseGrid.exe", "/FO", "CSV", "/NH"],
|
|
2592
|
+
EXEC_OPTS
|
|
2593
|
+
);
|
|
2594
|
+
const pidMatch = taskOut.match(/"BaseGrid\.exe","(\d+)"/);
|
|
2595
|
+
if (!pidMatch) return null;
|
|
2596
|
+
const pid = pidMatch[1];
|
|
2597
|
+
const lines = execFileSync2("netstat", ["-ano"], EXEC_OPTS).split("\n");
|
|
2598
|
+
let fallback = null;
|
|
2599
|
+
for (const line of lines) {
|
|
2600
|
+
if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
|
|
2601
|
+
const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
|
|
2602
|
+
if (!m) continue;
|
|
2603
|
+
if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
|
|
2604
|
+
fallback ??= parseInt(m[1], 10);
|
|
2605
|
+
}
|
|
2606
|
+
return fallback;
|
|
2607
|
+
} else {
|
|
2608
|
+
const lines = execFileSync2("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
|
|
2609
|
+
"\n"
|
|
2610
|
+
);
|
|
2611
|
+
let fallback = null;
|
|
2612
|
+
for (const line of lines) {
|
|
2613
|
+
if (!line.includes("BaseGrid")) continue;
|
|
2614
|
+
if (line.includes("*:")) {
|
|
2615
|
+
const m = line.match(/\*:(\d+)/);
|
|
2616
|
+
if (m) return parseInt(m[1], 10);
|
|
2617
|
+
}
|
|
2618
|
+
if (!fallback) {
|
|
2619
|
+
const m = line.match(/:(\d+)\s/);
|
|
2620
|
+
if (m) fallback = parseInt(m[1], 10);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return fallback;
|
|
2624
|
+
}
|
|
2625
|
+
} catch {
|
|
2626
|
+
}
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
function discoverAndHeal() {
|
|
2630
|
+
const now = Date.now();
|
|
2631
|
+
if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
|
|
2632
|
+
const discovered = discoverPort();
|
|
2633
|
+
cachedPort = discovered;
|
|
2634
|
+
cacheTimestamp = now;
|
|
2635
|
+
if (discovered) {
|
|
2636
|
+
try {
|
|
2637
|
+
fs5.mkdirSync(path5.dirname(PORT_FILE), { recursive: true });
|
|
2638
|
+
fs5.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
|
|
2639
|
+
} catch {
|
|
2640
|
+
}
|
|
2641
|
+
return { port: discovered };
|
|
2642
|
+
}
|
|
2643
|
+
return { port: null, reason: "missing" };
|
|
2644
|
+
}
|
|
2645
|
+
function readPort() {
|
|
2646
|
+
try {
|
|
2647
|
+
const raw = fs5.readFileSync(PORT_FILE, "utf-8").trim();
|
|
2648
|
+
if (!raw) return { port: null, reason: "invalid" };
|
|
2649
|
+
if (raw.startsWith("{")) {
|
|
2650
|
+
const parsed = JSON.parse(raw);
|
|
2651
|
+
const p2 = parsed?.port;
|
|
2652
|
+
const pid = parsed?.pid;
|
|
2653
|
+
if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
|
|
2654
|
+
return { port: null, reason: "invalid" };
|
|
2655
|
+
}
|
|
2656
|
+
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
|
|
2657
|
+
try {
|
|
2658
|
+
process.kill(pid, 0);
|
|
2659
|
+
} catch (err) {
|
|
2660
|
+
if (err.code === "EPERM") return { port: p2 };
|
|
2661
|
+
return discoverAndHeal();
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return { port: p2 };
|
|
2665
|
+
}
|
|
2666
|
+
const p = parseInt(raw, 10);
|
|
2667
|
+
return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
|
|
2668
|
+
} catch {
|
|
2669
|
+
return discoverAndHeal();
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
async function rpcCall(method, params) {
|
|
2673
|
+
const result = readPort();
|
|
2674
|
+
if (!result.port) {
|
|
2675
|
+
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
2676
|
+
}
|
|
2677
|
+
return new Promise((resolve, reject) => {
|
|
2678
|
+
const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
|
|
2679
|
+
const id = ++rpcId;
|
|
2680
|
+
const timer = setTimeout(() => {
|
|
2681
|
+
ws.close();
|
|
2682
|
+
reject(new Error(`RPC call "${method}" timed out after ${TIMEOUT_MS}ms`));
|
|
2683
|
+
}, TIMEOUT_MS);
|
|
2684
|
+
ws.on("open", () => {
|
|
2685
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
2686
|
+
});
|
|
2687
|
+
ws.on("message", (raw) => {
|
|
2688
|
+
try {
|
|
2689
|
+
const msg = JSON.parse(raw.toString());
|
|
2690
|
+
if (msg.id !== id) return;
|
|
2691
|
+
clearTimeout(timer);
|
|
2692
|
+
ws.close();
|
|
2693
|
+
if (msg.error) {
|
|
2694
|
+
reject(new Error(msg.error.message));
|
|
2695
|
+
} else {
|
|
2696
|
+
resolve(msg.result);
|
|
2697
|
+
}
|
|
2698
|
+
} catch {
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
2701
|
+
ws.on("error", (err) => {
|
|
2702
|
+
clearTimeout(timer);
|
|
2703
|
+
reject(new Error(`Cannot connect to BaseGrid server: ${err.message}. Is the app running?`));
|
|
2704
|
+
});
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
async function rpcNotify(method, params) {
|
|
2708
|
+
const result = readPort();
|
|
2709
|
+
if (!result.port) {
|
|
2710
|
+
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
2711
|
+
}
|
|
2712
|
+
return new Promise((resolve, reject) => {
|
|
2713
|
+
const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
|
|
2714
|
+
ws.on("open", () => {
|
|
2715
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
|
|
2716
|
+
ws.close();
|
|
2717
|
+
resolve();
|
|
2718
|
+
});
|
|
2719
|
+
ws.on("error", (err) => {
|
|
2720
|
+
reject(new Error(`Cannot connect to BaseGrid server: ${err.message}. Is the app running?`));
|
|
2721
|
+
});
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// src/tools/sessions.ts
|
|
2726
|
+
var AGENT_TYPES3 = [
|
|
2727
|
+
"claude",
|
|
2728
|
+
"copilot",
|
|
2729
|
+
"codex",
|
|
2730
|
+
"opencode",
|
|
2731
|
+
"gemini"
|
|
2732
|
+
];
|
|
2733
|
+
function registerSessionTools(server) {
|
|
2734
|
+
server.tool(
|
|
2735
|
+
"list_sessions",
|
|
2736
|
+
'List terminal sessions. Filter by status: "active" (running terminals) or "recent" (past sessions).',
|
|
2737
|
+
{
|
|
2738
|
+
filter: z4.enum(["active", "recent"]).optional().describe("Session filter (default: active)"),
|
|
2739
|
+
project_name: V.name.optional().describe("Filter by project name"),
|
|
2740
|
+
project_path: V.absolutePath.optional().describe("Filter by project path (for recent sessions)")
|
|
2741
|
+
},
|
|
2742
|
+
async (args) => {
|
|
2743
|
+
const filter = args.filter ?? "active";
|
|
2744
|
+
try {
|
|
2745
|
+
if (filter === "active") {
|
|
2746
|
+
let sessions = await rpcCall("terminal:listActive");
|
|
2747
|
+
if (args.project_name) {
|
|
2748
|
+
sessions = sessions.filter((s) => s.projectName === args.project_name);
|
|
2749
|
+
}
|
|
2750
|
+
const summary = sessions.map((s) => ({
|
|
2751
|
+
id: s.id,
|
|
2752
|
+
agentType: s.agentType,
|
|
2753
|
+
projectName: s.projectName,
|
|
2754
|
+
status: s.status,
|
|
2755
|
+
displayName: s.displayName,
|
|
2756
|
+
branch: s.branch,
|
|
2757
|
+
pid: s.pid
|
|
2758
|
+
}));
|
|
2759
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
2760
|
+
} else {
|
|
2761
|
+
const sessions = await rpcCall("sessions:getRecent", args.project_path);
|
|
2762
|
+
return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
|
|
2763
|
+
}
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
return {
|
|
2766
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
|
|
2767
|
+
isError: true
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
);
|
|
2772
|
+
server.tool(
|
|
2773
|
+
"launch_session",
|
|
2774
|
+
"Launch an AI agent session (interactive terminal or headless). Requires the BaseGrid app to be running.",
|
|
2775
|
+
{
|
|
2776
|
+
agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
|
|
2777
|
+
project_name: V.name.describe("Project name"),
|
|
2778
|
+
project_path: V.absolutePath.describe("Absolute path to project directory"),
|
|
2779
|
+
prompt: V.prompt.optional().describe("Initial prompt to send to the agent"),
|
|
2780
|
+
branch: V.shortText.optional().describe("Git branch to checkout"),
|
|
2781
|
+
use_worktree: z4.boolean().optional().describe("Create a git worktree"),
|
|
2782
|
+
display_name: V.shortText.optional().describe("Display name for the session"),
|
|
2783
|
+
headless: z4.boolean().optional().describe("Launch as headless (no UI) session")
|
|
2784
|
+
},
|
|
2785
|
+
async (args) => {
|
|
2786
|
+
const payload = {
|
|
2787
|
+
agentType: args.agent_type,
|
|
2788
|
+
projectName: args.project_name,
|
|
2789
|
+
projectPath: args.project_path,
|
|
2790
|
+
...args.prompt && { initialPrompt: args.prompt },
|
|
2791
|
+
...args.branch && { branch: args.branch },
|
|
2792
|
+
...args.use_worktree && { useWorktree: args.use_worktree },
|
|
2793
|
+
...args.display_name && { displayName: args.display_name }
|
|
2794
|
+
};
|
|
2795
|
+
const rpcMethod = args.headless ? "headless:create" : "terminal:create";
|
|
2796
|
+
const label = args.headless ? "headless" : "terminal";
|
|
2797
|
+
try {
|
|
2798
|
+
const session = await rpcCall(rpcMethod, payload);
|
|
2799
|
+
return {
|
|
2800
|
+
content: [
|
|
2801
|
+
{
|
|
2802
|
+
type: "text",
|
|
2803
|
+
text: JSON.stringify(
|
|
2804
|
+
{
|
|
2805
|
+
id: session.id,
|
|
2806
|
+
agentType: session.agentType,
|
|
2807
|
+
projectName: session.projectName,
|
|
2808
|
+
pid: session.pid,
|
|
2809
|
+
status: session.status
|
|
2810
|
+
},
|
|
2811
|
+
null,
|
|
2812
|
+
2
|
|
2813
|
+
)
|
|
2814
|
+
}
|
|
2815
|
+
]
|
|
2816
|
+
};
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
return {
|
|
2819
|
+
content: [
|
|
2820
|
+
{
|
|
2821
|
+
type: "text",
|
|
2822
|
+
text: `Error launching ${label} agent: ${err instanceof Error ? err.message : err}`
|
|
2823
|
+
}
|
|
2824
|
+
],
|
|
2825
|
+
isError: true
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
);
|
|
2830
|
+
server.tool(
|
|
2831
|
+
"kill_session",
|
|
2832
|
+
"Kill a terminal or headless session. Requires the BaseGrid app to be running.",
|
|
2833
|
+
{
|
|
2834
|
+
id: V.id.describe("Session ID to kill"),
|
|
2835
|
+
headless: z4.boolean().optional().describe("Kill a headless session instead of a terminal")
|
|
2836
|
+
},
|
|
2837
|
+
async (args) => {
|
|
2838
|
+
const rpcMethod = args.headless ? "headless:kill" : "terminal:kill";
|
|
2839
|
+
const label = args.headless ? "headless session" : "session";
|
|
2840
|
+
try {
|
|
2841
|
+
await rpcCall(rpcMethod, args.id);
|
|
2842
|
+
return { content: [{ type: "text", text: `Killed ${label}: ${args.id}` }] };
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
return {
|
|
2845
|
+
content: [
|
|
2846
|
+
{
|
|
2847
|
+
type: "text",
|
|
2848
|
+
text: `Error killing ${label}: ${err instanceof Error ? err.message : err}`
|
|
2849
|
+
}
|
|
2850
|
+
],
|
|
2851
|
+
isError: true
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
);
|
|
2856
|
+
server.tool(
|
|
2857
|
+
"rename_session",
|
|
2858
|
+
"Rename a terminal session. Changes the display name shown in the UI.",
|
|
2859
|
+
{
|
|
2860
|
+
id: V.id.describe("Session ID"),
|
|
2861
|
+
display_name: V.shortText.describe("New display name")
|
|
2862
|
+
},
|
|
2863
|
+
async (args) => {
|
|
2864
|
+
try {
|
|
2865
|
+
await rpcCall("terminal:rename", { id: args.id, displayName: args.display_name });
|
|
2866
|
+
return {
|
|
2867
|
+
content: [{ type: "text", text: `Renamed session ${args.id} to "${args.display_name}"` }]
|
|
2868
|
+
};
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
return {
|
|
2871
|
+
content: [
|
|
2872
|
+
{
|
|
2873
|
+
type: "text",
|
|
2874
|
+
text: `Error renaming session: ${err instanceof Error ? err.message : err}`
|
|
2875
|
+
}
|
|
2876
|
+
],
|
|
2877
|
+
isError: true
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
);
|
|
2882
|
+
server.tool(
|
|
2883
|
+
"reorder_sessions",
|
|
2884
|
+
"Reorder terminal sessions in the grid. Provide session IDs in the desired display order.",
|
|
2885
|
+
{
|
|
2886
|
+
session_ids: z4.array(V.id).min(1, "At least one session ID is required").describe("Session IDs in desired order")
|
|
2887
|
+
},
|
|
2888
|
+
async (args) => {
|
|
2889
|
+
try {
|
|
2890
|
+
await rpcCall("terminal:reorder", args.session_ids);
|
|
2891
|
+
return {
|
|
2892
|
+
content: [
|
|
2893
|
+
{
|
|
2894
|
+
type: "text",
|
|
2895
|
+
text: `Reordered ${args.session_ids.length} sessions`
|
|
2896
|
+
}
|
|
2897
|
+
]
|
|
2898
|
+
};
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
return {
|
|
2901
|
+
content: [
|
|
2902
|
+
{
|
|
2903
|
+
type: "text",
|
|
2904
|
+
text: `Error reordering sessions: ${err instanceof Error ? err.message : err}`
|
|
2905
|
+
}
|
|
2906
|
+
],
|
|
2907
|
+
isError: true
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
);
|
|
2912
|
+
server.tool(
|
|
2913
|
+
"read_session_output",
|
|
2914
|
+
"Read terminal output from a running session. Output is stored in a rolling 1000-line buffer with ANSI codes stripped.",
|
|
2915
|
+
{
|
|
2916
|
+
id: V.id.describe("Session ID"),
|
|
2917
|
+
lines: z4.number().int().min(1).max(1e3).optional().describe("Number of lines to read from the end (default: all)")
|
|
2918
|
+
},
|
|
2919
|
+
async (args) => {
|
|
2920
|
+
try {
|
|
2921
|
+
const output = await rpcCall("terminal:readOutput", {
|
|
2922
|
+
id: args.id,
|
|
2923
|
+
lines: args.lines
|
|
2924
|
+
});
|
|
2925
|
+
return {
|
|
2926
|
+
content: [{ type: "text", text: output.join("\n") }]
|
|
2927
|
+
};
|
|
2928
|
+
} catch (err) {
|
|
2929
|
+
return {
|
|
2930
|
+
content: [
|
|
2931
|
+
{
|
|
2932
|
+
type: "text",
|
|
2933
|
+
text: `Error reading session output: ${err instanceof Error ? err.message : err}`
|
|
2934
|
+
}
|
|
2935
|
+
],
|
|
2936
|
+
isError: true
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
);
|
|
2941
|
+
server.tool(
|
|
2942
|
+
"write_to_terminal",
|
|
2943
|
+
"Send input to a running terminal session. Requires the BaseGrid app to be running.",
|
|
2944
|
+
{
|
|
2945
|
+
id: V.id.describe("Session ID"),
|
|
2946
|
+
data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)"),
|
|
2947
|
+
raw: z4.boolean().optional().describe("Send data as-is without appending carriage return (for raw terminal control)")
|
|
2948
|
+
},
|
|
2949
|
+
async (args) => {
|
|
2950
|
+
try {
|
|
2951
|
+
const data = args.raw ? args.data : args.data.replace(/[\r\n]+$/, "") + "\r";
|
|
2952
|
+
await rpcNotify("terminal:write", { id: args.id, data });
|
|
2953
|
+
return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
|
|
2954
|
+
} catch (err) {
|
|
2955
|
+
return {
|
|
2956
|
+
content: [
|
|
2957
|
+
{
|
|
2958
|
+
type: "text",
|
|
2959
|
+
text: `Error writing to terminal: ${err instanceof Error ? err.message : err}`
|
|
2960
|
+
}
|
|
2961
|
+
],
|
|
2962
|
+
isError: true
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
);
|
|
2967
|
+
const KEY_MAP = {
|
|
2968
|
+
enter: "\r",
|
|
2969
|
+
escape: "\x1B",
|
|
2970
|
+
esc: "\x1B",
|
|
2971
|
+
tab: " ",
|
|
2972
|
+
"shift+tab": "\x1B[Z",
|
|
2973
|
+
up: "\x1B[A",
|
|
2974
|
+
down: "\x1B[B",
|
|
2975
|
+
left: "\x1B[D",
|
|
2976
|
+
right: "\x1B[C",
|
|
2977
|
+
backspace: "\x7F",
|
|
2978
|
+
delete: "\x1B[3~",
|
|
2979
|
+
home: "\x1B[H",
|
|
2980
|
+
end: "\x1B[F",
|
|
2981
|
+
"ctrl+c": "",
|
|
2982
|
+
"ctrl+d": "",
|
|
2983
|
+
"ctrl+x": "",
|
|
2984
|
+
"ctrl+z": ""
|
|
2985
|
+
};
|
|
2986
|
+
server.tool(
|
|
2987
|
+
"send_key",
|
|
2988
|
+
"Send a single keystroke or key combo to a terminal session without appending Enter. Use for TUI interactions like selecting menu options (1, 2, y, n), pressing Escape, Ctrl+C, arrow keys, etc.",
|
|
2989
|
+
{
|
|
2990
|
+
id: V.id.describe("Session ID"),
|
|
2991
|
+
key: z4.string().min(1).max(20).describe(
|
|
2992
|
+
"Key to send: single char (1, y, n), named key (enter, escape, tab, up, down, left, right, backspace, delete, home, end), or combo (ctrl+c, ctrl+d, ctrl+x, ctrl+z, shift+tab)"
|
|
2993
|
+
)
|
|
2994
|
+
},
|
|
2995
|
+
async (args) => {
|
|
2996
|
+
const key = args.key.toLowerCase().trim();
|
|
2997
|
+
let data = KEY_MAP[key];
|
|
2998
|
+
if (!data) {
|
|
2999
|
+
const ctrlMatch = key.match(/^ctrl\+([a-z])$/);
|
|
3000
|
+
if (ctrlMatch) {
|
|
3001
|
+
data = String.fromCharCode(ctrlMatch[1].toUpperCase().charCodeAt(0) - 64);
|
|
3002
|
+
} else if (args.key.length === 1) {
|
|
3003
|
+
data = args.key;
|
|
3004
|
+
} else {
|
|
3005
|
+
return {
|
|
3006
|
+
content: [
|
|
3007
|
+
{
|
|
3008
|
+
type: "text",
|
|
3009
|
+
text: `Unknown key: "${args.key}". Supported: single chars (1, y, n), named keys (${Object.keys(KEY_MAP).join(", ")}), or ctrl+<letter>.`
|
|
3010
|
+
}
|
|
3011
|
+
],
|
|
3012
|
+
isError: true
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
try {
|
|
3017
|
+
await rpcNotify("terminal:write", { id: args.id, data });
|
|
3018
|
+
return {
|
|
3019
|
+
content: [{ type: "text", text: `Sent key "${args.key}" to session: ${args.id}` }]
|
|
3020
|
+
};
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
return {
|
|
3023
|
+
content: [
|
|
3024
|
+
{
|
|
3025
|
+
type: "text",
|
|
3026
|
+
text: `Error sending key to terminal: ${err instanceof Error ? err.message : err}`
|
|
3027
|
+
}
|
|
3028
|
+
],
|
|
3029
|
+
isError: true
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
);
|
|
3034
|
+
server.tool(
|
|
3035
|
+
"list_session_events",
|
|
3036
|
+
"List session lifecycle events (created, exited, task_linked, renamed). Use for post-mortem analysis and multi-agent coordination.",
|
|
3037
|
+
{
|
|
3038
|
+
session_id: V.id.optional().describe("Filter by session ID"),
|
|
3039
|
+
event_type: z4.enum(["created", "exited", "task_linked", "renamed"]).optional().describe("Filter by event type"),
|
|
3040
|
+
limit: z4.number().int().min(1).max(200).optional().describe("Max events to return (default: 50)")
|
|
3041
|
+
},
|
|
3042
|
+
async (args) => {
|
|
3043
|
+
try {
|
|
3044
|
+
let events;
|
|
3045
|
+
if (args.session_id) {
|
|
3046
|
+
events = await rpcCall("sessionEvent:listBySession", {
|
|
3047
|
+
sessionId: args.session_id,
|
|
3048
|
+
limit: args.limit ?? 50
|
|
3049
|
+
});
|
|
3050
|
+
} else {
|
|
3051
|
+
events = await rpcCall("sessionEvent:list", {
|
|
3052
|
+
eventType: args.event_type,
|
|
3053
|
+
limit: args.limit ?? 50
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
return {
|
|
3059
|
+
content: [
|
|
3060
|
+
{
|
|
3061
|
+
type: "text",
|
|
3062
|
+
text: `Error listing session events: ${err instanceof Error ? err.message : err}`
|
|
3063
|
+
}
|
|
3064
|
+
],
|
|
3065
|
+
isError: true
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// src/tools/workflows.ts
|
|
3073
|
+
import crypto2 from "crypto";
|
|
3074
|
+
import { z as z5 } from "zod";
|
|
3075
|
+
var launchAgentConfigSchema = z5.object({
|
|
3076
|
+
agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
|
|
3077
|
+
projectName: V.name,
|
|
3078
|
+
projectPath: V.absolutePath,
|
|
3079
|
+
args: z5.array(V.shortText).optional(),
|
|
3080
|
+
displayName: V.shortText.optional(),
|
|
3081
|
+
branch: V.shortText.optional(),
|
|
3082
|
+
useWorktree: z5.boolean().optional(),
|
|
3083
|
+
remoteHostId: V.id.optional(),
|
|
3084
|
+
prompt: V.prompt.optional(),
|
|
3085
|
+
promptDelayMs: z5.number().optional(),
|
|
3086
|
+
taskId: V.id.optional(),
|
|
3087
|
+
taskFromQueue: z5.boolean().optional()
|
|
3088
|
+
});
|
|
3089
|
+
var triggerConfigSchema = z5.union([
|
|
3090
|
+
z5.object({ triggerType: z5.literal("manual") }),
|
|
3091
|
+
z5.object({ triggerType: z5.literal("once"), runAt: V.shortText }),
|
|
3092
|
+
z5.object({
|
|
3093
|
+
triggerType: z5.literal("recurring"),
|
|
3094
|
+
cron: V.shortText,
|
|
3095
|
+
timezone: V.shortText.optional()
|
|
3096
|
+
}),
|
|
3097
|
+
z5.object({ triggerType: z5.literal("taskCreated"), projectFilter: V.name.optional() }),
|
|
3098
|
+
z5.object({
|
|
3099
|
+
triggerType: z5.literal("taskStatusChanged"),
|
|
3100
|
+
projectFilter: V.name.optional(),
|
|
3101
|
+
fromStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional(),
|
|
3102
|
+
toStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional()
|
|
3103
|
+
})
|
|
3104
|
+
]);
|
|
3105
|
+
var nodeSchema = z5.object({
|
|
3106
|
+
id: V.id,
|
|
3107
|
+
type: z5.enum(["trigger", "launchAgent"]),
|
|
3108
|
+
label: V.shortText,
|
|
3109
|
+
config: z5.record(z5.string(), z5.unknown()),
|
|
3110
|
+
position: z5.object({ x: z5.number(), y: z5.number() })
|
|
3111
|
+
});
|
|
3112
|
+
var edgeSchema = z5.object({
|
|
3113
|
+
id: V.id,
|
|
3114
|
+
source: V.id,
|
|
3115
|
+
target: V.id
|
|
3116
|
+
});
|
|
3117
|
+
function buildGraphFromFlat(trigger, actions) {
|
|
3118
|
+
const nodes = [];
|
|
3119
|
+
const edges = [];
|
|
3120
|
+
const triggerNode = {
|
|
3121
|
+
id: crypto2.randomUUID(),
|
|
3122
|
+
type: "trigger",
|
|
3123
|
+
label: trigger.triggerType === "manual" ? "Manual Trigger" : trigger.triggerType === "once" ? "Schedule (Once)" : trigger.triggerType === "recurring" ? "Schedule (Recurring)" : trigger.triggerType === "taskCreated" ? "When Task Created" : trigger.triggerType === "taskStatusChanged" ? "When Task Status Changes" : "Trigger",
|
|
3124
|
+
config: trigger,
|
|
3125
|
+
position: { x: 0, y: 0 }
|
|
3126
|
+
};
|
|
3127
|
+
nodes.push(triggerNode);
|
|
3128
|
+
let prevId = triggerNode.id;
|
|
3129
|
+
const NODE_GAP = 140;
|
|
3130
|
+
for (let i = 0; i < actions.length; i++) {
|
|
3131
|
+
const action = actions[i];
|
|
3132
|
+
const nodeId = crypto2.randomUUID();
|
|
3133
|
+
nodes.push({
|
|
3134
|
+
id: nodeId,
|
|
3135
|
+
type: "launchAgent",
|
|
3136
|
+
label: `Launch ${action.agentType}`,
|
|
3137
|
+
config: action,
|
|
3138
|
+
position: { x: 0, y: (i + 1) * NODE_GAP }
|
|
3139
|
+
});
|
|
3140
|
+
edges.push({
|
|
3141
|
+
id: crypto2.randomUUID(),
|
|
3142
|
+
source: prevId,
|
|
3143
|
+
target: nodeId
|
|
3144
|
+
});
|
|
3145
|
+
prevId = nodeId;
|
|
3146
|
+
}
|
|
3147
|
+
return { nodes, edges };
|
|
3148
|
+
}
|
|
3149
|
+
function registerWorkflowTools(server) {
|
|
3150
|
+
server.tool(
|
|
3151
|
+
"list_workflows",
|
|
3152
|
+
"List all workflows, optionally filtered by workspace",
|
|
3153
|
+
{
|
|
3154
|
+
workspace_id: V.id.optional().describe("Filter by workspace ID")
|
|
3155
|
+
},
|
|
3156
|
+
async (args) => {
|
|
3157
|
+
let workflows = dbListWorkflows();
|
|
3158
|
+
if (args.workspace_id) {
|
|
3159
|
+
workflows = workflows.filter((w) => (w.workspaceId ?? "personal") === args.workspace_id);
|
|
3160
|
+
}
|
|
3161
|
+
return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
|
|
3162
|
+
}
|
|
3163
|
+
);
|
|
3164
|
+
server.tool(
|
|
3165
|
+
"create_workflow",
|
|
3166
|
+
"Create a new workflow. Accepts either full nodes/edges or a convenience flat format (trigger + actions array).",
|
|
3167
|
+
{
|
|
3168
|
+
name: V.title.describe("Workflow name"),
|
|
3169
|
+
trigger: triggerConfigSchema.optional().describe("Trigger config (convenience mode). Defaults to manual."),
|
|
3170
|
+
actions: z5.array(launchAgentConfigSchema).optional().describe("Actions to execute (convenience mode). Auto-generates graph."),
|
|
3171
|
+
nodes: z5.array(nodeSchema).optional().describe("Full graph nodes (advanced mode)"),
|
|
3172
|
+
edges: z5.array(edgeSchema).optional().describe("Full graph edges (advanced mode)"),
|
|
3173
|
+
icon: V.shortText.optional().describe("Lucide icon name (default: zap)"),
|
|
3174
|
+
icon_color: V.hexColor.optional().describe("Hex color (default: #6366f1)"),
|
|
3175
|
+
enabled: z5.boolean().optional().describe("Whether workflow is enabled (default: true)"),
|
|
3176
|
+
stagger_delay_ms: z5.number().optional().describe("Delay in ms between actions")
|
|
3177
|
+
},
|
|
3178
|
+
async (args) => {
|
|
3179
|
+
let nodes;
|
|
3180
|
+
let edges;
|
|
3181
|
+
if (args.nodes && args.edges) {
|
|
3182
|
+
nodes = args.nodes;
|
|
3183
|
+
edges = args.edges;
|
|
3184
|
+
} else {
|
|
3185
|
+
const trigger = args.trigger ?? { triggerType: "manual" };
|
|
3186
|
+
const actions = args.actions ?? [];
|
|
3187
|
+
const graph = buildGraphFromFlat(trigger, actions);
|
|
3188
|
+
nodes = graph.nodes;
|
|
3189
|
+
edges = graph.edges;
|
|
3190
|
+
}
|
|
3191
|
+
const workflow = {
|
|
3192
|
+
id: crypto2.randomUUID(),
|
|
3193
|
+
name: args.name,
|
|
3194
|
+
icon: args.icon ?? "Zap",
|
|
3195
|
+
iconColor: args.icon_color ?? "#6366f1",
|
|
3196
|
+
nodes,
|
|
3197
|
+
edges,
|
|
3198
|
+
enabled: args.enabled ?? true,
|
|
3199
|
+
...args.stagger_delay_ms && { staggerDelayMs: args.stagger_delay_ms }
|
|
3200
|
+
};
|
|
3201
|
+
dbInsertWorkflow(workflow);
|
|
3202
|
+
dbSignalChange();
|
|
3203
|
+
return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
|
|
3204
|
+
}
|
|
3205
|
+
);
|
|
3206
|
+
server.tool(
|
|
3207
|
+
"update_workflow",
|
|
3208
|
+
"Update a workflow's properties",
|
|
3209
|
+
{
|
|
3210
|
+
id: V.id.describe("Workflow ID"),
|
|
3211
|
+
name: V.title.optional(),
|
|
3212
|
+
nodes: z5.array(nodeSchema).optional(),
|
|
3213
|
+
edges: z5.array(edgeSchema).optional(),
|
|
3214
|
+
icon: V.shortText.optional(),
|
|
3215
|
+
icon_color: V.hexColor.optional(),
|
|
3216
|
+
enabled: z5.boolean().optional(),
|
|
3217
|
+
stagger_delay_ms: z5.number().optional()
|
|
3218
|
+
},
|
|
3219
|
+
async (args) => {
|
|
3220
|
+
const workflows = dbListWorkflows();
|
|
3221
|
+
const workflow = workflows.find((w) => w.id === args.id);
|
|
3222
|
+
if (!workflow) {
|
|
3223
|
+
return {
|
|
3224
|
+
content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
|
|
3225
|
+
isError: true
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
const updates = {};
|
|
3229
|
+
if (args.name !== void 0) updates.name = args.name;
|
|
3230
|
+
if (args.nodes !== void 0) updates.nodes = args.nodes;
|
|
3231
|
+
if (args.edges !== void 0) updates.edges = args.edges;
|
|
3232
|
+
if (args.icon !== void 0) updates.icon = args.icon;
|
|
3233
|
+
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
3234
|
+
if (args.enabled !== void 0) updates.enabled = args.enabled;
|
|
3235
|
+
if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
|
|
3236
|
+
dbUpdateWorkflow(args.id, updates);
|
|
3237
|
+
dbSignalChange();
|
|
3238
|
+
return {
|
|
3239
|
+
content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
);
|
|
3243
|
+
server.tool(
|
|
3244
|
+
"delete_workflow",
|
|
3245
|
+
"Delete a workflow",
|
|
3246
|
+
{ id: V.id.describe("Workflow ID") },
|
|
3247
|
+
async (args) => {
|
|
3248
|
+
const workflows = dbListWorkflows();
|
|
3249
|
+
const workflow = workflows.find((w) => w.id === args.id);
|
|
3250
|
+
if (!workflow) {
|
|
3251
|
+
return {
|
|
3252
|
+
content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
|
|
3253
|
+
isError: true
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
dbDeleteWorkflow(args.id);
|
|
3257
|
+
dbSignalChange();
|
|
3258
|
+
return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
|
|
3259
|
+
}
|
|
3260
|
+
);
|
|
3261
|
+
server.tool(
|
|
3262
|
+
"list_workflow_runs",
|
|
3263
|
+
"List workflow execution history. Filter by workflow_id or task_id.",
|
|
3264
|
+
{
|
|
3265
|
+
workflow_id: V.id.optional().describe("Filter by workflow ID"),
|
|
3266
|
+
task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
|
|
3267
|
+
limit: z5.number().int().min(1).max(100).optional().describe("Max results (default: 20)")
|
|
3268
|
+
},
|
|
3269
|
+
async (args) => {
|
|
3270
|
+
if (args.workflow_id && args.task_id) {
|
|
3271
|
+
return {
|
|
3272
|
+
content: [{ type: "text", text: "Error: provide workflow_id or task_id, not both" }],
|
|
3273
|
+
isError: true
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3276
|
+
if (args.task_id) {
|
|
3277
|
+
const runs = listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
|
|
3278
|
+
return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
|
|
3279
|
+
}
|
|
3280
|
+
if (args.workflow_id) {
|
|
3281
|
+
const runs = listWorkflowRuns(args.workflow_id, args.limit ?? 20);
|
|
3282
|
+
return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
|
|
3283
|
+
}
|
|
3284
|
+
return {
|
|
3285
|
+
content: [{ type: "text", text: "Error: provide either workflow_id or task_id" }],
|
|
3286
|
+
isError: true
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
);
|
|
3290
|
+
server.tool(
|
|
3291
|
+
"get_workflow_schedule",
|
|
3292
|
+
"Get scheduler info for a workflow: execution log or next scheduled run. Requires the BaseGrid app to be running.",
|
|
3293
|
+
{
|
|
3294
|
+
workflow_id: V.id.optional().describe("Workflow ID (required for next_run, optional for log)"),
|
|
3295
|
+
info: z5.enum(["log", "next_run"]).optional().describe("What to retrieve (default: log)")
|
|
3296
|
+
},
|
|
3297
|
+
async (args) => {
|
|
3298
|
+
const info = args.info ?? "log";
|
|
3299
|
+
try {
|
|
3300
|
+
if (info === "next_run") {
|
|
3301
|
+
if (!args.workflow_id) {
|
|
3302
|
+
return {
|
|
3303
|
+
content: [{ type: "text", text: "Error: workflow_id is required for next_run" }],
|
|
3304
|
+
isError: true
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
const nextRun = await rpcCall("scheduler:getNextRun", args.workflow_id);
|
|
3308
|
+
return {
|
|
3309
|
+
content: [
|
|
3310
|
+
{
|
|
3311
|
+
type: "text",
|
|
3312
|
+
text: nextRun ? JSON.stringify({ nextRun }, null, 2) : "No scheduled run (workflow may be manual or disabled)"
|
|
3313
|
+
}
|
|
3314
|
+
]
|
|
3315
|
+
};
|
|
3316
|
+
} else {
|
|
3317
|
+
const log2 = await rpcCall("scheduler:getLog", args.workflow_id);
|
|
3318
|
+
return { content: [{ type: "text", text: JSON.stringify(log2, null, 2) }] };
|
|
3319
|
+
}
|
|
3320
|
+
} catch (err) {
|
|
3321
|
+
return {
|
|
3322
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
|
|
3323
|
+
isError: true
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// src/tools/config.ts
|
|
3331
|
+
function registerConfigTools(server) {
|
|
3332
|
+
server.tool(
|
|
3333
|
+
"get_config",
|
|
3334
|
+
"Get the full BaseGrid configuration (projects, tasks, workflows, settings)",
|
|
3335
|
+
async () => {
|
|
3336
|
+
const config = configManager.loadConfig();
|
|
3337
|
+
return { content: [{ type: "text", text: JSON.stringify(config, null, 2) }] };
|
|
3338
|
+
}
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
// src/tools/workspaces.ts
|
|
3343
|
+
import crypto3 from "crypto";
|
|
3344
|
+
import { z as z6 } from "zod";
|
|
3345
|
+
function registerWorkspaceTools(server) {
|
|
3346
|
+
server.tool("list_workspaces", "List all workspaces", async () => {
|
|
3347
|
+
const workspaces = dbListWorkspaces();
|
|
3348
|
+
return { content: [{ type: "text", text: JSON.stringify(workspaces, null, 2) }] };
|
|
3349
|
+
});
|
|
3350
|
+
server.tool(
|
|
3351
|
+
"create_workspace",
|
|
3352
|
+
"Create a new workspace for organizing projects",
|
|
3353
|
+
{
|
|
3354
|
+
name: V.name.describe("Workspace name"),
|
|
3355
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
3356
|
+
icon_color: V.hexColor.optional().describe("Hex color for icon")
|
|
3357
|
+
},
|
|
3358
|
+
async (args) => {
|
|
3359
|
+
const existing = dbListWorkspaces();
|
|
3360
|
+
const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
|
|
3361
|
+
const workspace = {
|
|
3362
|
+
id: crypto3.randomUUID(),
|
|
3363
|
+
name: args.name,
|
|
3364
|
+
order: maxOrder + 1,
|
|
3365
|
+
...args.icon && { icon: args.icon },
|
|
3366
|
+
...args.icon_color && { iconColor: args.icon_color }
|
|
3367
|
+
};
|
|
3368
|
+
dbInsertWorkspace(workspace);
|
|
3369
|
+
dbSignalChange();
|
|
3370
|
+
return { content: [{ type: "text", text: JSON.stringify(workspace, null, 2) }] };
|
|
3371
|
+
}
|
|
3372
|
+
);
|
|
3373
|
+
server.tool(
|
|
3374
|
+
"update_workspace",
|
|
3375
|
+
"Update a workspace's properties",
|
|
3376
|
+
{
|
|
3377
|
+
id: V.id.describe("Workspace ID"),
|
|
3378
|
+
name: V.name.optional().describe("New name"),
|
|
3379
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
3380
|
+
icon_color: V.hexColor.optional().describe("Hex color for icon"),
|
|
3381
|
+
order: z6.number().int().min(0).optional().describe("Sort order")
|
|
3382
|
+
},
|
|
3383
|
+
async (args) => {
|
|
3384
|
+
const existing = dbListWorkspaces();
|
|
3385
|
+
if (!existing.find((w) => w.id === args.id)) {
|
|
3386
|
+
return {
|
|
3387
|
+
content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
|
|
3388
|
+
isError: true
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
const updates = {};
|
|
3392
|
+
if (args.name !== void 0) updates.name = args.name;
|
|
3393
|
+
if (args.icon !== void 0) updates.icon = args.icon;
|
|
3394
|
+
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
3395
|
+
if (args.order !== void 0) updates.order = args.order;
|
|
3396
|
+
dbUpdateWorkspace(args.id, updates);
|
|
3397
|
+
dbSignalChange();
|
|
3398
|
+
const updated = dbListWorkspaces().find((w) => w.id === args.id);
|
|
3399
|
+
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
3400
|
+
}
|
|
3401
|
+
);
|
|
3402
|
+
server.tool(
|
|
3403
|
+
"delete_workspace",
|
|
3404
|
+
"Delete a workspace",
|
|
3405
|
+
{ id: V.id.describe("Workspace ID") },
|
|
3406
|
+
async (args) => {
|
|
3407
|
+
if (args.id === "personal") {
|
|
3408
|
+
return {
|
|
3409
|
+
content: [{ type: "text", text: "Error: cannot delete the default workspace" }],
|
|
3410
|
+
isError: true
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
const existing = dbListWorkspaces();
|
|
3414
|
+
const workspace = existing.find((w) => w.id === args.id);
|
|
3415
|
+
if (!workspace) {
|
|
3416
|
+
return {
|
|
3417
|
+
content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
|
|
3418
|
+
isError: true
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
dbDeleteWorkspace(args.id);
|
|
3422
|
+
dbSignalChange();
|
|
3423
|
+
return { content: [{ type: "text", text: `Deleted workspace: ${workspace.name}` }] };
|
|
3424
|
+
}
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// src/server.ts
|
|
3429
|
+
function createMcpServer(version) {
|
|
3430
|
+
const server = new McpServer({ name: "basegrid", version }, { capabilities: { tools: {} } });
|
|
3431
|
+
registerConfigTools(server);
|
|
3432
|
+
registerProjectTools(server);
|
|
3433
|
+
registerTaskTools(server);
|
|
3434
|
+
registerSessionTools(server);
|
|
3435
|
+
registerWorkflowTools(server);
|
|
3436
|
+
registerWorkspaceTools(server);
|
|
3437
|
+
return server;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// src/index.ts
|
|
3441
|
+
var _origError = console.error;
|
|
3442
|
+
console.log = (...args) => _origError("[mcp]", ...args);
|
|
3443
|
+
console.info = (...args) => _origError("[mcp]", ...args);
|
|
3444
|
+
console.debug = (...args) => _origError("[mcp:debug]", ...args);
|
|
3445
|
+
console.warn = (...args) => _origError("[mcp:warn]", ...args);
|
|
3446
|
+
console.error = (...args) => _origError("[mcp:error]", ...args);
|
|
3447
|
+
async function main() {
|
|
3448
|
+
configManager.init();
|
|
3449
|
+
const version = true ? "0.6.5" : createRequire(import.meta.url)("../package.json").version;
|
|
3450
|
+
const server = createMcpServer(version);
|
|
3451
|
+
const transport = new StdioServerTransport();
|
|
3452
|
+
await server.connect(transport);
|
|
3453
|
+
transport.onclose = () => {
|
|
3454
|
+
configManager.close();
|
|
3455
|
+
process.exit(0);
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
main().catch((err) => {
|
|
3459
|
+
console.error("Failed to start MCP server:", err);
|
|
3460
|
+
process.exit(1);
|
|
3461
|
+
});
|