@vornrun/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2507 -0
  2. package/package.json +47 -0
package/dist/index.js ADDED
@@ -0,0 +1,2507 @@
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 fs2 from "fs";
9
+ import path2 from "path";
10
+ import os2 from "os";
11
+
12
+ // ../shared/src/agent-defaults.ts
13
+ var DEFAULT_AGENT_COMMANDS = {
14
+ claude: {
15
+ command: "claude",
16
+ args: [],
17
+ headlessArgs: ["--dangerously-skip-permissions"]
18
+ },
19
+ copilot: {
20
+ command: "copilot",
21
+ args: [],
22
+ headlessArgs: ["--allow-all"]
23
+ },
24
+ codex: {
25
+ command: "codex",
26
+ args: [],
27
+ headlessArgs: ["-a", "never"]
28
+ },
29
+ opencode: { command: "opencode", args: [] },
30
+ gemini: {
31
+ command: "gemini",
32
+ args: [],
33
+ headlessArgs: ["-y"]
34
+ }
35
+ };
36
+
37
+ // ../server/src/database.ts
38
+ import Database from "libsql";
39
+ import path from "path";
40
+ import os from "os";
41
+ import fs from "fs";
42
+
43
+ // ../server/src/logger.ts
44
+ import pino from "pino";
45
+ var log = pino({
46
+ level: process.env.VITEST ? "silent" : "info",
47
+ // Always write to stderr so the main process can capture via electron-log.
48
+ // Previously production used the default (stdout), but nothing reads stdout
49
+ // after the initial port banner — so all server logs were silently dropped.
50
+ transport: { target: "pino/file", options: { destination: 2 } }
51
+ });
52
+ var logger_default = log;
53
+
54
+ // ../shared/src/types.ts
55
+ var DEFAULT_WORKSPACE = {
56
+ id: "personal",
57
+ name: "Personal",
58
+ icon: "User",
59
+ iconColor: "#6b7280",
60
+ order: 0
61
+ };
62
+
63
+ // ../server/src/database.ts
64
+ var CONFIG_DIR = path.join(os.homedir(), ".vorn");
65
+ var DB_PATH = path.join(CONFIG_DIR, "vorn.db");
66
+ var db = null;
67
+ function getDb() {
68
+ if (!db) throw new Error("Database not initialized. Call initDatabase() first.");
69
+ return db;
70
+ }
71
+ function initDatabase() {
72
+ if (!fs.existsSync(CONFIG_DIR)) {
73
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
74
+ }
75
+ try {
76
+ db = new Database(DB_PATH);
77
+ db.pragma("journal_mode = WAL");
78
+ db.pragma("foreign_keys = ON");
79
+ createSchema();
80
+ } catch (err) {
81
+ logger_default.error("[database] Failed to open database:", err);
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ const isCorrupt = /corrupt|notadb|malformed|not a database|file is not a database/i.test(
84
+ message
85
+ );
86
+ if (isCorrupt) {
87
+ logger_default.warn("[database] Database appears corrupt, attempting recovery...");
88
+ recoverCorruptDatabase();
89
+ } else {
90
+ throw err;
91
+ }
92
+ }
93
+ }
94
+ function recoverCorruptDatabase() {
95
+ try {
96
+ db?.close();
97
+ } catch {
98
+ }
99
+ db = null;
100
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
101
+ const backupPath = `${DB_PATH}.corrupt-${timestamp}`;
102
+ try {
103
+ if (fs.existsSync(DB_PATH)) {
104
+ fs.copyFileSync(DB_PATH, backupPath);
105
+ logger_default.info(`[database] Backed up corrupt database to ${backupPath}`);
106
+ }
107
+ for (const suffix of ["", "-wal", "-shm"]) {
108
+ const file = DB_PATH + suffix;
109
+ if (fs.existsSync(file)) fs.unlinkSync(file);
110
+ }
111
+ } catch (backupErr) {
112
+ logger_default.error("[database] Failed to back up corrupt database:", backupErr);
113
+ }
114
+ try {
115
+ db = new Database(DB_PATH);
116
+ db.pragma("journal_mode = WAL");
117
+ db.pragma("foreign_keys = ON");
118
+ createSchema();
119
+ logger_default.info("[database] Successfully created fresh database after corruption recovery");
120
+ } catch (freshErr) {
121
+ logger_default.error("[database] Failed to create fresh database after corruption:", freshErr);
122
+ throw freshErr;
123
+ }
124
+ logger_default.warn(`[database] Database was corrupted and has been reset. Backup saved to: ${backupPath}`);
125
+ }
126
+ function dbSignalChange() {
127
+ try {
128
+ const signalPath = path.join(CONFIG_DIR, ".db-signal");
129
+ fs.writeFileSync(signalPath, Date.now().toString());
130
+ } catch {
131
+ }
132
+ }
133
+ function closeDatabase() {
134
+ if (db) {
135
+ db.close();
136
+ db = null;
137
+ }
138
+ }
139
+ function createSchema() {
140
+ const d = getDb();
141
+ const cols = d.prepare("PRAGMA table_info(workflows)").all();
142
+ if (cols.some((c) => c.name === "actions")) {
143
+ d.exec("ALTER TABLE workflows RENAME TO workflows_backup_old_format");
144
+ logger_default.warn("[database] migrated old-format workflows table to workflows_backup_old_format");
145
+ }
146
+ d.exec(`
147
+ CREATE TABLE IF NOT EXISTS schema_meta (
148
+ key TEXT PRIMARY KEY,
149
+ value TEXT NOT NULL
150
+ );
151
+
152
+ CREATE TABLE IF NOT EXISTS defaults (
153
+ key TEXT PRIMARY KEY,
154
+ value TEXT NOT NULL
155
+ );
156
+
157
+ CREATE TABLE IF NOT EXISTS projects (
158
+ name TEXT PRIMARY KEY,
159
+ path TEXT NOT NULL,
160
+ preferred_agents TEXT NOT NULL DEFAULT '[]',
161
+ icon TEXT,
162
+ icon_color TEXT,
163
+ host_ids TEXT
164
+ );
165
+
166
+ CREATE TABLE IF NOT EXISTS workflows (
167
+ id TEXT PRIMARY KEY,
168
+ name TEXT NOT NULL,
169
+ icon TEXT NOT NULL,
170
+ icon_color TEXT NOT NULL,
171
+ nodes TEXT NOT NULL DEFAULT '[]',
172
+ edges TEXT NOT NULL DEFAULT '[]',
173
+ enabled INTEGER NOT NULL DEFAULT 1,
174
+ last_run_at TEXT,
175
+ last_run_status TEXT,
176
+ stagger_delay_ms INTEGER
177
+ );
178
+
179
+ CREATE TABLE IF NOT EXISTS agent_commands (
180
+ agent_type TEXT PRIMARY KEY,
181
+ command TEXT NOT NULL,
182
+ args TEXT NOT NULL DEFAULT '[]',
183
+ headless_args TEXT,
184
+ fallback_command TEXT,
185
+ fallback_args TEXT
186
+ );
187
+
188
+ CREATE TABLE IF NOT EXISTS remote_hosts (
189
+ id TEXT PRIMARY KEY,
190
+ label TEXT NOT NULL,
191
+ hostname TEXT NOT NULL,
192
+ user TEXT NOT NULL,
193
+ port INTEGER NOT NULL DEFAULT 22,
194
+ auth_method TEXT DEFAULT 'agent',
195
+ ssh_key_path TEXT,
196
+ credential_id TEXT,
197
+ encrypted_password TEXT,
198
+ ssh_options TEXT
199
+ );
200
+
201
+ CREATE TABLE IF NOT EXISTS ssh_keys (
202
+ id TEXT PRIMARY KEY,
203
+ label TEXT NOT NULL,
204
+ encrypted_private_key TEXT NOT NULL,
205
+ public_key TEXT,
206
+ certificate TEXT,
207
+ key_type TEXT,
208
+ created_at TEXT NOT NULL
209
+ );
210
+
211
+ CREATE TABLE IF NOT EXISTS tasks (
212
+ id TEXT PRIMARY KEY,
213
+ project_name TEXT NOT NULL,
214
+ title TEXT NOT NULL,
215
+ description TEXT NOT NULL DEFAULT '',
216
+ status TEXT NOT NULL DEFAULT 'todo',
217
+ "order" INTEGER NOT NULL DEFAULT 0,
218
+ assigned_session_id TEXT,
219
+ assigned_agent TEXT,
220
+ agent_session_id TEXT,
221
+ branch TEXT,
222
+ use_worktree INTEGER DEFAULT 0,
223
+ created_at TEXT NOT NULL,
224
+ updated_at TEXT NOT NULL,
225
+ completed_at TEXT
226
+ );
227
+
228
+ CREATE TABLE IF NOT EXISTS sessions (
229
+ id TEXT PRIMARY KEY,
230
+ agent_type TEXT NOT NULL,
231
+ project_name TEXT NOT NULL,
232
+ project_path TEXT NOT NULL,
233
+ status TEXT NOT NULL,
234
+ created_at INTEGER NOT NULL,
235
+ pid INTEGER NOT NULL,
236
+ display_name TEXT,
237
+ branch TEXT,
238
+ worktree_path TEXT,
239
+ is_worktree INTEGER DEFAULT 0,
240
+ remote_host_id TEXT,
241
+ remote_host_label TEXT,
242
+ hook_session_id TEXT,
243
+ status_source TEXT,
244
+ saved_at INTEGER,
245
+ sort_order INTEGER NOT NULL DEFAULT 0,
246
+ worktree_name TEXT
247
+ );
248
+
249
+ CREATE TABLE IF NOT EXISTS schedule_log (
250
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
251
+ workflow_id TEXT NOT NULL,
252
+ workflow_name TEXT NOT NULL,
253
+ executed_at TEXT NOT NULL,
254
+ status TEXT NOT NULL,
255
+ sessions_launched INTEGER NOT NULL DEFAULT 0,
256
+ error TEXT
257
+ );
258
+
259
+ CREATE INDEX IF NOT EXISTS idx_schedule_log_workflow_id ON schedule_log(workflow_id);
260
+ CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_name, status);
261
+
262
+ CREATE TABLE IF NOT EXISTS archived_sessions (
263
+ id TEXT PRIMARY KEY,
264
+ agent_type TEXT NOT NULL,
265
+ project_name TEXT NOT NULL,
266
+ project_path TEXT NOT NULL,
267
+ display_name TEXT,
268
+ branch TEXT,
269
+ agent_session_id TEXT,
270
+ archived_at INTEGER NOT NULL
271
+ );
272
+
273
+ CREATE TABLE IF NOT EXISTS workspaces (
274
+ id TEXT PRIMARY KEY,
275
+ name TEXT NOT NULL,
276
+ icon TEXT,
277
+ icon_color TEXT,
278
+ "order" INTEGER NOT NULL DEFAULT 0
279
+ );
280
+
281
+ CREATE TABLE IF NOT EXISTS workflow_runs (
282
+ id TEXT PRIMARY KEY,
283
+ workflow_id TEXT NOT NULL,
284
+ started_at TEXT NOT NULL,
285
+ completed_at TEXT,
286
+ status TEXT NOT NULL DEFAULT 'running',
287
+ trigger_task_id TEXT
288
+ );
289
+
290
+ CREATE TABLE IF NOT EXISTS workflow_run_nodes (
291
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
292
+ run_id TEXT NOT NULL,
293
+ node_id TEXT NOT NULL,
294
+ status TEXT NOT NULL DEFAULT 'pending',
295
+ started_at TEXT,
296
+ completed_at TEXT,
297
+ session_id TEXT,
298
+ error TEXT,
299
+ logs TEXT,
300
+ task_id TEXT,
301
+ agent_session_id TEXT,
302
+ FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
303
+ );
304
+
305
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow ON workflow_runs(workflow_id);
306
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_task ON workflow_runs(trigger_task_id);
307
+ CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_run ON workflow_run_nodes(run_id);
308
+ CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_task ON workflow_run_nodes(task_id);
309
+
310
+ CREATE TABLE IF NOT EXISTS session_logs (
311
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
312
+ task_id TEXT NOT NULL,
313
+ session_id TEXT NOT NULL,
314
+ agent_type TEXT,
315
+ branch TEXT,
316
+ status TEXT NOT NULL DEFAULT 'running',
317
+ started_at TEXT NOT NULL,
318
+ completed_at TEXT,
319
+ exit_code INTEGER,
320
+ logs TEXT,
321
+ project_name TEXT
322
+ );
323
+
324
+ CREATE INDEX IF NOT EXISTS idx_session_logs_task ON session_logs(task_id);
325
+ CREATE INDEX IF NOT EXISTS idx_session_logs_session ON session_logs(session_id);
326
+
327
+ CREATE TABLE IF NOT EXISTS session_events (
328
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
329
+ session_id TEXT NOT NULL,
330
+ event_type TEXT NOT NULL,
331
+ timestamp TEXT NOT NULL,
332
+ metadata TEXT
333
+ );
334
+
335
+ CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp DESC);
336
+ CREATE INDEX IF NOT EXISTS idx_session_events_type ON session_events(event_type, timestamp DESC);
337
+ `);
338
+ migrateSchema(d);
339
+ verifySchema(d);
340
+ }
341
+ function migrateSchema(d) {
342
+ const row = d.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
343
+ const version = row ? parseInt(row.value, 10) : 0;
344
+ if (version < 1) {
345
+ d.transaction(() => {
346
+ const projectCols = d.prepare("PRAGMA table_info(projects)").all();
347
+ if (!projectCols.some((c) => c.name === "workspace_id")) {
348
+ d.exec("ALTER TABLE projects ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'");
349
+ }
350
+ const workflowCols = d.prepare("PRAGMA table_info(workflows)").all();
351
+ if (!workflowCols.some((c) => c.name === "workspace_id")) {
352
+ d.exec("ALTER TABLE workflows ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'");
353
+ }
354
+ d.prepare(
355
+ `INSERT OR IGNORE INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
356
+ ).run(
357
+ DEFAULT_WORKSPACE.id,
358
+ DEFAULT_WORKSPACE.name,
359
+ DEFAULT_WORKSPACE.icon ?? null,
360
+ DEFAULT_WORKSPACE.iconColor ?? null,
361
+ DEFAULT_WORKSPACE.order
362
+ );
363
+ d.prepare(
364
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '1')"
365
+ ).run();
366
+ })();
367
+ logger_default.info("[database] migrated schema to version 1 (workspaces)");
368
+ }
369
+ if (version < 2) {
370
+ d.transaction(() => {
371
+ const hostCols = d.prepare("PRAGMA table_info(remote_hosts)").all();
372
+ if (!hostCols.some((c) => c.name === "auth_method")) {
373
+ d.exec("ALTER TABLE remote_hosts ADD COLUMN auth_method TEXT");
374
+ d.exec("ALTER TABLE remote_hosts ADD COLUMN credential_id TEXT");
375
+ d.exec("ALTER TABLE remote_hosts ADD COLUMN encrypted_password TEXT");
376
+ d.exec(
377
+ "UPDATE remote_hosts SET auth_method = CASE WHEN ssh_key_path IS NOT NULL AND ssh_key_path != '' THEN 'key-file' ELSE 'agent' END"
378
+ );
379
+ }
380
+ d.exec(`
381
+ CREATE TABLE IF NOT EXISTS ssh_keys (
382
+ id TEXT PRIMARY KEY,
383
+ label TEXT NOT NULL,
384
+ encrypted_private_key TEXT NOT NULL,
385
+ public_key TEXT,
386
+ certificate TEXT,
387
+ key_type TEXT,
388
+ created_at TEXT NOT NULL
389
+ )
390
+ `);
391
+ d.prepare(
392
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '2')"
393
+ ).run();
394
+ })();
395
+ logger_default.info("[database] migrated schema to version 2 (ssh credential vault)");
396
+ }
397
+ if (version < 3) {
398
+ d.transaction(() => {
399
+ const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
400
+ if (!sessionCols.some((c) => c.name === "sort_order")) {
401
+ d.exec("ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0");
402
+ }
403
+ d.prepare(
404
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '3')"
405
+ ).run();
406
+ })();
407
+ logger_default.info("[database] migrated schema to version 3 (session sort order)");
408
+ }
409
+ if (version < 4) {
410
+ d.transaction(() => {
411
+ const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
412
+ if (!sessionCols.some((c) => c.name === "worktree_name")) {
413
+ d.exec("ALTER TABLE sessions ADD COLUMN worktree_name TEXT");
414
+ }
415
+ d.prepare(
416
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '4')"
417
+ ).run();
418
+ })();
419
+ logger_default.info("[database] migrated schema to version 4 (worktree name)");
420
+ }
421
+ if (version < 5) {
422
+ d.transaction(() => {
423
+ const agentCols = d.prepare("PRAGMA table_info(agent_commands)").all();
424
+ if (!agentCols.some((c) => c.name === "headless_args")) {
425
+ d.exec("ALTER TABLE agent_commands ADD COLUMN headless_args TEXT");
426
+ }
427
+ d.prepare(
428
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '5')"
429
+ ).run();
430
+ })();
431
+ logger_default.info("[database] migrated schema to version 5 (headless args)");
432
+ }
433
+ if (version < 6) {
434
+ d.transaction(() => {
435
+ const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
436
+ if (!sessionCols.some((c) => c.name === "claude_session_id")) {
437
+ d.exec("ALTER TABLE sessions ADD COLUMN claude_session_id TEXT");
438
+ }
439
+ d.prepare(
440
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '6')"
441
+ ).run();
442
+ })();
443
+ logger_default.info("[database] migrated schema to version 6 (claude session id)");
444
+ }
445
+ }
446
+ function verifySchema(d) {
447
+ const expectedByTable = {
448
+ projects: [
449
+ {
450
+ column: "workspace_id",
451
+ ddl: "ALTER TABLE projects ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'"
452
+ }
453
+ ],
454
+ workflows: [
455
+ {
456
+ column: "workspace_id",
457
+ ddl: "ALTER TABLE workflows ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'"
458
+ }
459
+ ],
460
+ remote_hosts: [
461
+ { column: "auth_method", ddl: "ALTER TABLE remote_hosts ADD COLUMN auth_method TEXT" },
462
+ { column: "credential_id", ddl: "ALTER TABLE remote_hosts ADD COLUMN credential_id TEXT" },
463
+ {
464
+ column: "encrypted_password",
465
+ ddl: "ALTER TABLE remote_hosts ADD COLUMN encrypted_password TEXT"
466
+ }
467
+ ],
468
+ sessions: [
469
+ {
470
+ column: "sort_order",
471
+ ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
472
+ },
473
+ { column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
474
+ { column: "claude_session_id", ddl: "ALTER TABLE sessions ADD COLUMN claude_session_id TEXT" }
475
+ ],
476
+ agent_commands: [
477
+ {
478
+ column: "headless_args",
479
+ ddl: "ALTER TABLE agent_commands ADD COLUMN headless_args TEXT"
480
+ }
481
+ ]
482
+ };
483
+ for (const [table, columns] of Object.entries(expectedByTable)) {
484
+ const existing = new Set(
485
+ d.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name)
486
+ );
487
+ for (const { column, ddl } of columns) {
488
+ if (existing.has(column)) continue;
489
+ try {
490
+ d.exec(ddl);
491
+ logger_default.warn(`[database] self-heal: added missing column ${table}.${column}`);
492
+ } catch (err) {
493
+ logger_default.error(`[database] self-heal: failed to add ${table}.${column}:`, err);
494
+ }
495
+ }
496
+ }
497
+ }
498
+ function loadConfig() {
499
+ const d = getDb();
500
+ const defaults = loadDefaults(d);
501
+ const projects = loadProjects(d);
502
+ const agentCommands = loadAgentCommands(d);
503
+ const workflows = loadWorkflows(d);
504
+ const remoteHosts = loadRemoteHosts(d);
505
+ const tasks = loadTasks(d);
506
+ const workspaces = loadWorkspaces(d);
507
+ return {
508
+ version: 1,
509
+ defaults,
510
+ projects,
511
+ agentCommands: Object.keys(agentCommands).length > 0 ? agentCommands : { ...DEFAULT_AGENT_COMMANDS },
512
+ workflows,
513
+ remoteHosts,
514
+ tasks,
515
+ workspaces
516
+ };
517
+ }
518
+ function loadDefaults(d) {
519
+ const rows = d.prepare("SELECT key, value FROM defaults").all();
520
+ const map = {};
521
+ for (const row of rows) {
522
+ map[row.key] = JSON.parse(row.value);
523
+ }
524
+ return {
525
+ shell: map.shell ?? (process.platform === "win32" ? process.env.COMSPEC || "powershell.exe" : process.env.SHELL || "/bin/zsh"),
526
+ fontSize: map.fontSize ?? 13,
527
+ theme: map.theme ?? "dark",
528
+ ...map.rowHeight !== void 0 && { rowHeight: map.rowHeight },
529
+ ...map.defaultAgent !== void 0 && { defaultAgent: map.defaultAgent },
530
+ ...map.notifications !== void 0 && {
531
+ notifications: map.notifications
532
+ },
533
+ ...map.hasSeenOnboarding !== void 0 && {
534
+ hasSeenOnboarding: map.hasSeenOnboarding
535
+ },
536
+ ...map.reopenSessions !== void 0 && { reopenSessions: map.reopenSessions },
537
+ ...map.widgetEnabled !== void 0 && { widgetEnabled: map.widgetEnabled },
538
+ ...map.taskViewMode !== void 0 && {
539
+ taskViewMode: map.taskViewMode
540
+ },
541
+ ...map.activeWorkspace !== void 0 && {
542
+ activeWorkspace: map.activeWorkspace
543
+ },
544
+ ...map.mainViewMode !== void 0 && {
545
+ mainViewMode: map.mainViewMode
546
+ },
547
+ ...map.layoutMode !== void 0 && {
548
+ layoutMode: map.layoutMode
549
+ },
550
+ ...map.updateChannel !== void 0 && {
551
+ updateChannel: map.updateChannel
552
+ },
553
+ ...map.webAccessEnabled !== void 0 && {
554
+ webAccessEnabled: map.webAccessEnabled
555
+ },
556
+ ...map.mobileAccessEnabled !== void 0 && {
557
+ mobileAccessEnabled: map.mobileAccessEnabled
558
+ },
559
+ ...map.networkAccessEnabled !== void 0 && {
560
+ networkAccessEnabled: map.networkAccessEnabled
561
+ },
562
+ ...map.showHeadlessAgents !== void 0 && {
563
+ showHeadlessAgents: map.showHeadlessAgents
564
+ },
565
+ ...map.headlessRetentionMinutes !== void 0 && {
566
+ headlessRetentionMinutes: map.headlessRetentionMinutes
567
+ }
568
+ };
569
+ }
570
+ function loadProjects(d) {
571
+ const rows = d.prepare("SELECT * FROM projects").all();
572
+ return rows.map(rowToProject);
573
+ }
574
+ function loadWorkflows(d) {
575
+ const rows = d.prepare("SELECT * FROM workflows").all();
576
+ return rows.map(rowToWorkflow);
577
+ }
578
+ function loadAgentCommands(d) {
579
+ const rows = d.prepare("SELECT * FROM agent_commands").all();
580
+ const result = {};
581
+ for (const r of rows) {
582
+ result[r.agent_type] = {
583
+ command: r.command,
584
+ args: JSON.parse(r.args),
585
+ ...r.headless_args != null && { headlessArgs: JSON.parse(r.headless_args) },
586
+ ...r.fallback_command != null && { fallbackCommand: r.fallback_command },
587
+ ...r.fallback_args != null && { fallbackArgs: JSON.parse(r.fallback_args) }
588
+ };
589
+ }
590
+ return result;
591
+ }
592
+ function loadRemoteHosts(d) {
593
+ const rows = d.prepare("SELECT * FROM remote_hosts").all();
594
+ return rows.map((r) => ({
595
+ id: r.id,
596
+ label: r.label,
597
+ hostname: r.hostname,
598
+ user: r.user,
599
+ port: r.port,
600
+ ...r.auth_method != null && { authMethod: r.auth_method },
601
+ ...r.ssh_key_path != null && { sshKeyPath: r.ssh_key_path },
602
+ ...r.credential_id != null && { credentialId: r.credential_id },
603
+ ...r.encrypted_password != null && { encryptedPassword: r.encrypted_password },
604
+ ...r.ssh_options != null && { sshOptions: r.ssh_options }
605
+ }));
606
+ }
607
+ function loadTasks(d) {
608
+ const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
609
+ return rows.map(rowToTask);
610
+ }
611
+ function loadWorkspaces(d) {
612
+ const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
613
+ return rows.map(rowToWorkspace);
614
+ }
615
+ function saveConfig(config) {
616
+ const d = getDb();
617
+ const run = d.transaction(() => {
618
+ d.prepare("DELETE FROM defaults").run();
619
+ const insertDefault = d.prepare("INSERT INTO defaults (key, value) VALUES (?, ?)");
620
+ for (const [key, value] of Object.entries(config.defaults)) {
621
+ if (value !== void 0) {
622
+ insertDefault.run(key, JSON.stringify(value));
623
+ }
624
+ }
625
+ d.prepare("DELETE FROM projects").run();
626
+ const insertProject = d.prepare(
627
+ "INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
628
+ );
629
+ for (const p of config.projects) {
630
+ insertProject.run(
631
+ p.name,
632
+ p.path,
633
+ JSON.stringify(p.preferredAgents),
634
+ p.icon ?? null,
635
+ p.iconColor ?? null,
636
+ p.hostIds ? JSON.stringify(p.hostIds) : null,
637
+ p.workspaceId ?? "personal"
638
+ );
639
+ }
640
+ d.prepare("DELETE FROM workflows").run();
641
+ const insertWorkflow = d.prepare(
642
+ `INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
643
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
644
+ );
645
+ for (const w of config.workflows ?? []) {
646
+ insertWorkflow.run(
647
+ w.id,
648
+ w.name,
649
+ w.icon,
650
+ w.iconColor,
651
+ JSON.stringify(w.nodes),
652
+ JSON.stringify(w.edges),
653
+ w.enabled ? 1 : 0,
654
+ w.lastRunAt ?? null,
655
+ w.lastRunStatus ?? null,
656
+ w.staggerDelayMs ?? null,
657
+ w.workspaceId ?? "personal"
658
+ );
659
+ }
660
+ d.prepare("DELETE FROM agent_commands").run();
661
+ const insertAgent = d.prepare(
662
+ "INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?, ?)"
663
+ );
664
+ if (config.agentCommands) {
665
+ for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
666
+ if (cmd) {
667
+ insertAgent.run(
668
+ agentType,
669
+ cmd.command,
670
+ JSON.stringify(cmd.args),
671
+ cmd.headlessArgs ? JSON.stringify(cmd.headlessArgs) : null,
672
+ cmd.fallbackCommand ?? null,
673
+ cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
674
+ );
675
+ }
676
+ }
677
+ }
678
+ d.prepare("DELETE FROM remote_hosts").run();
679
+ const insertHost = d.prepare(
680
+ "INSERT INTO remote_hosts (id, label, hostname, user, port, auth_method, ssh_key_path, credential_id, encrypted_password, ssh_options) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
681
+ );
682
+ for (const h of config.remoteHosts ?? []) {
683
+ insertHost.run(
684
+ h.id,
685
+ h.label,
686
+ h.hostname,
687
+ h.user,
688
+ h.port,
689
+ h.authMethod ?? "agent",
690
+ h.sshKeyPath ?? null,
691
+ h.credentialId ?? null,
692
+ h.encryptedPassword ?? null,
693
+ h.sshOptions ?? null
694
+ );
695
+ }
696
+ d.prepare("DELETE FROM tasks").run();
697
+ const insertTask = d.prepare(
698
+ `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)
699
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
700
+ );
701
+ for (const t of config.tasks ?? []) {
702
+ insertTask.run(
703
+ t.id,
704
+ t.projectName,
705
+ t.title,
706
+ t.description,
707
+ t.status,
708
+ t.order,
709
+ t.assignedSessionId ?? null,
710
+ t.assignedAgent ?? null,
711
+ t.agentSessionId ?? null,
712
+ t.branch ?? null,
713
+ t.useWorktree ? 1 : 0,
714
+ t.createdAt,
715
+ t.updatedAt,
716
+ t.completedAt ?? null
717
+ );
718
+ }
719
+ d.prepare("DELETE FROM workspaces").run();
720
+ const insertWorkspace = d.prepare(
721
+ `INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
722
+ );
723
+ for (const ws of config.workspaces ?? [DEFAULT_WORKSPACE]) {
724
+ insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order);
725
+ }
726
+ });
727
+ run();
728
+ }
729
+ function dbListTasks(projectName, status) {
730
+ const d = getDb();
731
+ let sql = "SELECT * FROM tasks";
732
+ const params = [];
733
+ const clauses = [];
734
+ if (projectName) {
735
+ clauses.push("project_name = ?");
736
+ params.push(projectName);
737
+ }
738
+ if (status) {
739
+ clauses.push("status = ?");
740
+ params.push(status);
741
+ }
742
+ if (clauses.length) sql += " WHERE " + clauses.join(" AND ");
743
+ sql += ' ORDER BY "order"';
744
+ const rows = d.prepare(sql).all(...params);
745
+ return rows.map(rowToTask);
746
+ }
747
+ function dbGetTask(id) {
748
+ const row = getDb().prepare("SELECT * FROM tasks WHERE id = ?").get(id);
749
+ return row ? rowToTask(row) : null;
750
+ }
751
+ function dbInsertTask(task) {
752
+ getDb().prepare(
753
+ `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)
754
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
755
+ ).run(
756
+ task.id,
757
+ task.projectName,
758
+ task.title,
759
+ task.description,
760
+ task.status,
761
+ task.order,
762
+ task.assignedSessionId ?? null,
763
+ task.assignedAgent ?? null,
764
+ task.agentSessionId ?? null,
765
+ task.branch ?? null,
766
+ task.useWorktree ? 1 : 0,
767
+ task.createdAt,
768
+ task.updatedAt,
769
+ task.completedAt ?? null
770
+ );
771
+ }
772
+ function dbUpdateTask(id, updates) {
773
+ const sets = [];
774
+ const params = [];
775
+ if (updates.title !== void 0) {
776
+ sets.push("title = ?");
777
+ params.push(updates.title);
778
+ }
779
+ if (updates.description !== void 0) {
780
+ sets.push("description = ?");
781
+ params.push(updates.description);
782
+ }
783
+ if (updates.status !== void 0) {
784
+ sets.push("status = ?");
785
+ params.push(updates.status);
786
+ }
787
+ if (updates.order !== void 0) {
788
+ sets.push('"order" = ?');
789
+ params.push(updates.order);
790
+ }
791
+ if (updates.branch !== void 0) {
792
+ sets.push("branch = ?");
793
+ params.push(updates.branch);
794
+ }
795
+ if (updates.useWorktree !== void 0) {
796
+ sets.push("use_worktree = ?");
797
+ params.push(updates.useWorktree ? 1 : 0);
798
+ }
799
+ if (updates.assignedAgent !== void 0) {
800
+ sets.push("assigned_agent = ?");
801
+ params.push(updates.assignedAgent);
802
+ }
803
+ if (updates.assignedSessionId !== void 0) {
804
+ sets.push("assigned_session_id = ?");
805
+ params.push(updates.assignedSessionId);
806
+ }
807
+ if (updates.agentSessionId !== void 0) {
808
+ sets.push("agent_session_id = ?");
809
+ params.push(updates.agentSessionId);
810
+ }
811
+ if (updates.updatedAt !== void 0) {
812
+ sets.push("updated_at = ?");
813
+ params.push(updates.updatedAt);
814
+ }
815
+ if ("completedAt" in updates) {
816
+ sets.push("completed_at = ?");
817
+ params.push(updates.completedAt ?? null);
818
+ }
819
+ if (sets.length === 0) return;
820
+ params.push(id);
821
+ getDb().prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
822
+ }
823
+ function dbDeleteTask(id) {
824
+ getDb().prepare("DELETE FROM tasks WHERE id = ?").run(id);
825
+ }
826
+ function dbGetMaxTaskOrder(projectName) {
827
+ const row = getDb().prepare('SELECT MAX("order") as m FROM tasks WHERE project_name = ?').get(projectName);
828
+ return row.m ?? -1;
829
+ }
830
+ function dbListProjects() {
831
+ const rows = getDb().prepare("SELECT * FROM projects").all();
832
+ return rows.map(rowToProject);
833
+ }
834
+ function dbGetProject(name) {
835
+ const row = getDb().prepare("SELECT * FROM projects WHERE name = ?").get(name);
836
+ return row ? rowToProject(row) : null;
837
+ }
838
+ function dbInsertProject(project) {
839
+ getDb().prepare(
840
+ "INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
841
+ ).run(
842
+ project.name,
843
+ project.path,
844
+ JSON.stringify(project.preferredAgents),
845
+ project.icon ?? null,
846
+ project.iconColor ?? null,
847
+ project.hostIds ? JSON.stringify(project.hostIds) : null,
848
+ project.workspaceId ?? "personal"
849
+ );
850
+ }
851
+ function dbUpdateProject(name, updates) {
852
+ const sets = [];
853
+ const params = [];
854
+ if (updates.path !== void 0) {
855
+ sets.push("path = ?");
856
+ params.push(updates.path);
857
+ }
858
+ if (updates.preferredAgents !== void 0) {
859
+ sets.push("preferred_agents = ?");
860
+ params.push(JSON.stringify(updates.preferredAgents));
861
+ }
862
+ if (updates.icon !== void 0) {
863
+ sets.push("icon = ?");
864
+ params.push(updates.icon);
865
+ }
866
+ if (updates.iconColor !== void 0) {
867
+ sets.push("icon_color = ?");
868
+ params.push(updates.iconColor);
869
+ }
870
+ if (updates.hostIds !== void 0) {
871
+ sets.push("host_ids = ?");
872
+ params.push(JSON.stringify(updates.hostIds));
873
+ }
874
+ if (updates.workspaceId !== void 0) {
875
+ sets.push("workspace_id = ?");
876
+ params.push(updates.workspaceId);
877
+ }
878
+ if (sets.length === 0) return;
879
+ params.push(name);
880
+ getDb().prepare(`UPDATE projects SET ${sets.join(", ")} WHERE name = ?`).run(...params);
881
+ }
882
+ function dbDeleteProject(name) {
883
+ const d = getDb();
884
+ d.transaction(() => {
885
+ d.prepare("DELETE FROM tasks WHERE project_name = ?").run(name);
886
+ d.prepare("DELETE FROM projects WHERE name = ?").run(name);
887
+ })();
888
+ }
889
+ function dbListWorkflows() {
890
+ const rows = getDb().prepare("SELECT * FROM workflows").all();
891
+ return rows.map(rowToWorkflow);
892
+ }
893
+ function dbInsertWorkflow(workflow) {
894
+ getDb().prepare(
895
+ `INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
896
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
897
+ ).run(
898
+ workflow.id,
899
+ workflow.name,
900
+ workflow.icon,
901
+ workflow.iconColor,
902
+ JSON.stringify(workflow.nodes),
903
+ JSON.stringify(workflow.edges),
904
+ workflow.enabled ? 1 : 0,
905
+ workflow.lastRunAt ?? null,
906
+ workflow.lastRunStatus ?? null,
907
+ workflow.staggerDelayMs ?? null,
908
+ workflow.workspaceId ?? "personal"
909
+ );
910
+ }
911
+ function dbUpdateWorkflow(id, updates) {
912
+ const sets = [];
913
+ const params = [];
914
+ if (updates.name !== void 0) {
915
+ sets.push("name = ?");
916
+ params.push(updates.name);
917
+ }
918
+ if (updates.nodes !== void 0) {
919
+ sets.push("nodes = ?");
920
+ params.push(JSON.stringify(updates.nodes));
921
+ }
922
+ if (updates.edges !== void 0) {
923
+ sets.push("edges = ?");
924
+ params.push(JSON.stringify(updates.edges));
925
+ }
926
+ if (updates.icon !== void 0) {
927
+ sets.push("icon = ?");
928
+ params.push(updates.icon);
929
+ }
930
+ if (updates.iconColor !== void 0) {
931
+ sets.push("icon_color = ?");
932
+ params.push(updates.iconColor);
933
+ }
934
+ if (updates.enabled !== void 0) {
935
+ sets.push("enabled = ?");
936
+ params.push(updates.enabled ? 1 : 0);
937
+ }
938
+ if (updates.staggerDelayMs !== void 0) {
939
+ sets.push("stagger_delay_ms = ?");
940
+ params.push(updates.staggerDelayMs);
941
+ }
942
+ if (updates.workspaceId !== void 0) {
943
+ sets.push("workspace_id = ?");
944
+ params.push(updates.workspaceId);
945
+ }
946
+ if (sets.length === 0) return;
947
+ params.push(id);
948
+ getDb().prepare(`UPDATE workflows SET ${sets.join(", ")} WHERE id = ?`).run(...params);
949
+ }
950
+ function dbDeleteWorkflow(id) {
951
+ getDb().prepare("DELETE FROM workflows WHERE id = ?").run(id);
952
+ }
953
+ function dbListWorkspaces() {
954
+ const rows = getDb().prepare('SELECT * FROM workspaces ORDER BY "order"').all();
955
+ return rows.map(rowToWorkspace);
956
+ }
957
+ function dbInsertWorkspace(workspace) {
958
+ getDb().prepare(`INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`).run(
959
+ workspace.id,
960
+ workspace.name,
961
+ workspace.icon ?? null,
962
+ workspace.iconColor ?? null,
963
+ workspace.order
964
+ );
965
+ }
966
+ function dbUpdateWorkspace(id, updates) {
967
+ const sets = [];
968
+ const params = [];
969
+ if (updates.name !== void 0) {
970
+ sets.push("name = ?");
971
+ params.push(updates.name);
972
+ }
973
+ if (updates.icon !== void 0) {
974
+ sets.push("icon = ?");
975
+ params.push(updates.icon);
976
+ }
977
+ if (updates.iconColor !== void 0) {
978
+ sets.push("icon_color = ?");
979
+ params.push(updates.iconColor);
980
+ }
981
+ if (updates.order !== void 0) {
982
+ sets.push('"order" = ?');
983
+ params.push(updates.order);
984
+ }
985
+ if (sets.length === 0) return;
986
+ params.push(id);
987
+ getDb().prepare(`UPDATE workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
988
+ }
989
+ function dbDeleteWorkspace(id) {
990
+ const d = getDb();
991
+ d.transaction(() => {
992
+ d.prepare("UPDATE projects SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
993
+ d.prepare("UPDATE workflows SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
994
+ d.prepare("DELETE FROM workspaces WHERE id = ?").run(id);
995
+ })();
996
+ }
997
+ function rowToTask(r) {
998
+ return {
999
+ id: r.id,
1000
+ projectName: r.project_name,
1001
+ title: r.title,
1002
+ description: r.description,
1003
+ status: r.status,
1004
+ order: r.order,
1005
+ ...r.assigned_session_id != null && { assignedSessionId: r.assigned_session_id },
1006
+ ...r.assigned_agent != null && { assignedAgent: r.assigned_agent },
1007
+ ...r.agent_session_id != null && { agentSessionId: r.agent_session_id },
1008
+ ...r.branch != null && { branch: r.branch },
1009
+ ...r.use_worktree != null && r.use_worktree !== 0 && { useWorktree: true },
1010
+ createdAt: r.created_at,
1011
+ updatedAt: r.updated_at,
1012
+ ...r.completed_at != null && { completedAt: r.completed_at }
1013
+ };
1014
+ }
1015
+ function rowToProject(r) {
1016
+ return {
1017
+ name: r.name,
1018
+ path: r.path,
1019
+ preferredAgents: JSON.parse(r.preferred_agents),
1020
+ ...r.icon != null && { icon: r.icon },
1021
+ ...r.icon_color != null && { iconColor: r.icon_color },
1022
+ ...r.host_ids != null && { hostIds: JSON.parse(r.host_ids) },
1023
+ workspaceId: r.workspace_id ?? "personal"
1024
+ };
1025
+ }
1026
+ function rowToWorkflow(r) {
1027
+ return {
1028
+ id: r.id,
1029
+ name: r.name,
1030
+ icon: r.icon,
1031
+ iconColor: r.icon_color,
1032
+ nodes: JSON.parse(r.nodes),
1033
+ edges: JSON.parse(r.edges),
1034
+ enabled: r.enabled === 1,
1035
+ ...r.last_run_at != null && { lastRunAt: r.last_run_at },
1036
+ ...r.last_run_status != null && { lastRunStatus: r.last_run_status },
1037
+ ...r.stagger_delay_ms != null && { staggerDelayMs: r.stagger_delay_ms },
1038
+ workspaceId: r.workspace_id ?? "personal"
1039
+ };
1040
+ }
1041
+ function rowToWorkspace(r) {
1042
+ return {
1043
+ id: r.id,
1044
+ name: r.name,
1045
+ ...r.icon != null && { icon: r.icon },
1046
+ ...r.icon_color != null && { iconColor: r.icon_color },
1047
+ order: r.order
1048
+ };
1049
+ }
1050
+ function listWorkflowRuns(workflowId, limit = 20) {
1051
+ const d = getDb();
1052
+ const rows = d.prepare("SELECT * FROM workflow_runs WHERE workflow_id = ? ORDER BY started_at DESC LIMIT ?").all(workflowId, limit);
1053
+ return rows.map((r) => {
1054
+ const nodeRows = d.prepare("SELECT * FROM workflow_run_nodes WHERE run_id = ?").all(r.id);
1055
+ return {
1056
+ workflowId: r.workflow_id,
1057
+ startedAt: r.started_at,
1058
+ ...r.completed_at != null && { completedAt: r.completed_at },
1059
+ status: r.status,
1060
+ ...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
1061
+ nodeStates: nodeRows.map((n) => ({
1062
+ nodeId: n.node_id,
1063
+ status: n.status,
1064
+ ...n.started_at != null && { startedAt: n.started_at },
1065
+ ...n.completed_at != null && { completedAt: n.completed_at },
1066
+ ...n.session_id != null && { sessionId: n.session_id },
1067
+ ...n.error != null && { error: n.error },
1068
+ ...n.logs != null && { logs: n.logs },
1069
+ ...n.task_id != null && { taskId: n.task_id },
1070
+ ...n.agent_session_id != null && { agentSessionId: n.agent_session_id }
1071
+ }))
1072
+ };
1073
+ });
1074
+ }
1075
+ function listWorkflowRunsByTask(taskId, limit = 20) {
1076
+ const d = getDb();
1077
+ const rows = d.prepare(
1078
+ `
1079
+ SELECT DISTINCT wr.*, w.name as workflow_name
1080
+ FROM workflow_runs wr
1081
+ LEFT JOIN workflows w ON w.id = wr.workflow_id
1082
+ WHERE wr.trigger_task_id = ?
1083
+ OR wr.id IN (SELECT run_id FROM workflow_run_nodes WHERE task_id = ?)
1084
+ ORDER BY wr.started_at DESC
1085
+ LIMIT ?
1086
+ `
1087
+ ).all(taskId, taskId, limit);
1088
+ return rows.map((r) => {
1089
+ const nodeRows = d.prepare("SELECT * FROM workflow_run_nodes WHERE run_id = ?").all(r.id);
1090
+ return {
1091
+ workflowId: r.workflow_id,
1092
+ startedAt: r.started_at,
1093
+ ...r.completed_at != null && { completedAt: r.completed_at },
1094
+ status: r.status,
1095
+ ...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
1096
+ ...r.workflow_name != null && { workflowName: r.workflow_name },
1097
+ nodeStates: nodeRows.map((n) => ({
1098
+ nodeId: n.node_id,
1099
+ status: n.status,
1100
+ ...n.started_at != null && { startedAt: n.started_at },
1101
+ ...n.completed_at != null && { completedAt: n.completed_at },
1102
+ ...n.session_id != null && { sessionId: n.session_id },
1103
+ ...n.error != null && { error: n.error },
1104
+ ...n.logs != null && { logs: n.logs },
1105
+ ...n.task_id != null && { taskId: n.task_id },
1106
+ ...n.agent_session_id != null && { agentSessionId: n.agent_session_id }
1107
+ }))
1108
+ };
1109
+ });
1110
+ }
1111
+
1112
+ // ../server/src/config-manager.ts
1113
+ var DB_DIR = path2.join(os2.homedir(), ".vorn");
1114
+ var ConfigManager = class {
1115
+ changeCallbacks = [];
1116
+ dbWatcher = null;
1117
+ debounceTimer = null;
1118
+ cachedConfig = null;
1119
+ init() {
1120
+ initDatabase();
1121
+ }
1122
+ close() {
1123
+ this.stopWatchingDb();
1124
+ closeDatabase();
1125
+ }
1126
+ loadConfig() {
1127
+ if (this.cachedConfig) return this.cachedConfig;
1128
+ try {
1129
+ const config = loadConfig();
1130
+ this.cachedConfig = config;
1131
+ return config;
1132
+ } catch (err) {
1133
+ logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
1134
+ return {
1135
+ version: 1,
1136
+ defaults: {
1137
+ shell: process.platform === "win32" ? process.env.COMSPEC || "powershell.exe" : process.env.SHELL || "/bin/zsh",
1138
+ fontSize: 13,
1139
+ theme: "dark"
1140
+ },
1141
+ projects: [],
1142
+ agentCommands: { ...DEFAULT_AGENT_COMMANDS },
1143
+ workflows: [],
1144
+ tasks: []
1145
+ };
1146
+ }
1147
+ }
1148
+ saveConfig(config) {
1149
+ try {
1150
+ saveConfig(config);
1151
+ this.cachedConfig = null;
1152
+ } catch (err) {
1153
+ logger_default.error("[config-manager] saveConfig failed:", err);
1154
+ throw err;
1155
+ }
1156
+ }
1157
+ /** Register a callback for when config changes from within the main process */
1158
+ onConfigChanged(callback) {
1159
+ this.changeCallbacks.push(callback);
1160
+ }
1161
+ /** Notify all registered callbacks (call after main-process config mutations) */
1162
+ notifyChanged() {
1163
+ this.cachedConfig = null;
1164
+ const config = this.loadConfig();
1165
+ for (const cb of this.changeCallbacks) {
1166
+ cb(config);
1167
+ }
1168
+ }
1169
+ /**
1170
+ * Watch for external DB writes (e.g. MCP stdio process).
1171
+ * Detects: .db-signal (explicit), .db-wal changes, and .db changes (post-checkpoint).
1172
+ */
1173
+ watchDb() {
1174
+ if (this.dbWatcher) return;
1175
+ const WATCH_SUFFIXES = [".db-signal", ".db-wal", ".db"];
1176
+ try {
1177
+ this.dbWatcher = fs2.watch(DB_DIR, (eventType, filename) => {
1178
+ if (!filename || !WATCH_SUFFIXES.some((s) => filename.endsWith(s))) return;
1179
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
1180
+ this.debounceTimer = setTimeout(() => {
1181
+ this.notifyChanged();
1182
+ }, 300);
1183
+ });
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ stopWatchingDb() {
1188
+ if (this.dbWatcher) {
1189
+ this.dbWatcher.close();
1190
+ this.dbWatcher = null;
1191
+ }
1192
+ if (this.debounceTimer) {
1193
+ clearTimeout(this.debounceTimer);
1194
+ this.debounceTimer = null;
1195
+ }
1196
+ }
1197
+ // No-ops -- retained for API compatibility during transition
1198
+ watchConfig(_callback) {
1199
+ }
1200
+ stopWatching() {
1201
+ }
1202
+ };
1203
+ var configManager = new ConfigManager();
1204
+
1205
+ // src/server.ts
1206
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1207
+
1208
+ // src/tools/tasks.ts
1209
+ import crypto from "crypto";
1210
+ import path3 from "path";
1211
+ import { z as z2 } from "zod";
1212
+
1213
+ // src/validation.ts
1214
+ import { z } from "zod";
1215
+ 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("\\"), {
1216
+ message: "Name must not contain path traversal characters (.. / \\)"
1217
+ });
1218
+ var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
1219
+ var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
1220
+ var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
1221
+ var safeShortText = z.string().max(200, "Value must be 200 characters or less");
1222
+ var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
1223
+ 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 /)" });
1224
+ var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
1225
+ var V = {
1226
+ name: safeName,
1227
+ id: safeId,
1228
+ title: safeTitle,
1229
+ description: safeDescription,
1230
+ shortText: safeShortText,
1231
+ prompt: safePrompt,
1232
+ absolutePath: safeAbsolutePath,
1233
+ hexColor: safeHexColor
1234
+ };
1235
+
1236
+ // src/tools/tasks.ts
1237
+ var TASK_STATUSES = ["todo", "in_progress", "in_review", "done", "cancelled"];
1238
+ var AGENT_TYPES = [
1239
+ "claude",
1240
+ "copilot",
1241
+ "codex",
1242
+ "opencode",
1243
+ "gemini"
1244
+ ];
1245
+ function registerTaskTools(server) {
1246
+ server.tool(
1247
+ "list_tasks",
1248
+ "List tasks, optionally filtered by project, status, assigned agent, or workspace",
1249
+ {
1250
+ project_name: V.name.optional().describe("Filter by project name"),
1251
+ status: z2.enum(TASK_STATUSES).optional().describe("Filter by status"),
1252
+ assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Filter by assigned agent type"),
1253
+ workspace_id: V.id.optional().describe("Filter by workspace ID (returns tasks from all projects in that workspace)")
1254
+ },
1255
+ async (args) => {
1256
+ let tasks = dbListTasks(args.project_name, args.status);
1257
+ if (args.workspace_id) {
1258
+ const projects = dbListProjects();
1259
+ const wsProjectNames = new Set(
1260
+ projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id).map((p) => p.name)
1261
+ );
1262
+ tasks = tasks.filter((t) => wsProjectNames.has(t.projectName));
1263
+ }
1264
+ if (args.assigned_agent) {
1265
+ tasks = tasks.filter((t) => t.assignedAgent === args.assigned_agent);
1266
+ }
1267
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
1268
+ }
1269
+ );
1270
+ server.tool(
1271
+ "create_task",
1272
+ "Create a new task in a project",
1273
+ {
1274
+ project_name: V.name.describe("Project name (must match existing project)"),
1275
+ title: V.title.describe("Task title"),
1276
+ description: V.description.optional().describe("Task description (markdown)"),
1277
+ status: z2.enum(TASK_STATUSES).optional().describe("Task status (default: todo)"),
1278
+ branch: V.shortText.optional().describe("Git branch for this task"),
1279
+ use_worktree: z2.boolean().optional().describe("Create a git worktree for this task"),
1280
+ assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assign to an agent type")
1281
+ },
1282
+ async (args) => {
1283
+ const project = dbGetProject(args.project_name);
1284
+ if (!project) {
1285
+ return {
1286
+ content: [{ type: "text", text: `Error: project "${args.project_name}" not found` }],
1287
+ isError: true
1288
+ };
1289
+ }
1290
+ const maxOrder = dbGetMaxTaskOrder(args.project_name);
1291
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1292
+ const status = args.status ?? "todo";
1293
+ const task = {
1294
+ id: crypto.randomUUID(),
1295
+ projectName: args.project_name,
1296
+ title: args.title,
1297
+ description: args.description ?? "",
1298
+ status,
1299
+ order: maxOrder + 1,
1300
+ createdAt: now,
1301
+ updatedAt: now,
1302
+ ...args.branch && { branch: args.branch },
1303
+ ...args.use_worktree && { useWorktree: args.use_worktree },
1304
+ ...args.assigned_agent && { assignedAgent: args.assigned_agent },
1305
+ ...(status === "done" || status === "cancelled") && { completedAt: now }
1306
+ };
1307
+ dbInsertTask(task);
1308
+ dbSignalChange();
1309
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1310
+ }
1311
+ );
1312
+ server.tool("get_task", "Get a task by ID", { id: V.id.describe("Task ID") }, async (args) => {
1313
+ const task = dbGetTask(args.id);
1314
+ if (!task) {
1315
+ return {
1316
+ content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
1317
+ isError: true
1318
+ };
1319
+ }
1320
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1321
+ });
1322
+ server.tool(
1323
+ "update_task",
1324
+ "Update a task's properties",
1325
+ {
1326
+ id: V.id.describe("Task ID"),
1327
+ title: V.title.optional().describe("New title"),
1328
+ description: V.description.optional().describe("New description"),
1329
+ status: z2.enum(TASK_STATUSES).optional().describe("New status"),
1330
+ branch: V.shortText.optional().describe("Git branch"),
1331
+ use_worktree: z2.boolean().optional().describe("Use git worktree"),
1332
+ assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assigned agent type"),
1333
+ order: z2.number().optional().describe("Queue order")
1334
+ },
1335
+ async (args) => {
1336
+ const task = dbGetTask(args.id);
1337
+ if (!task) {
1338
+ return {
1339
+ content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
1340
+ isError: true
1341
+ };
1342
+ }
1343
+ const updates = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1344
+ if (args.title !== void 0) updates.title = args.title;
1345
+ if (args.description !== void 0) updates.description = args.description;
1346
+ if (args.branch !== void 0) updates.branch = args.branch;
1347
+ if (args.use_worktree !== void 0) updates.useWorktree = args.use_worktree;
1348
+ if (args.assigned_agent !== void 0)
1349
+ updates.assignedAgent = args.assigned_agent;
1350
+ if (args.order !== void 0) updates.order = args.order;
1351
+ if (args.status !== void 0) {
1352
+ const newStatus = args.status;
1353
+ const wasDone = task.status === "done" || task.status === "cancelled";
1354
+ const isDone = newStatus === "done" || newStatus === "cancelled";
1355
+ updates.status = newStatus;
1356
+ if (isDone && !wasDone) updates.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1357
+ if (!isDone && wasDone) updates.completedAt = void 0;
1358
+ }
1359
+ dbUpdateTask(args.id, updates);
1360
+ dbSignalChange();
1361
+ const updated = dbGetTask(args.id);
1362
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
1363
+ }
1364
+ );
1365
+ server.tool(
1366
+ "delete_task",
1367
+ "Delete a task by ID",
1368
+ { id: V.id.describe("Task ID") },
1369
+ async (args) => {
1370
+ const task = dbGetTask(args.id);
1371
+ if (!task) {
1372
+ return {
1373
+ content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
1374
+ isError: true
1375
+ };
1376
+ }
1377
+ dbDeleteTask(args.id);
1378
+ dbSignalChange();
1379
+ return { content: [{ type: "text", text: `Deleted task: ${task.title}` }] };
1380
+ }
1381
+ );
1382
+ server.tool(
1383
+ "get_my_context",
1384
+ "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.",
1385
+ {
1386
+ cwd: V.absolutePath.optional().describe(
1387
+ "Your current working directory (auto-detected if omitted). Used to match against known projects and task worktrees."
1388
+ ),
1389
+ task_id: V.id.optional().describe("Specific task ID to get context for (overrides auto-detection)")
1390
+ },
1391
+ async (args) => {
1392
+ if (args.task_id) {
1393
+ const task = dbGetTask(args.task_id);
1394
+ if (!task) {
1395
+ return {
1396
+ content: [{ type: "text", text: `Error: task "${args.task_id}" not found` }],
1397
+ isError: true
1398
+ };
1399
+ }
1400
+ const project = dbGetProject(task.projectName);
1401
+ const siblingTasks = dbListTasks(task.projectName);
1402
+ return {
1403
+ content: [
1404
+ {
1405
+ type: "text",
1406
+ text: JSON.stringify(
1407
+ {
1408
+ task,
1409
+ project: project ?? void 0,
1410
+ siblingTasks: siblingTasks.filter((t) => t.id !== task.id).map((t) => ({
1411
+ id: t.id,
1412
+ title: t.title,
1413
+ status: t.status,
1414
+ branch: t.branch
1415
+ }))
1416
+ },
1417
+ null,
1418
+ 2
1419
+ )
1420
+ }
1421
+ ]
1422
+ };
1423
+ }
1424
+ const cwd = args.cwd || process.cwd();
1425
+ const normalizedCwd = path3.resolve(cwd);
1426
+ const projects = dbListProjects();
1427
+ let matchedProject = null;
1428
+ let matchLen = 0;
1429
+ for (const p of projects) {
1430
+ const normalizedPath = path3.resolve(p.path);
1431
+ if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
1432
+ matchedProject = p;
1433
+ matchLen = normalizedPath.length;
1434
+ }
1435
+ }
1436
+ if (!matchedProject) {
1437
+ return {
1438
+ content: [
1439
+ {
1440
+ type: "text",
1441
+ text: JSON.stringify(
1442
+ {
1443
+ message: "No matching project found for current directory.",
1444
+ cwd: normalizedCwd,
1445
+ hint: "Use list_projects to see available projects, or pass a task_id directly."
1446
+ },
1447
+ null,
1448
+ 2
1449
+ )
1450
+ }
1451
+ ]
1452
+ };
1453
+ }
1454
+ const projectTasks = dbListTasks(matchedProject.name);
1455
+ let matchedTask = null;
1456
+ for (const t of projectTasks) {
1457
+ if (t.worktreePath) {
1458
+ const normalizedWorktree = path3.resolve(t.worktreePath);
1459
+ if (normalizedCwd.startsWith(normalizedWorktree)) {
1460
+ matchedTask = t;
1461
+ break;
1462
+ }
1463
+ }
1464
+ }
1465
+ if (!matchedTask) {
1466
+ matchedTask = projectTasks.find((t) => t.status === "in_progress") ?? null;
1467
+ }
1468
+ const result = {
1469
+ project: {
1470
+ name: matchedProject.name,
1471
+ path: matchedProject.path,
1472
+ preferredAgents: matchedProject.preferredAgents
1473
+ },
1474
+ cwd: normalizedCwd
1475
+ };
1476
+ if (matchedTask) {
1477
+ result.task = matchedTask;
1478
+ result.siblingTasks = projectTasks.filter((t) => t.id !== matchedTask.id).map((t) => ({ id: t.id, title: t.title, status: t.status, branch: t.branch }));
1479
+ } else {
1480
+ result.message = "No specific task matched. Showing all project tasks.";
1481
+ result.tasks = projectTasks.map((t) => ({
1482
+ id: t.id,
1483
+ title: t.title,
1484
+ status: t.status,
1485
+ branch: t.branch
1486
+ }));
1487
+ }
1488
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1489
+ }
1490
+ );
1491
+ }
1492
+
1493
+ // src/tools/projects.ts
1494
+ import { z as z3 } from "zod";
1495
+ var AGENT_TYPES2 = [
1496
+ "claude",
1497
+ "copilot",
1498
+ "codex",
1499
+ "opencode",
1500
+ "gemini"
1501
+ ];
1502
+ function registerProjectTools(server) {
1503
+ server.tool(
1504
+ "list_projects",
1505
+ "List all projects, optionally filtered by workspace",
1506
+ {
1507
+ workspace_id: V.id.optional().describe('Filter by workspace ID (e.g. "personal")')
1508
+ },
1509
+ async (args) => {
1510
+ let projects = dbListProjects();
1511
+ if (args.workspace_id) {
1512
+ projects = projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id);
1513
+ }
1514
+ return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
1515
+ }
1516
+ );
1517
+ server.tool(
1518
+ "create_project",
1519
+ "Create a new project",
1520
+ {
1521
+ name: V.name.describe("Project name (unique identifier)"),
1522
+ path: V.absolutePath.describe("Absolute path to project directory"),
1523
+ preferred_agents: z3.array(z3.enum(AGENT_TYPES2)).optional().describe("Preferred agent types"),
1524
+ icon: V.shortText.optional().describe("Lucide icon name"),
1525
+ icon_color: V.hexColor.optional().describe("Hex color for icon")
1526
+ },
1527
+ async (args) => {
1528
+ if (dbGetProject(args.name)) {
1529
+ return {
1530
+ content: [{ type: "text", text: `Error: project "${args.name}" already exists` }],
1531
+ isError: true
1532
+ };
1533
+ }
1534
+ const project = {
1535
+ name: args.name,
1536
+ path: args.path,
1537
+ preferredAgents: args.preferred_agents ?? [],
1538
+ ...args.icon && { icon: args.icon },
1539
+ ...args.icon_color && { iconColor: args.icon_color }
1540
+ };
1541
+ dbInsertProject(project);
1542
+ dbSignalChange();
1543
+ return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }] };
1544
+ }
1545
+ );
1546
+ server.tool(
1547
+ "update_project",
1548
+ "Update a project's properties",
1549
+ {
1550
+ name: V.name.describe("Project name (identifier, cannot be changed)"),
1551
+ path: V.absolutePath.optional().describe("New project path"),
1552
+ preferred_agents: z3.array(z3.enum(AGENT_TYPES2)).optional().describe("Preferred agent types"),
1553
+ icon: V.shortText.optional().describe("Lucide icon name"),
1554
+ icon_color: V.hexColor.optional().describe("Hex color for icon")
1555
+ },
1556
+ async (args) => {
1557
+ if (!dbGetProject(args.name)) {
1558
+ return {
1559
+ content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
1560
+ isError: true
1561
+ };
1562
+ }
1563
+ const updates = {};
1564
+ if (args.path !== void 0) updates.path = args.path;
1565
+ if (args.preferred_agents !== void 0)
1566
+ updates.preferredAgents = args.preferred_agents;
1567
+ if (args.icon !== void 0) updates.icon = args.icon;
1568
+ if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
1569
+ dbUpdateProject(args.name, updates);
1570
+ dbSignalChange();
1571
+ const updated = dbGetProject(args.name);
1572
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
1573
+ }
1574
+ );
1575
+ server.tool(
1576
+ "delete_project",
1577
+ "Delete a project and all its tasks",
1578
+ { name: V.name.describe("Project name") },
1579
+ async (args) => {
1580
+ if (!dbGetProject(args.name)) {
1581
+ return {
1582
+ content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
1583
+ isError: true
1584
+ };
1585
+ }
1586
+ dbDeleteProject(args.name);
1587
+ dbSignalChange();
1588
+ return { content: [{ type: "text", text: `Deleted project: ${args.name}` }] };
1589
+ }
1590
+ );
1591
+ }
1592
+
1593
+ // src/tools/sessions.ts
1594
+ import { z as z4 } from "zod";
1595
+
1596
+ // src/ws-client.ts
1597
+ import fs3 from "fs";
1598
+ import path4 from "path";
1599
+ import os3 from "os";
1600
+ import { execFileSync } from "child_process";
1601
+ import { WebSocket } from "ws";
1602
+ var PORT_FILE = path4.join(os3.homedir(), ".vorn", "ws-port");
1603
+ var TIMEOUT_MS = 1e4;
1604
+ var IS_WIN = process.platform === "win32";
1605
+ var PORT_FILE_MISSING_MSG = IS_WIN ? `Vorn port file not found (~/.vorn/ws-port).
1606
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1607
+ To fix, find the Vorn process and its listening port:
1608
+ powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort"
1609
+ Then write the WS port to the file:
1610
+ echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vorn\\ws-port
1611
+ Or restart Vorn to regenerate it.` : `Vorn port file not found (~/.vorn/ws-port).
1612
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1613
+ To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
1614
+ Then write the WS port (the one on *:<port>) to the file:
1615
+ echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
1616
+ Or restart Vorn to regenerate it.`;
1617
+ var PORT_FILE_INVALID_MSG = `Vorn port file exists but contains invalid data (~/.vorn/ws-port).
1618
+ Delete it and restart Vorn, or overwrite it with the correct port:
1619
+ ${IS_WIN ? "del %USERPROFILE%\\.vorn\\ws-port" : "rm ~/.vorn/ws-port"}`;
1620
+ var rpcId = 0;
1621
+ var cachedPort = null;
1622
+ var cacheTimestamp = 0;
1623
+ var CACHE_TTL_MS = 5e3;
1624
+ var EXEC_OPTS = {
1625
+ encoding: "utf-8",
1626
+ timeout: 5e3,
1627
+ stdio: ["pipe", "pipe", "pipe"]
1628
+ };
1629
+ function discoverPort() {
1630
+ try {
1631
+ if (IS_WIN) {
1632
+ const taskOut = execFileSync(
1633
+ "tasklist",
1634
+ ["/FI", "IMAGENAME eq Vorn.exe", "/FO", "CSV", "/NH"],
1635
+ EXEC_OPTS
1636
+ );
1637
+ const pidMatch = taskOut.match(/"Vorn\.exe","(\d+)"/);
1638
+ if (!pidMatch) return null;
1639
+ const pid = pidMatch[1];
1640
+ const lines = execFileSync("netstat", ["-ano"], EXEC_OPTS).split("\n");
1641
+ let fallback = null;
1642
+ for (const line of lines) {
1643
+ if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
1644
+ const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
1645
+ if (!m) continue;
1646
+ if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
1647
+ fallback ??= parseInt(m[1], 10);
1648
+ }
1649
+ return fallback;
1650
+ } else {
1651
+ const lines = execFileSync("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
1652
+ "\n"
1653
+ );
1654
+ let fallback = null;
1655
+ for (const line of lines) {
1656
+ if (!line.includes("Vorn")) continue;
1657
+ if (line.includes("*:")) {
1658
+ const m = line.match(/\*:(\d+)/);
1659
+ if (m) return parseInt(m[1], 10);
1660
+ }
1661
+ if (!fallback) {
1662
+ const m = line.match(/:(\d+)\s/);
1663
+ if (m) fallback = parseInt(m[1], 10);
1664
+ }
1665
+ }
1666
+ return fallback;
1667
+ }
1668
+ } catch {
1669
+ }
1670
+ return null;
1671
+ }
1672
+ function discoverAndHeal() {
1673
+ const now = Date.now();
1674
+ if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1675
+ const discovered = discoverPort();
1676
+ cachedPort = discovered;
1677
+ cacheTimestamp = now;
1678
+ if (discovered) {
1679
+ try {
1680
+ fs3.mkdirSync(path4.dirname(PORT_FILE), { recursive: true });
1681
+ fs3.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
1682
+ } catch {
1683
+ }
1684
+ return { port: discovered };
1685
+ }
1686
+ return { port: null, reason: "missing" };
1687
+ }
1688
+ function readPort() {
1689
+ try {
1690
+ const raw = fs3.readFileSync(PORT_FILE, "utf-8").trim();
1691
+ if (!raw) return { port: null, reason: "invalid" };
1692
+ if (raw.startsWith("{")) {
1693
+ const parsed = JSON.parse(raw);
1694
+ const p2 = parsed?.port;
1695
+ const pid = parsed?.pid;
1696
+ if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
1697
+ return { port: null, reason: "invalid" };
1698
+ }
1699
+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
1700
+ try {
1701
+ process.kill(pid, 0);
1702
+ } catch (err) {
1703
+ if (err.code === "EPERM") return { port: p2 };
1704
+ return discoverAndHeal();
1705
+ }
1706
+ }
1707
+ return { port: p2 };
1708
+ }
1709
+ const p = parseInt(raw, 10);
1710
+ return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
1711
+ } catch {
1712
+ return discoverAndHeal();
1713
+ }
1714
+ }
1715
+ async function rpcCall(method, params) {
1716
+ const result = readPort();
1717
+ if (!result.port) {
1718
+ throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1719
+ }
1720
+ return new Promise((resolve, reject) => {
1721
+ const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
1722
+ const id = ++rpcId;
1723
+ const timer = setTimeout(() => {
1724
+ ws.close();
1725
+ reject(new Error(`RPC call "${method}" timed out after ${TIMEOUT_MS}ms`));
1726
+ }, TIMEOUT_MS);
1727
+ ws.on("open", () => {
1728
+ ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
1729
+ });
1730
+ ws.on("message", (raw) => {
1731
+ try {
1732
+ const msg = JSON.parse(raw.toString());
1733
+ if (msg.id !== id) return;
1734
+ clearTimeout(timer);
1735
+ ws.close();
1736
+ if (msg.error) {
1737
+ reject(new Error(msg.error.message));
1738
+ } else {
1739
+ resolve(msg.result);
1740
+ }
1741
+ } catch {
1742
+ }
1743
+ });
1744
+ ws.on("error", (err) => {
1745
+ clearTimeout(timer);
1746
+ reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
1747
+ });
1748
+ });
1749
+ }
1750
+ async function rpcNotify(method, params) {
1751
+ const result = readPort();
1752
+ if (!result.port) {
1753
+ throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1754
+ }
1755
+ return new Promise((resolve, reject) => {
1756
+ const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
1757
+ ws.on("open", () => {
1758
+ ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
1759
+ ws.close();
1760
+ resolve();
1761
+ });
1762
+ ws.on("error", (err) => {
1763
+ reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
1764
+ });
1765
+ });
1766
+ }
1767
+
1768
+ // src/tools/sessions.ts
1769
+ var AGENT_TYPES3 = [
1770
+ "claude",
1771
+ "copilot",
1772
+ "codex",
1773
+ "opencode",
1774
+ "gemini"
1775
+ ];
1776
+ function registerSessionTools(server) {
1777
+ server.tool(
1778
+ "list_sessions",
1779
+ 'List terminal sessions. Filter by status: "active" (running terminals), "recent" (past sessions), or "archived".',
1780
+ {
1781
+ filter: z4.enum(["active", "recent", "archived"]).optional().describe("Session filter (default: active)"),
1782
+ project_name: V.name.optional().describe("Filter by project name"),
1783
+ project_path: V.absolutePath.optional().describe("Filter by project path (for recent sessions)")
1784
+ },
1785
+ async (args) => {
1786
+ const filter = args.filter ?? "active";
1787
+ try {
1788
+ if (filter === "active") {
1789
+ let sessions = await rpcCall("terminal:listActive");
1790
+ if (args.project_name) {
1791
+ sessions = sessions.filter((s) => s.projectName === args.project_name);
1792
+ }
1793
+ const summary = sessions.map((s) => ({
1794
+ id: s.id,
1795
+ agentType: s.agentType,
1796
+ projectName: s.projectName,
1797
+ status: s.status,
1798
+ displayName: s.displayName,
1799
+ branch: s.branch,
1800
+ pid: s.pid
1801
+ }));
1802
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
1803
+ } else if (filter === "recent") {
1804
+ const sessions = await rpcCall("sessions:getRecent", args.project_path);
1805
+ return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
1806
+ } else {
1807
+ const sessions = await rpcCall("session:listArchived");
1808
+ return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
1809
+ }
1810
+ } catch (err) {
1811
+ return {
1812
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
1813
+ isError: true
1814
+ };
1815
+ }
1816
+ }
1817
+ );
1818
+ server.tool(
1819
+ "launch_session",
1820
+ "Launch an AI agent session (interactive terminal or headless). Requires the Vorn app to be running.",
1821
+ {
1822
+ agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
1823
+ project_name: V.name.describe("Project name"),
1824
+ project_path: V.absolutePath.describe("Absolute path to project directory"),
1825
+ prompt: V.prompt.optional().describe("Initial prompt to send to the agent"),
1826
+ branch: V.shortText.optional().describe("Git branch to checkout"),
1827
+ use_worktree: z4.boolean().optional().describe("Create a git worktree"),
1828
+ display_name: V.shortText.optional().describe("Display name for the session"),
1829
+ headless: z4.boolean().optional().describe("Launch as headless (no UI) session")
1830
+ },
1831
+ async (args) => {
1832
+ const payload = {
1833
+ agentType: args.agent_type,
1834
+ projectName: args.project_name,
1835
+ projectPath: args.project_path,
1836
+ ...args.prompt && { initialPrompt: args.prompt },
1837
+ ...args.branch && { branch: args.branch },
1838
+ ...args.use_worktree && { useWorktree: args.use_worktree },
1839
+ ...args.display_name && { displayName: args.display_name }
1840
+ };
1841
+ const rpcMethod = args.headless ? "headless:create" : "terminal:create";
1842
+ const label = args.headless ? "headless" : "terminal";
1843
+ try {
1844
+ const session = await rpcCall(rpcMethod, payload);
1845
+ return {
1846
+ content: [
1847
+ {
1848
+ type: "text",
1849
+ text: JSON.stringify(
1850
+ {
1851
+ id: session.id,
1852
+ agentType: session.agentType,
1853
+ projectName: session.projectName,
1854
+ pid: session.pid,
1855
+ status: session.status
1856
+ },
1857
+ null,
1858
+ 2
1859
+ )
1860
+ }
1861
+ ]
1862
+ };
1863
+ } catch (err) {
1864
+ return {
1865
+ content: [
1866
+ {
1867
+ type: "text",
1868
+ text: `Error launching ${label} agent: ${err instanceof Error ? err.message : err}`
1869
+ }
1870
+ ],
1871
+ isError: true
1872
+ };
1873
+ }
1874
+ }
1875
+ );
1876
+ server.tool(
1877
+ "kill_session",
1878
+ "Kill a terminal or headless session. Requires the Vorn app to be running.",
1879
+ {
1880
+ id: V.id.describe("Session ID to kill"),
1881
+ headless: z4.boolean().optional().describe("Kill a headless session instead of a terminal")
1882
+ },
1883
+ async (args) => {
1884
+ const rpcMethod = args.headless ? "headless:kill" : "terminal:kill";
1885
+ const label = args.headless ? "headless session" : "session";
1886
+ try {
1887
+ await rpcCall(rpcMethod, args.id);
1888
+ return { content: [{ type: "text", text: `Killed ${label}: ${args.id}` }] };
1889
+ } catch (err) {
1890
+ return {
1891
+ content: [
1892
+ {
1893
+ type: "text",
1894
+ text: `Error killing ${label}: ${err instanceof Error ? err.message : err}`
1895
+ }
1896
+ ],
1897
+ isError: true
1898
+ };
1899
+ }
1900
+ }
1901
+ );
1902
+ server.tool(
1903
+ "rename_session",
1904
+ "Rename a terminal session. Changes the display name shown in the UI.",
1905
+ {
1906
+ id: V.id.describe("Session ID"),
1907
+ display_name: V.shortText.describe("New display name")
1908
+ },
1909
+ async (args) => {
1910
+ try {
1911
+ await rpcCall("terminal:rename", { id: args.id, displayName: args.display_name });
1912
+ return {
1913
+ content: [{ type: "text", text: `Renamed session ${args.id} to "${args.display_name}"` }]
1914
+ };
1915
+ } catch (err) {
1916
+ return {
1917
+ content: [
1918
+ {
1919
+ type: "text",
1920
+ text: `Error renaming session: ${err instanceof Error ? err.message : err}`
1921
+ }
1922
+ ],
1923
+ isError: true
1924
+ };
1925
+ }
1926
+ }
1927
+ );
1928
+ server.tool(
1929
+ "reorder_sessions",
1930
+ "Reorder terminal sessions in the grid. Provide session IDs in the desired display order.",
1931
+ {
1932
+ session_ids: z4.array(V.id).min(1, "At least one session ID is required").describe("Session IDs in desired order")
1933
+ },
1934
+ async (args) => {
1935
+ try {
1936
+ await rpcCall("terminal:reorder", args.session_ids);
1937
+ return {
1938
+ content: [
1939
+ {
1940
+ type: "text",
1941
+ text: `Reordered ${args.session_ids.length} sessions`
1942
+ }
1943
+ ]
1944
+ };
1945
+ } catch (err) {
1946
+ return {
1947
+ content: [
1948
+ {
1949
+ type: "text",
1950
+ text: `Error reordering sessions: ${err instanceof Error ? err.message : err}`
1951
+ }
1952
+ ],
1953
+ isError: true
1954
+ };
1955
+ }
1956
+ }
1957
+ );
1958
+ server.tool(
1959
+ "read_session_output",
1960
+ "Read terminal output from a running session. Output is stored in a rolling 1000-line buffer with ANSI codes stripped.",
1961
+ {
1962
+ id: V.id.describe("Session ID"),
1963
+ lines: z4.number().int().min(1).max(1e3).optional().describe("Number of lines to read from the end (default: all)")
1964
+ },
1965
+ async (args) => {
1966
+ try {
1967
+ const output = await rpcCall("terminal:readOutput", {
1968
+ id: args.id,
1969
+ lines: args.lines
1970
+ });
1971
+ return {
1972
+ content: [{ type: "text", text: output.join("\n") }]
1973
+ };
1974
+ } catch (err) {
1975
+ return {
1976
+ content: [
1977
+ {
1978
+ type: "text",
1979
+ text: `Error reading session output: ${err instanceof Error ? err.message : err}`
1980
+ }
1981
+ ],
1982
+ isError: true
1983
+ };
1984
+ }
1985
+ }
1986
+ );
1987
+ server.tool(
1988
+ "write_to_terminal",
1989
+ "Send input to a running terminal session. Requires the Vorn app to be running.",
1990
+ {
1991
+ id: V.id.describe("Session ID"),
1992
+ data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)"),
1993
+ raw: z4.boolean().optional().describe("Send data as-is without appending carriage return (for raw terminal control)")
1994
+ },
1995
+ async (args) => {
1996
+ try {
1997
+ const data = args.raw ? args.data : args.data.replace(/[\r\n]+$/, "") + "\r";
1998
+ await rpcNotify("terminal:write", { id: args.id, data });
1999
+ return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
2000
+ } catch (err) {
2001
+ return {
2002
+ content: [
2003
+ {
2004
+ type: "text",
2005
+ text: `Error writing to terminal: ${err instanceof Error ? err.message : err}`
2006
+ }
2007
+ ],
2008
+ isError: true
2009
+ };
2010
+ }
2011
+ }
2012
+ );
2013
+ const KEY_MAP = {
2014
+ enter: "\r",
2015
+ escape: "\x1B",
2016
+ esc: "\x1B",
2017
+ tab: " ",
2018
+ "shift+tab": "\x1B[Z",
2019
+ up: "\x1B[A",
2020
+ down: "\x1B[B",
2021
+ left: "\x1B[D",
2022
+ right: "\x1B[C",
2023
+ backspace: "\x7F",
2024
+ delete: "\x1B[3~",
2025
+ home: "\x1B[H",
2026
+ end: "\x1B[F",
2027
+ "ctrl+c": "",
2028
+ "ctrl+d": "",
2029
+ "ctrl+x": "",
2030
+ "ctrl+z": ""
2031
+ };
2032
+ server.tool(
2033
+ "send_key",
2034
+ "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.",
2035
+ {
2036
+ id: V.id.describe("Session ID"),
2037
+ key: z4.string().min(1).max(20).describe(
2038
+ "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)"
2039
+ )
2040
+ },
2041
+ async (args) => {
2042
+ const key = args.key.toLowerCase().trim();
2043
+ let data = KEY_MAP[key];
2044
+ if (!data) {
2045
+ const ctrlMatch = key.match(/^ctrl\+([a-z])$/);
2046
+ if (ctrlMatch) {
2047
+ data = String.fromCharCode(ctrlMatch[1].toUpperCase().charCodeAt(0) - 64);
2048
+ } else if (args.key.length === 1) {
2049
+ data = args.key;
2050
+ } else {
2051
+ return {
2052
+ content: [
2053
+ {
2054
+ type: "text",
2055
+ text: `Unknown key: "${args.key}". Supported: single chars (1, y, n), named keys (${Object.keys(KEY_MAP).join(", ")}), or ctrl+<letter>.`
2056
+ }
2057
+ ],
2058
+ isError: true
2059
+ };
2060
+ }
2061
+ }
2062
+ try {
2063
+ await rpcNotify("terminal:write", { id: args.id, data });
2064
+ return {
2065
+ content: [{ type: "text", text: `Sent key "${args.key}" to session: ${args.id}` }]
2066
+ };
2067
+ } catch (err) {
2068
+ return {
2069
+ content: [
2070
+ {
2071
+ type: "text",
2072
+ text: `Error sending key to terminal: ${err instanceof Error ? err.message : err}`
2073
+ }
2074
+ ],
2075
+ isError: true
2076
+ };
2077
+ }
2078
+ }
2079
+ );
2080
+ server.tool(
2081
+ "list_session_events",
2082
+ "List session lifecycle events (created, exited, task_linked, renamed, archived, unarchived). Use for post-mortem analysis and multi-agent coordination.",
2083
+ {
2084
+ session_id: V.id.optional().describe("Filter by session ID"),
2085
+ event_type: z4.enum(["created", "exited", "task_linked", "renamed", "archived", "unarchived"]).optional().describe("Filter by event type"),
2086
+ limit: z4.number().int().min(1).max(200).optional().describe("Max events to return (default: 50)")
2087
+ },
2088
+ async (args) => {
2089
+ try {
2090
+ let events;
2091
+ if (args.session_id) {
2092
+ events = await rpcCall("sessionEvent:listBySession", {
2093
+ sessionId: args.session_id,
2094
+ limit: args.limit ?? 50
2095
+ });
2096
+ } else {
2097
+ events = await rpcCall("sessionEvent:list", {
2098
+ eventType: args.event_type,
2099
+ limit: args.limit ?? 50
2100
+ });
2101
+ }
2102
+ return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
2103
+ } catch (err) {
2104
+ return {
2105
+ content: [
2106
+ {
2107
+ type: "text",
2108
+ text: `Error listing session events: ${err instanceof Error ? err.message : err}`
2109
+ }
2110
+ ],
2111
+ isError: true
2112
+ };
2113
+ }
2114
+ }
2115
+ );
2116
+ }
2117
+
2118
+ // src/tools/workflows.ts
2119
+ import crypto2 from "crypto";
2120
+ import { z as z5 } from "zod";
2121
+ var launchAgentConfigSchema = z5.object({
2122
+ agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
2123
+ projectName: V.name,
2124
+ projectPath: V.absolutePath,
2125
+ args: z5.array(V.shortText).optional(),
2126
+ displayName: V.shortText.optional(),
2127
+ branch: V.shortText.optional(),
2128
+ useWorktree: z5.boolean().optional(),
2129
+ remoteHostId: V.id.optional(),
2130
+ prompt: V.prompt.optional(),
2131
+ promptDelayMs: z5.number().optional(),
2132
+ taskId: V.id.optional(),
2133
+ taskFromQueue: z5.boolean().optional()
2134
+ });
2135
+ var triggerConfigSchema = z5.union([
2136
+ z5.object({ triggerType: z5.literal("manual") }),
2137
+ z5.object({ triggerType: z5.literal("once"), runAt: V.shortText }),
2138
+ z5.object({
2139
+ triggerType: z5.literal("recurring"),
2140
+ cron: V.shortText,
2141
+ timezone: V.shortText.optional()
2142
+ }),
2143
+ z5.object({ triggerType: z5.literal("taskCreated"), projectFilter: V.name.optional() }),
2144
+ z5.object({
2145
+ triggerType: z5.literal("taskStatusChanged"),
2146
+ projectFilter: V.name.optional(),
2147
+ fromStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional(),
2148
+ toStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional()
2149
+ })
2150
+ ]);
2151
+ var nodeSchema = z5.object({
2152
+ id: V.id,
2153
+ type: z5.enum(["trigger", "launchAgent"]),
2154
+ label: V.shortText,
2155
+ config: z5.record(z5.unknown()),
2156
+ position: z5.object({ x: z5.number(), y: z5.number() })
2157
+ });
2158
+ var edgeSchema = z5.object({
2159
+ id: V.id,
2160
+ source: V.id,
2161
+ target: V.id
2162
+ });
2163
+ function buildGraphFromFlat(trigger, actions) {
2164
+ const nodes = [];
2165
+ const edges = [];
2166
+ const triggerNode = {
2167
+ id: crypto2.randomUUID(),
2168
+ type: "trigger",
2169
+ 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",
2170
+ config: trigger,
2171
+ position: { x: 0, y: 0 }
2172
+ };
2173
+ nodes.push(triggerNode);
2174
+ let prevId = triggerNode.id;
2175
+ const NODE_GAP = 140;
2176
+ for (let i = 0; i < actions.length; i++) {
2177
+ const action = actions[i];
2178
+ const nodeId = crypto2.randomUUID();
2179
+ nodes.push({
2180
+ id: nodeId,
2181
+ type: "launchAgent",
2182
+ label: `Launch ${action.agentType}`,
2183
+ config: action,
2184
+ position: { x: 0, y: (i + 1) * NODE_GAP }
2185
+ });
2186
+ edges.push({
2187
+ id: crypto2.randomUUID(),
2188
+ source: prevId,
2189
+ target: nodeId
2190
+ });
2191
+ prevId = nodeId;
2192
+ }
2193
+ return { nodes, edges };
2194
+ }
2195
+ function registerWorkflowTools(server) {
2196
+ server.tool(
2197
+ "list_workflows",
2198
+ "List all workflows, optionally filtered by workspace",
2199
+ {
2200
+ workspace_id: V.id.optional().describe("Filter by workspace ID")
2201
+ },
2202
+ async (args) => {
2203
+ let workflows = dbListWorkflows();
2204
+ if (args.workspace_id) {
2205
+ workflows = workflows.filter((w) => (w.workspaceId ?? "personal") === args.workspace_id);
2206
+ }
2207
+ return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
2208
+ }
2209
+ );
2210
+ server.tool(
2211
+ "create_workflow",
2212
+ "Create a new workflow. Accepts either full nodes/edges or a convenience flat format (trigger + actions array).",
2213
+ {
2214
+ name: V.title.describe("Workflow name"),
2215
+ trigger: triggerConfigSchema.optional().describe("Trigger config (convenience mode). Defaults to manual."),
2216
+ actions: z5.array(launchAgentConfigSchema).optional().describe("Actions to execute (convenience mode). Auto-generates graph."),
2217
+ nodes: z5.array(nodeSchema).optional().describe("Full graph nodes (advanced mode)"),
2218
+ edges: z5.array(edgeSchema).optional().describe("Full graph edges (advanced mode)"),
2219
+ icon: V.shortText.optional().describe("Lucide icon name (default: zap)"),
2220
+ icon_color: V.hexColor.optional().describe("Hex color (default: #6366f1)"),
2221
+ enabled: z5.boolean().optional().describe("Whether workflow is enabled (default: true)"),
2222
+ stagger_delay_ms: z5.number().optional().describe("Delay in ms between actions")
2223
+ },
2224
+ async (args) => {
2225
+ let nodes;
2226
+ let edges;
2227
+ if (args.nodes && args.edges) {
2228
+ nodes = args.nodes;
2229
+ edges = args.edges;
2230
+ } else {
2231
+ const trigger = args.trigger ?? { triggerType: "manual" };
2232
+ const actions = args.actions ?? [];
2233
+ const graph = buildGraphFromFlat(trigger, actions);
2234
+ nodes = graph.nodes;
2235
+ edges = graph.edges;
2236
+ }
2237
+ const workflow = {
2238
+ id: crypto2.randomUUID(),
2239
+ name: args.name,
2240
+ icon: args.icon ?? "Zap",
2241
+ iconColor: args.icon_color ?? "#6366f1",
2242
+ nodes,
2243
+ edges,
2244
+ enabled: args.enabled ?? true,
2245
+ ...args.stagger_delay_ms && { staggerDelayMs: args.stagger_delay_ms }
2246
+ };
2247
+ dbInsertWorkflow(workflow);
2248
+ dbSignalChange();
2249
+ return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
2250
+ }
2251
+ );
2252
+ server.tool(
2253
+ "update_workflow",
2254
+ "Update a workflow's properties",
2255
+ {
2256
+ id: V.id.describe("Workflow ID"),
2257
+ name: V.title.optional(),
2258
+ nodes: z5.array(nodeSchema).optional(),
2259
+ edges: z5.array(edgeSchema).optional(),
2260
+ icon: V.shortText.optional(),
2261
+ icon_color: V.hexColor.optional(),
2262
+ enabled: z5.boolean().optional(),
2263
+ stagger_delay_ms: z5.number().optional()
2264
+ },
2265
+ async (args) => {
2266
+ const workflows = dbListWorkflows();
2267
+ const workflow = workflows.find((w) => w.id === args.id);
2268
+ if (!workflow) {
2269
+ return {
2270
+ content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
2271
+ isError: true
2272
+ };
2273
+ }
2274
+ const updates = {};
2275
+ if (args.name !== void 0) updates.name = args.name;
2276
+ if (args.nodes !== void 0) updates.nodes = args.nodes;
2277
+ if (args.edges !== void 0) updates.edges = args.edges;
2278
+ if (args.icon !== void 0) updates.icon = args.icon;
2279
+ if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
2280
+ if (args.enabled !== void 0) updates.enabled = args.enabled;
2281
+ if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
2282
+ dbUpdateWorkflow(args.id, updates);
2283
+ dbSignalChange();
2284
+ return {
2285
+ content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
2286
+ };
2287
+ }
2288
+ );
2289
+ server.tool(
2290
+ "delete_workflow",
2291
+ "Delete a workflow",
2292
+ { id: V.id.describe("Workflow ID") },
2293
+ async (args) => {
2294
+ const workflows = dbListWorkflows();
2295
+ const workflow = workflows.find((w) => w.id === args.id);
2296
+ if (!workflow) {
2297
+ return {
2298
+ content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
2299
+ isError: true
2300
+ };
2301
+ }
2302
+ dbDeleteWorkflow(args.id);
2303
+ dbSignalChange();
2304
+ return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
2305
+ }
2306
+ );
2307
+ server.tool(
2308
+ "list_workflow_runs",
2309
+ "List workflow execution history. Filter by workflow_id or task_id.",
2310
+ {
2311
+ workflow_id: V.id.optional().describe("Filter by workflow ID"),
2312
+ task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
2313
+ limit: z5.number().int().min(1).max(100).optional().describe("Max results (default: 20)")
2314
+ },
2315
+ async (args) => {
2316
+ if (args.workflow_id && args.task_id) {
2317
+ return {
2318
+ content: [{ type: "text", text: "Error: provide workflow_id or task_id, not both" }],
2319
+ isError: true
2320
+ };
2321
+ }
2322
+ if (args.task_id) {
2323
+ const runs = listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
2324
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
2325
+ }
2326
+ if (args.workflow_id) {
2327
+ const runs = listWorkflowRuns(args.workflow_id, args.limit ?? 20);
2328
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
2329
+ }
2330
+ return {
2331
+ content: [{ type: "text", text: "Error: provide either workflow_id or task_id" }],
2332
+ isError: true
2333
+ };
2334
+ }
2335
+ );
2336
+ server.tool(
2337
+ "get_workflow_schedule",
2338
+ "Get scheduler info for a workflow: execution log or next scheduled run. Requires the Vorn app to be running.",
2339
+ {
2340
+ workflow_id: V.id.optional().describe("Workflow ID (required for next_run, optional for log)"),
2341
+ info: z5.enum(["log", "next_run"]).optional().describe("What to retrieve (default: log)")
2342
+ },
2343
+ async (args) => {
2344
+ const info = args.info ?? "log";
2345
+ try {
2346
+ if (info === "next_run") {
2347
+ if (!args.workflow_id) {
2348
+ return {
2349
+ content: [{ type: "text", text: "Error: workflow_id is required for next_run" }],
2350
+ isError: true
2351
+ };
2352
+ }
2353
+ const nextRun = await rpcCall("scheduler:getNextRun", args.workflow_id);
2354
+ return {
2355
+ content: [
2356
+ {
2357
+ type: "text",
2358
+ text: nextRun ? JSON.stringify({ nextRun }, null, 2) : "No scheduled run (workflow may be manual or disabled)"
2359
+ }
2360
+ ]
2361
+ };
2362
+ } else {
2363
+ const log2 = await rpcCall("scheduler:getLog", args.workflow_id);
2364
+ return { content: [{ type: "text", text: JSON.stringify(log2, null, 2) }] };
2365
+ }
2366
+ } catch (err) {
2367
+ return {
2368
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
2369
+ isError: true
2370
+ };
2371
+ }
2372
+ }
2373
+ );
2374
+ }
2375
+
2376
+ // src/tools/config.ts
2377
+ function registerConfigTools(server) {
2378
+ server.tool(
2379
+ "get_config",
2380
+ "Get the full Vorn configuration (projects, tasks, workflows, settings)",
2381
+ async () => {
2382
+ const config = configManager.loadConfig();
2383
+ return { content: [{ type: "text", text: JSON.stringify(config, null, 2) }] };
2384
+ }
2385
+ );
2386
+ }
2387
+
2388
+ // src/tools/workspaces.ts
2389
+ import crypto3 from "crypto";
2390
+ import { z as z6 } from "zod";
2391
+ function registerWorkspaceTools(server) {
2392
+ server.tool("list_workspaces", "List all workspaces", async () => {
2393
+ const workspaces = dbListWorkspaces();
2394
+ return { content: [{ type: "text", text: JSON.stringify(workspaces, null, 2) }] };
2395
+ });
2396
+ server.tool(
2397
+ "create_workspace",
2398
+ "Create a new workspace for organizing projects",
2399
+ {
2400
+ name: V.name.describe("Workspace name"),
2401
+ icon: V.shortText.optional().describe("Lucide icon name"),
2402
+ icon_color: V.hexColor.optional().describe("Hex color for icon")
2403
+ },
2404
+ async (args) => {
2405
+ const existing = dbListWorkspaces();
2406
+ const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
2407
+ const workspace = {
2408
+ id: crypto3.randomUUID(),
2409
+ name: args.name,
2410
+ order: maxOrder + 1,
2411
+ ...args.icon && { icon: args.icon },
2412
+ ...args.icon_color && { iconColor: args.icon_color }
2413
+ };
2414
+ dbInsertWorkspace(workspace);
2415
+ dbSignalChange();
2416
+ return { content: [{ type: "text", text: JSON.stringify(workspace, null, 2) }] };
2417
+ }
2418
+ );
2419
+ server.tool(
2420
+ "update_workspace",
2421
+ "Update a workspace's properties",
2422
+ {
2423
+ id: V.id.describe("Workspace ID"),
2424
+ name: V.name.optional().describe("New name"),
2425
+ icon: V.shortText.optional().describe("Lucide icon name"),
2426
+ icon_color: V.hexColor.optional().describe("Hex color for icon"),
2427
+ order: z6.number().int().min(0).optional().describe("Sort order")
2428
+ },
2429
+ async (args) => {
2430
+ const existing = dbListWorkspaces();
2431
+ if (!existing.find((w) => w.id === args.id)) {
2432
+ return {
2433
+ content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
2434
+ isError: true
2435
+ };
2436
+ }
2437
+ const updates = {};
2438
+ if (args.name !== void 0) updates.name = args.name;
2439
+ if (args.icon !== void 0) updates.icon = args.icon;
2440
+ if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
2441
+ if (args.order !== void 0) updates.order = args.order;
2442
+ dbUpdateWorkspace(args.id, updates);
2443
+ dbSignalChange();
2444
+ const updated = dbListWorkspaces().find((w) => w.id === args.id);
2445
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
2446
+ }
2447
+ );
2448
+ server.tool(
2449
+ "delete_workspace",
2450
+ "Delete a workspace",
2451
+ { id: V.id.describe("Workspace ID") },
2452
+ async (args) => {
2453
+ if (args.id === "personal") {
2454
+ return {
2455
+ content: [{ type: "text", text: "Error: cannot delete the default workspace" }],
2456
+ isError: true
2457
+ };
2458
+ }
2459
+ const existing = dbListWorkspaces();
2460
+ const workspace = existing.find((w) => w.id === args.id);
2461
+ if (!workspace) {
2462
+ return {
2463
+ content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
2464
+ isError: true
2465
+ };
2466
+ }
2467
+ dbDeleteWorkspace(args.id);
2468
+ dbSignalChange();
2469
+ return { content: [{ type: "text", text: `Deleted workspace: ${workspace.name}` }] };
2470
+ }
2471
+ );
2472
+ }
2473
+
2474
+ // src/server.ts
2475
+ function createMcpServer(version) {
2476
+ const server = new McpServer({ name: "vorn", version }, { capabilities: { tools: {} } });
2477
+ registerConfigTools(server);
2478
+ registerProjectTools(server);
2479
+ registerTaskTools(server);
2480
+ registerSessionTools(server);
2481
+ registerWorkflowTools(server);
2482
+ registerWorkspaceTools(server);
2483
+ return server;
2484
+ }
2485
+
2486
+ // src/index.ts
2487
+ var _origError = console.error;
2488
+ console.log = (...args) => _origError("[mcp]", ...args);
2489
+ console.info = (...args) => _origError("[mcp]", ...args);
2490
+ console.debug = (...args) => _origError("[mcp:debug]", ...args);
2491
+ console.warn = (...args) => _origError("[mcp:warn]", ...args);
2492
+ console.error = (...args) => _origError("[mcp:error]", ...args);
2493
+ async function main() {
2494
+ configManager.init();
2495
+ const version = true ? "0.1.0" : createRequire(import.meta.url)("../package.json").version;
2496
+ const server = createMcpServer(version);
2497
+ const transport = new StdioServerTransport();
2498
+ await server.connect(transport);
2499
+ transport.onclose = () => {
2500
+ configManager.close();
2501
+ process.exit(0);
2502
+ };
2503
+ }
2504
+ main().catch((err) => {
2505
+ console.error("Failed to start MCP server:", err);
2506
+ process.exit(1);
2507
+ });