@vornrun/mcp 0.6.0 → 0.6.1-beta.1

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 +688 -735
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -6,8 +6,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
 
7
7
  // ../server/src/config-manager.ts
8
8
  import fs3 from "fs";
9
- import path3 from "path";
10
- import os2 from "os";
11
9
 
12
10
  // ../shared/src/agent-defaults.ts
13
11
  var DEFAULT_AGENT_COMMANDS = {
@@ -50,6 +48,11 @@ var logger_default = log;
50
48
  import { execFileSync, execFile } from "child_process";
51
49
  import fs from "fs";
52
50
  import path from "path";
51
+
52
+ // ../shared/src/protocol.ts
53
+ var LOCAL_TOKEN_FILENAME = "local-token";
54
+
55
+ // ../server/src/process-utils.ts
53
56
  function getDefaultShell(configured) {
54
57
  const chosen = configured?.trim();
55
58
  if (chosen) return chosen;
@@ -92,9 +95,9 @@ var SDK_FILTER_KEYS = {
92
95
  version: "sdkVersion",
93
96
  icon: "sdkIcon"
94
97
  };
95
- function connectionConnectorId(connection) {
96
- const packaged = connection.filters?.[SDK_FILTER_KEYS.connectorId];
97
- return typeof packaged === "string" && packaged !== "" ? packaged : connection.connectorId;
98
+ function connectionConnectorId(connection2) {
99
+ const packaged = connection2.filters?.[SDK_FILTER_KEYS.connectorId];
100
+ return typeof packaged === "string" && packaged !== "" ? packaged : connection2.connectorId;
98
101
  }
99
102
 
100
103
  // ../server/src/default-workflows.ts
@@ -140,25 +143,35 @@ function buildDefaultTaskWorkflow() {
140
143
  }
141
144
 
142
145
  // ../server/src/database.ts
143
- var CONFIG_DIR = path2.join(os.homedir(), ".vorn");
144
- var DB_PATH = path2.join(CONFIG_DIR, "vorn.db");
146
+ var DEFAULT_DATA_DIR = path2.join(os.homedir(), ".vorn");
147
+ var resolvedDataDir = null;
148
+ function dbPath() {
149
+ return path2.join(getDataDir(), "vorn.db");
150
+ }
145
151
  var db = null;
146
152
  function getDb() {
147
153
  if (!db) throw new Error("Database not initialized. Call initDatabase() first.");
148
154
  return db;
149
155
  }
150
- function initDatabase() {
151
- if (!fs2.existsSync(CONFIG_DIR)) {
152
- fs2.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
156
+ function getDataDir() {
157
+ if (!resolvedDataDir) {
158
+ throw new Error("Data directory not resolved. Call initDatabase() first.");
159
+ }
160
+ return resolvedDataDir;
161
+ }
162
+ function initDatabase(dataDir) {
163
+ resolvedDataDir = dataDir ?? DEFAULT_DATA_DIR;
164
+ if (!fs2.existsSync(getDataDir())) {
165
+ fs2.mkdirSync(getDataDir(), { recursive: true, mode: 448 });
153
166
  }
154
167
  try {
155
- db = new Database(DB_PATH);
168
+ db = new Database(dbPath());
156
169
  db.pragma("journal_mode = WAL");
157
170
  db.pragma("foreign_keys = ON");
158
171
  createSchema();
159
172
  seedSystemDefaults();
160
173
  } catch (err) {
161
- logger_default.error("[database] Failed to open database:", err);
174
+ logger_default.error({ err }, "[database] Failed to open database:");
162
175
  const message = err instanceof Error ? err.message : String(err);
163
176
  const isCorrupt = /corrupt|notadb|malformed|not a database|file is not a database/i.test(
164
177
  message
@@ -212,39 +225,32 @@ function recoverCorruptDatabase() {
212
225
  }
213
226
  db = null;
214
227
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
215
- const backupPath = `${DB_PATH}.corrupt-${timestamp}`;
228
+ const backupPath = `${dbPath()}.corrupt-${timestamp}`;
216
229
  try {
217
- if (fs2.existsSync(DB_PATH)) {
218
- fs2.copyFileSync(DB_PATH, backupPath);
230
+ if (fs2.existsSync(dbPath())) {
231
+ fs2.copyFileSync(dbPath(), backupPath);
219
232
  logger_default.info(`[database] Backed up corrupt database to ${backupPath}`);
220
233
  }
221
234
  for (const suffix of ["", "-wal", "-shm"]) {
222
- const file = DB_PATH + suffix;
235
+ const file = dbPath() + suffix;
223
236
  if (fs2.existsSync(file)) fs2.unlinkSync(file);
224
237
  }
225
238
  } catch (backupErr) {
226
- logger_default.error("[database] Failed to back up corrupt database:", backupErr);
239
+ logger_default.error({ backupErr }, "[database] Failed to back up corrupt database:");
227
240
  }
228
241
  try {
229
- db = new Database(DB_PATH);
242
+ db = new Database(dbPath());
230
243
  db.pragma("journal_mode = WAL");
231
244
  db.pragma("foreign_keys = ON");
232
245
  createSchema();
233
246
  seedSystemDefaults();
234
247
  logger_default.info("[database] Successfully created fresh database after corruption recovery");
235
248
  } catch (freshErr) {
236
- logger_default.error("[database] Failed to create fresh database after corruption:", freshErr);
249
+ logger_default.error({ freshErr }, "[database] Failed to create fresh database after corruption:");
237
250
  throw freshErr;
238
251
  }
239
252
  logger_default.warn(`[database] Database was corrupted and has been reset. Backup saved to: ${backupPath}`);
240
253
  }
241
- function dbSignalChange() {
242
- try {
243
- const signalPath = path2.join(CONFIG_DIR, ".db-signal");
244
- fs2.writeFileSync(signalPath, Date.now().toString());
245
- } catch {
246
- }
247
- }
248
254
  function closeDatabase() {
249
255
  if (db) {
250
256
  db.close();
@@ -269,6 +275,29 @@ function createSchema() {
269
275
  value TEXT NOT NULL
270
276
  );
271
277
 
278
+ CREATE TABLE IF NOT EXISTS users (
279
+ id TEXT PRIMARY KEY,
280
+ name TEXT NOT NULL,
281
+ role TEXT NOT NULL DEFAULT 'owner',
282
+ created_at TEXT NOT NULL
283
+ );
284
+
285
+ -- Only the hash is stored. The plaintext is shown once at creation and is
286
+ -- not recoverable afterwards, so a leaked database yields no usable
287
+ -- credential.
288
+ CREATE TABLE IF NOT EXISTS device_tokens (
289
+ id TEXT PRIMARY KEY,
290
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
291
+ name TEXT NOT NULL,
292
+ token_hash TEXT NOT NULL,
293
+ created_at TEXT NOT NULL,
294
+ last_seen_at TEXT,
295
+ revoked_at TEXT
296
+ );
297
+
298
+ CREATE INDEX IF NOT EXISTS idx_device_tokens_user
299
+ ON device_tokens(user_id);
300
+
272
301
  CREATE TABLE IF NOT EXISTS projects (
273
302
  name TEXT PRIMARY KEY,
274
303
  path TEXT NOT NULL,
@@ -496,9 +525,9 @@ function seedLegacyConnectorPollState(d) {
496
525
  );
497
526
  const connectionId = trigger?.config?.connectionId;
498
527
  if (!connectionId) continue;
499
- const connection = readConnection.get(connectionId);
500
- if (!connection) continue;
501
- insertState.run(workflow.id, connectionId, connection.sync_cursor, connection.last_sync_at);
528
+ const connection2 = readConnection.get(connectionId);
529
+ if (!connection2) continue;
530
+ insertState.run(workflow.id, connectionId, connection2.sync_cursor, connection2.last_sync_at);
502
531
  }
503
532
  }
504
533
  function migrateSchema(d) {
@@ -821,6 +850,57 @@ function migrateSchema(d) {
821
850
  })();
822
851
  logger_default.info("[database] migrated schema to version 13 (connection-scoped inbox)");
823
852
  }
853
+ if (version < 14) {
854
+ d.transaction(() => {
855
+ const existing = d.prepare("SELECT COUNT(*) AS n FROM users").get();
856
+ if (existing.n === 0) {
857
+ let name = "owner";
858
+ try {
859
+ name = os.userInfo().username || name;
860
+ } catch {
861
+ }
862
+ d.prepare("INSERT INTO users (id, name, role, created_at) VALUES (?, ?, 'owner', ?)").run(
863
+ randomUUID(),
864
+ name,
865
+ (/* @__PURE__ */ new Date()).toISOString()
866
+ );
867
+ }
868
+ d.prepare(
869
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '14')"
870
+ ).run();
871
+ })();
872
+ logger_default.info("[database] migrated schema to version 14 (identity and device tokens)");
873
+ }
874
+ if (version < 15) {
875
+ d.transaction(() => {
876
+ for (const table of REVISIONED_TABLES) {
877
+ const columns = d.prepare(`PRAGMA table_info(${table})`).all();
878
+ if (columns.some((c) => c.name === "row_revision")) continue;
879
+ d.exec(`ALTER TABLE ${table} ADD COLUMN row_revision INTEGER NOT NULL DEFAULT 0`);
880
+ }
881
+ d.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)").run(
882
+ CONFIG_REVISION_KEY,
883
+ "0"
884
+ );
885
+ d.prepare(
886
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '15')"
887
+ ).run();
888
+ })();
889
+ logger_default.info("[database] migrated schema to version 15 (config row revisions)");
890
+ }
891
+ }
892
+ var REVISIONED_TABLES = [
893
+ "projects",
894
+ "tasks",
895
+ "workspaces",
896
+ "remote_hosts",
897
+ "agent_commands"
898
+ ];
899
+ var CONFIG_REVISION_KEY = "config_revision";
900
+ function readConfigRevision(d) {
901
+ const row = d.prepare("SELECT value FROM schema_meta WHERE key = ?").get(CONFIG_REVISION_KEY);
902
+ const parsed = Number(row?.value);
903
+ return Number.isFinite(parsed) ? parsed : 0;
824
904
  }
825
905
  function verifySchema(d) {
826
906
  const expectedByTable = {
@@ -930,6 +1010,15 @@ function verifySchema(d) {
930
1010
  }
931
1011
  ]
932
1012
  };
1013
+ for (const table of REVISIONED_TABLES) {
1014
+ expectedByTable[table] = [
1015
+ ...expectedByTable[table] ?? [],
1016
+ {
1017
+ column: "row_revision",
1018
+ ddl: `ALTER TABLE ${table} ADD COLUMN row_revision INTEGER NOT NULL DEFAULT 0`
1019
+ }
1020
+ ];
1021
+ }
933
1022
  for (const [table, columns] of Object.entries(expectedByTable)) {
934
1023
  const existing = new Set(
935
1024
  d.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name)
@@ -940,7 +1029,7 @@ function verifySchema(d) {
940
1029
  d.exec(ddl);
941
1030
  logger_default.warn(`[database] self-heal: added missing column ${table}.${column}`);
942
1031
  } catch (err) {
943
- logger_default.error(`[database] self-heal: failed to add ${table}.${column}:`, err);
1032
+ logger_default.error({ err }, `[database] self-heal: failed to add ${table}.${column}:`);
944
1033
  }
945
1034
  }
946
1035
  }
@@ -956,6 +1045,7 @@ function loadConfig() {
956
1045
  const workspaces = loadWorkspaces(d);
957
1046
  return {
958
1047
  version: 1,
1048
+ revision: readConfigRevision(d),
959
1049
  defaults,
960
1050
  projects,
961
1051
  agentCommands: Object.keys(agentCommands).length > 0 ? agentCommands : { ...DEFAULT_AGENT_COMMANDS },
@@ -1027,6 +1117,7 @@ function loadDefaults(d) {
1027
1117
  ...map.mobileAccessEnabled !== void 0 && {
1028
1118
  mobileAccessEnabled: map.mobileAccessEnabled
1029
1119
  },
1120
+ ...map.serverPort !== void 0 && { serverPort: map.serverPort },
1030
1121
  ...map.networkAccessEnabled !== void 0 && {
1031
1122
  networkAccessEnabled: map.networkAccessEnabled
1032
1123
  },
@@ -1038,6 +1129,23 @@ function loadDefaults(d) {
1038
1129
  },
1039
1130
  ...map.hasSeededDefaultTaskWorkflow !== void 0 && {
1040
1131
  hasSeededDefaultTaskWorkflow: map.hasSeededDefaultTaskWorkflow
1132
+ },
1133
+ // The four below were declared in AppConfig and consumed, but never listed
1134
+ // here — exactly the failure the comment above describes. Each was written on
1135
+ // save and dropped on the next load, so the setting appeared to work until a
1136
+ // reload. `worktreeRetention` is the worst of them: it is read server-side
1137
+ // (register-methods.ts) and so always resolved to undefined.
1138
+ ...map.updateAutoDownload !== void 0 && {
1139
+ updateAutoDownload: map.updateAutoDownload
1140
+ },
1141
+ ...map.headlessStepTimeoutMinutes !== void 0 && {
1142
+ headlessStepTimeoutMinutes: map.headlessStepTimeoutMinutes
1143
+ },
1144
+ ...map.enableHoverPreview !== void 0 && {
1145
+ enableHoverPreview: map.enableHoverPreview
1146
+ },
1147
+ ...map.worktreeRetention !== void 0 && {
1148
+ worktreeRetention: map.worktreeRetention
1041
1149
  }
1042
1150
  };
1043
1151
  }
@@ -1086,19 +1194,48 @@ function loadWorkspaces(d) {
1086
1194
  const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
1087
1195
  return rows.map(rowToWorkspace);
1088
1196
  }
1197
+ function pruneMissing(d, table, keyColumn, keep, baseRevision) {
1198
+ const wanted = new Set(keep.filter((k) => typeof k === "string"));
1199
+ const existing = d.prepare(`SELECT ${keyColumn} AS key, row_revision AS revision FROM ${table}`).all();
1200
+ const remove = d.prepare(`DELETE FROM ${table} WHERE ${keyColumn} = ?`);
1201
+ for (const { key, revision } of existing) {
1202
+ if (wanted.has(key)) continue;
1203
+ if (revision > baseRevision) continue;
1204
+ remove.run(key);
1205
+ }
1206
+ }
1089
1207
  function saveConfig(config) {
1090
1208
  const d = getDb();
1091
1209
  const run = d.transaction(() => {
1092
- d.prepare("DELETE FROM defaults").run();
1093
- const insertDefault = d.prepare("INSERT INTO defaults (key, value) VALUES (?, ?)");
1210
+ const baseRevision = config.revision ?? Number.MAX_SAFE_INTEGER;
1211
+ const revision = readConfigRevision(d) + 1;
1212
+ const upsertDefault = d.prepare(
1213
+ `INSERT INTO defaults (key, value) VALUES (?, ?)
1214
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
1215
+ );
1216
+ const deleteDefault = d.prepare("DELETE FROM defaults WHERE key = ?");
1094
1217
  for (const [key, value] of Object.entries(config.defaults)) {
1095
- if (value !== void 0) {
1096
- insertDefault.run(key, JSON.stringify(value));
1097
- }
1218
+ if (value === void 0) deleteDefault.run(key);
1219
+ else upsertDefault.run(key, JSON.stringify(value));
1098
1220
  }
1099
- d.prepare("DELETE FROM projects").run();
1221
+ pruneMissing(
1222
+ d,
1223
+ "projects",
1224
+ "name",
1225
+ config.projects.map((p) => p.name),
1226
+ baseRevision
1227
+ );
1100
1228
  const insertProject = d.prepare(
1101
- "INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
1229
+ `INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id, row_revision)
1230
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1231
+ ON CONFLICT(name) DO UPDATE SET
1232
+ row_revision = excluded.row_revision,
1233
+ path = excluded.path,
1234
+ preferred_agents = excluded.preferred_agents,
1235
+ icon = excluded.icon,
1236
+ icon_color = excluded.icon_color,
1237
+ host_ids = excluded.host_ids,
1238
+ workspace_id = excluded.workspace_id`
1102
1239
  );
1103
1240
  for (const p of config.projects) {
1104
1241
  insertProject.run(
@@ -1108,7 +1245,8 @@ function saveConfig(config) {
1108
1245
  p.icon ?? null,
1109
1246
  p.iconColor ?? null,
1110
1247
  p.hostIds ? JSON.stringify(p.hostIds) : null,
1111
- p.workspaceId ?? "personal"
1248
+ p.workspaceId ?? "personal",
1249
+ revision
1112
1250
  );
1113
1251
  }
1114
1252
  const workflows = config.workflows ?? [];
@@ -1148,9 +1286,23 @@ function saveConfig(config) {
1148
1286
  w.workspaceId ?? "personal"
1149
1287
  );
1150
1288
  }
1151
- d.prepare("DELETE FROM agent_commands").run();
1289
+ pruneMissing(
1290
+ d,
1291
+ "agent_commands",
1292
+ "agent_type",
1293
+ Object.keys(config.agentCommands ?? {}),
1294
+ baseRevision
1295
+ );
1152
1296
  const insertAgent = d.prepare(
1153
- "INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?, ?)"
1297
+ `INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args, row_revision)
1298
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1299
+ ON CONFLICT(agent_type) DO UPDATE SET
1300
+ row_revision = excluded.row_revision,
1301
+ command = excluded.command,
1302
+ args = excluded.args,
1303
+ headless_args = excluded.headless_args,
1304
+ fallback_command = excluded.fallback_command,
1305
+ fallback_args = excluded.fallback_args`
1154
1306
  );
1155
1307
  if (config.agentCommands) {
1156
1308
  for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
@@ -1161,14 +1313,33 @@ function saveConfig(config) {
1161
1313
  JSON.stringify(cmd.args),
1162
1314
  cmd.headlessArgs ? JSON.stringify(cmd.headlessArgs) : null,
1163
1315
  cmd.fallbackCommand ?? null,
1164
- cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
1316
+ cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null,
1317
+ revision
1165
1318
  );
1166
1319
  }
1167
1320
  }
1168
1321
  }
1169
- d.prepare("DELETE FROM remote_hosts").run();
1322
+ pruneMissing(
1323
+ d,
1324
+ "remote_hosts",
1325
+ "id",
1326
+ (config.remoteHosts ?? []).map((h) => h.id),
1327
+ baseRevision
1328
+ );
1170
1329
  const insertHost = d.prepare(
1171
- "INSERT INTO remote_hosts (id, label, hostname, user, port, auth_method, ssh_key_path, credential_id, encrypted_password, ssh_options) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
1330
+ `INSERT INTO remote_hosts (id, label, hostname, user, port, auth_method, ssh_key_path, credential_id, encrypted_password, ssh_options, row_revision)
1331
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1332
+ ON CONFLICT(id) DO UPDATE SET
1333
+ row_revision = excluded.row_revision,
1334
+ label = excluded.label,
1335
+ hostname = excluded.hostname,
1336
+ user = excluded.user,
1337
+ port = excluded.port,
1338
+ auth_method = excluded.auth_method,
1339
+ ssh_key_path = excluded.ssh_key_path,
1340
+ credential_id = excluded.credential_id,
1341
+ encrypted_password = excluded.encrypted_password,
1342
+ ssh_options = excluded.ssh_options`
1172
1343
  );
1173
1344
  for (const h of config.remoteHosts ?? []) {
1174
1345
  insertHost.run(
@@ -1181,13 +1352,39 @@ function saveConfig(config) {
1181
1352
  h.sshKeyPath ?? null,
1182
1353
  h.credentialId ?? null,
1183
1354
  h.encryptedPassword ?? null,
1184
- h.sshOptions ?? null
1355
+ h.sshOptions ?? null,
1356
+ revision
1185
1357
  );
1186
1358
  }
1187
- d.prepare("DELETE FROM tasks").run();
1359
+ pruneMissing(
1360
+ d,
1361
+ "tasks",
1362
+ "id",
1363
+ (config.tasks ?? []).map((t) => t.id),
1364
+ baseRevision
1365
+ );
1188
1366
  const insertTask = d.prepare(
1189
- `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, archived_at, source_connector_id, source_external_url, source_external_id)
1190
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1367
+ `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, archived_at, source_connector_id, source_external_url, source_external_id, row_revision)
1368
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1369
+ ON CONFLICT(id) DO UPDATE SET
1370
+ row_revision = excluded.row_revision,
1371
+ project_name = excluded.project_name,
1372
+ title = excluded.title,
1373
+ description = excluded.description,
1374
+ status = excluded.status,
1375
+ "order" = excluded."order",
1376
+ assigned_session_id = excluded.assigned_session_id,
1377
+ assigned_agent = excluded.assigned_agent,
1378
+ agent_session_id = excluded.agent_session_id,
1379
+ branch = excluded.branch,
1380
+ use_worktree = excluded.use_worktree,
1381
+ created_at = excluded.created_at,
1382
+ updated_at = excluded.updated_at,
1383
+ completed_at = excluded.completed_at,
1384
+ archived_at = excluded.archived_at,
1385
+ source_connector_id = excluded.source_connector_id,
1386
+ source_external_url = excluded.source_external_url,
1387
+ source_external_id = excluded.source_external_id`
1191
1388
  );
1192
1389
  for (const t of config.tasks ?? []) {
1193
1390
  insertTask.run(
@@ -1208,307 +1405,37 @@ function saveConfig(config) {
1208
1405
  t.archivedAt ?? null,
1209
1406
  t.sourceConnectorId ?? null,
1210
1407
  t.sourceExternalUrl ?? null,
1211
- t.sourceExternalId ?? null
1408
+ t.sourceExternalId ?? null,
1409
+ revision
1212
1410
  );
1213
1411
  }
1214
- d.prepare("DELETE FROM workspaces").run();
1412
+ const workspaces = config.workspaces ?? [DEFAULT_WORKSPACE];
1413
+ pruneMissing(
1414
+ d,
1415
+ "workspaces",
1416
+ "id",
1417
+ workspaces.map((ws) => ws.id),
1418
+ baseRevision
1419
+ );
1215
1420
  const insertWorkspace = d.prepare(
1216
- `INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
1421
+ `INSERT INTO workspaces (id, name, icon, icon_color, "order", row_revision) VALUES (?, ?, ?, ?, ?, ?)
1422
+ ON CONFLICT(id) DO UPDATE SET
1423
+ row_revision = excluded.row_revision,
1424
+ name = excluded.name,
1425
+ icon = excluded.icon,
1426
+ icon_color = excluded.icon_color,
1427
+ "order" = excluded."order"`
1217
1428
  );
1218
- for (const ws of config.workspaces ?? [DEFAULT_WORKSPACE]) {
1219
- insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order);
1429
+ for (const ws of workspaces) {
1430
+ insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order, revision);
1220
1431
  }
1432
+ d.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)").run(
1433
+ CONFIG_REVISION_KEY,
1434
+ String(revision)
1435
+ );
1221
1436
  });
1222
1437
  run();
1223
1438
  }
1224
- function dbListTasks(projectName, status) {
1225
- const d = getDb();
1226
- let sql = "SELECT * FROM tasks";
1227
- const params = [];
1228
- const clauses = [];
1229
- if (projectName) {
1230
- clauses.push("project_name = ?");
1231
- params.push(projectName);
1232
- }
1233
- if (status) {
1234
- clauses.push("status = ?");
1235
- params.push(status);
1236
- }
1237
- if (clauses.length) sql += " WHERE " + clauses.join(" AND ");
1238
- sql += ' ORDER BY "order"';
1239
- const rows = d.prepare(sql).all(...params);
1240
- return rows.map(rowToTask);
1241
- }
1242
- function dbGetTask(id) {
1243
- const row = getDb().prepare("SELECT * FROM tasks WHERE id = ?").get(id);
1244
- return row ? rowToTask(row) : null;
1245
- }
1246
- function dbInsertTask(task) {
1247
- getDb().prepare(
1248
- `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, archived_at, source_connector_id, source_external_url, source_external_id)
1249
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1250
- ).run(
1251
- task.id,
1252
- task.projectName,
1253
- task.title,
1254
- task.description,
1255
- task.status,
1256
- task.order,
1257
- task.assignedSessionId ?? null,
1258
- task.assignedAgent ?? null,
1259
- task.agentSessionId ?? null,
1260
- task.branch ?? null,
1261
- task.useWorktree ? 1 : 0,
1262
- task.createdAt,
1263
- task.updatedAt,
1264
- task.completedAt ?? null,
1265
- task.archivedAt ?? null,
1266
- task.sourceConnectorId ?? null,
1267
- task.sourceExternalUrl ?? null,
1268
- task.sourceExternalId ?? null
1269
- );
1270
- }
1271
- function dbUpdateTask(id, updates) {
1272
- const sets = [];
1273
- const params = [];
1274
- if (updates.title !== void 0) {
1275
- sets.push("title = ?");
1276
- params.push(updates.title);
1277
- }
1278
- if (updates.description !== void 0) {
1279
- sets.push("description = ?");
1280
- params.push(updates.description);
1281
- }
1282
- if (updates.status !== void 0) {
1283
- sets.push("status = ?");
1284
- params.push(updates.status);
1285
- }
1286
- if (updates.order !== void 0) {
1287
- sets.push('"order" = ?');
1288
- params.push(updates.order);
1289
- }
1290
- if (updates.branch !== void 0) {
1291
- sets.push("branch = ?");
1292
- params.push(updates.branch);
1293
- }
1294
- if (updates.useWorktree !== void 0) {
1295
- sets.push("use_worktree = ?");
1296
- params.push(updates.useWorktree ? 1 : 0);
1297
- }
1298
- if (updates.assignedAgent !== void 0) {
1299
- sets.push("assigned_agent = ?");
1300
- params.push(updates.assignedAgent);
1301
- }
1302
- if (updates.assignedSessionId !== void 0) {
1303
- sets.push("assigned_session_id = ?");
1304
- params.push(updates.assignedSessionId);
1305
- }
1306
- if (updates.agentSessionId !== void 0) {
1307
- sets.push("agent_session_id = ?");
1308
- params.push(updates.agentSessionId);
1309
- }
1310
- if (updates.updatedAt !== void 0) {
1311
- sets.push("updated_at = ?");
1312
- params.push(updates.updatedAt);
1313
- }
1314
- if ("completedAt" in updates) {
1315
- sets.push("completed_at = ?");
1316
- params.push(updates.completedAt ?? null);
1317
- }
1318
- if ("archivedAt" in updates) {
1319
- sets.push("archived_at = ?");
1320
- params.push(updates.archivedAt ?? null);
1321
- }
1322
- if (updates.sourceConnectorId !== void 0) {
1323
- sets.push("source_connector_id = ?");
1324
- params.push(updates.sourceConnectorId);
1325
- }
1326
- if (updates.sourceExternalUrl !== void 0) {
1327
- sets.push("source_external_url = ?");
1328
- params.push(updates.sourceExternalUrl);
1329
- }
1330
- if (updates.sourceExternalId !== void 0) {
1331
- sets.push("source_external_id = ?");
1332
- params.push(updates.sourceExternalId);
1333
- }
1334
- if (sets.length === 0) return;
1335
- params.push(id);
1336
- getDb().prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1337
- }
1338
- function dbDeleteTask(id) {
1339
- getDb().prepare("DELETE FROM tasks WHERE id = ?").run(id);
1340
- }
1341
- function dbGetMaxTaskOrder(projectName) {
1342
- const row = getDb().prepare('SELECT MAX("order") as m FROM tasks WHERE project_name = ?').get(projectName);
1343
- return row.m ?? -1;
1344
- }
1345
- function dbListProjects() {
1346
- const rows = getDb().prepare("SELECT * FROM projects").all();
1347
- return rows.map(rowToProject);
1348
- }
1349
- function dbGetProject(name) {
1350
- const row = getDb().prepare("SELECT * FROM projects WHERE name = ?").get(name);
1351
- return row ? rowToProject(row) : null;
1352
- }
1353
- function dbInsertProject(project) {
1354
- getDb().prepare(
1355
- "INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
1356
- ).run(
1357
- project.name,
1358
- project.path,
1359
- JSON.stringify(project.preferredAgents),
1360
- project.icon ?? null,
1361
- project.iconColor ?? null,
1362
- project.hostIds ? JSON.stringify(project.hostIds) : null,
1363
- project.workspaceId ?? "personal"
1364
- );
1365
- }
1366
- function dbUpdateProject(name, updates) {
1367
- const sets = [];
1368
- const params = [];
1369
- if (updates.path !== void 0) {
1370
- sets.push("path = ?");
1371
- params.push(updates.path);
1372
- }
1373
- if (updates.preferredAgents !== void 0) {
1374
- sets.push("preferred_agents = ?");
1375
- params.push(JSON.stringify(updates.preferredAgents));
1376
- }
1377
- if (updates.icon !== void 0) {
1378
- sets.push("icon = ?");
1379
- params.push(updates.icon);
1380
- }
1381
- if (updates.iconColor !== void 0) {
1382
- sets.push("icon_color = ?");
1383
- params.push(updates.iconColor);
1384
- }
1385
- if (updates.hostIds !== void 0) {
1386
- sets.push("host_ids = ?");
1387
- params.push(JSON.stringify(updates.hostIds));
1388
- }
1389
- if (updates.workspaceId !== void 0) {
1390
- sets.push("workspace_id = ?");
1391
- params.push(updates.workspaceId);
1392
- }
1393
- if (sets.length === 0) return;
1394
- params.push(name);
1395
- getDb().prepare(`UPDATE projects SET ${sets.join(", ")} WHERE name = ?`).run(...params);
1396
- }
1397
- function dbDeleteProject(name) {
1398
- const d = getDb();
1399
- d.transaction(() => {
1400
- d.prepare("DELETE FROM tasks WHERE project_name = ?").run(name);
1401
- d.prepare("DELETE FROM projects WHERE name = ?").run(name);
1402
- })();
1403
- }
1404
- function dbListWorkflows() {
1405
- const rows = getDb().prepare("SELECT * FROM workflows").all();
1406
- return rows.map(rowToWorkflow);
1407
- }
1408
- function dbInsertWorkflow(workflow) {
1409
- getDb().prepare(
1410
- `INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
1411
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1412
- ).run(
1413
- workflow.id,
1414
- workflow.name,
1415
- workflow.icon,
1416
- workflow.iconColor,
1417
- JSON.stringify(workflow.nodes),
1418
- JSON.stringify(workflow.edges),
1419
- workflow.enabled ? 1 : 0,
1420
- workflow.lastRunAt ?? null,
1421
- workflow.lastRunStatus ?? null,
1422
- workflow.staggerDelayMs ?? null,
1423
- workflow.workspaceId ?? "personal"
1424
- );
1425
- }
1426
- function dbUpdateWorkflow(id, updates) {
1427
- const sets = [];
1428
- const params = [];
1429
- if (updates.name !== void 0) {
1430
- sets.push("name = ?");
1431
- params.push(updates.name);
1432
- }
1433
- if (updates.nodes !== void 0) {
1434
- sets.push("nodes = ?");
1435
- params.push(JSON.stringify(updates.nodes));
1436
- }
1437
- if (updates.edges !== void 0) {
1438
- sets.push("edges = ?");
1439
- params.push(JSON.stringify(updates.edges));
1440
- }
1441
- if (updates.icon !== void 0) {
1442
- sets.push("icon = ?");
1443
- params.push(updates.icon);
1444
- }
1445
- if (updates.iconColor !== void 0) {
1446
- sets.push("icon_color = ?");
1447
- params.push(updates.iconColor);
1448
- }
1449
- if (updates.enabled !== void 0) {
1450
- sets.push("enabled = ?");
1451
- params.push(updates.enabled ? 1 : 0);
1452
- }
1453
- if (updates.staggerDelayMs !== void 0) {
1454
- sets.push("stagger_delay_ms = ?");
1455
- params.push(updates.staggerDelayMs);
1456
- }
1457
- if (updates.workspaceId !== void 0) {
1458
- sets.push("workspace_id = ?");
1459
- params.push(updates.workspaceId);
1460
- }
1461
- if (sets.length === 0) return;
1462
- params.push(id);
1463
- getDb().prepare(`UPDATE workflows SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1464
- }
1465
- function dbDeleteWorkflow(id) {
1466
- getDb().prepare("DELETE FROM workflows WHERE id = ?").run(id);
1467
- }
1468
- function dbListWorkspaces() {
1469
- const rows = getDb().prepare('SELECT * FROM workspaces ORDER BY "order"').all();
1470
- return rows.map(rowToWorkspace);
1471
- }
1472
- function dbInsertWorkspace(workspace) {
1473
- getDb().prepare(`INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`).run(
1474
- workspace.id,
1475
- workspace.name,
1476
- workspace.icon ?? null,
1477
- workspace.iconColor ?? null,
1478
- workspace.order
1479
- );
1480
- }
1481
- function dbUpdateWorkspace(id, updates) {
1482
- const sets = [];
1483
- const params = [];
1484
- if (updates.name !== void 0) {
1485
- sets.push("name = ?");
1486
- params.push(updates.name);
1487
- }
1488
- if (updates.icon !== void 0) {
1489
- sets.push("icon = ?");
1490
- params.push(updates.icon);
1491
- }
1492
- if (updates.iconColor !== void 0) {
1493
- sets.push("icon_color = ?");
1494
- params.push(updates.iconColor);
1495
- }
1496
- if (updates.order !== void 0) {
1497
- sets.push('"order" = ?');
1498
- params.push(updates.order);
1499
- }
1500
- if (sets.length === 0) return;
1501
- params.push(id);
1502
- getDb().prepare(`UPDATE workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1503
- }
1504
- function dbDeleteWorkspace(id) {
1505
- const d = getDb();
1506
- d.transaction(() => {
1507
- d.prepare("UPDATE projects SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
1508
- d.prepare("UPDATE workflows SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
1509
- d.prepare("DELETE FROM workspaces WHERE id = ?").run(id);
1510
- })();
1511
- }
1512
1439
  function rowToTask(r) {
1513
1440
  return {
1514
1441
  id: r.id,
@@ -1568,141 +1495,15 @@ function rowToWorkspace(r) {
1568
1495
  order: r.order
1569
1496
  };
1570
1497
  }
1571
- function mapNodeRow(n) {
1572
- const structured = n.structured_output != null ? parseStructuredOutput(n.structured_output) : void 0;
1573
- return {
1574
- nodeId: n.node_id,
1575
- status: n.status,
1576
- ...n.started_at != null && { startedAt: n.started_at },
1577
- ...n.completed_at != null && { completedAt: n.completed_at },
1578
- ...n.session_id != null && { sessionId: n.session_id },
1579
- ...n.error != null && { error: n.error },
1580
- ...n.logs != null && { logs: n.logs },
1581
- ...n.task_id != null && { taskId: n.task_id },
1582
- ...n.agent_session_id != null && { agentSessionId: n.agent_session_id },
1583
- ...n.agent_type != null && { agentType: n.agent_type },
1584
- ...n.project_name != null && { projectName: n.project_name },
1585
- ...n.project_path != null && { projectPath: n.project_path },
1586
- ...n.approved_at != null && { approvedAt: n.approved_at },
1587
- ...n.diagnostics != null && { diagnostics: n.diagnostics },
1588
- ...n.output != null && { output: n.output },
1589
- ...structured !== void 0 && { structuredOutput: structured },
1590
- ...n.iteration != null && { iteration: n.iteration }
1591
- };
1592
- }
1593
- function parseStructuredOutput(raw) {
1594
- try {
1595
- const parsed = JSON.parse(raw);
1596
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1597
- } catch {
1598
- return void 0;
1599
- }
1600
- }
1601
- function fetchNodesByRunIds(d, runIds) {
1602
- if (runIds.length === 0) return /* @__PURE__ */ new Map();
1603
- const placeholders = runIds.map(() => "?").join(",");
1604
- const rows = d.prepare(`SELECT * FROM workflow_run_nodes WHERE run_id IN (${placeholders})`).all(...runIds);
1605
- const out = /* @__PURE__ */ new Map();
1606
- for (const r of rows) {
1607
- const bucket = out.get(r.run_id);
1608
- const node = mapNodeRow(r);
1609
- if (bucket) bucket.push(node);
1610
- else out.set(r.run_id, [node]);
1611
- }
1612
- return out;
1613
- }
1614
- function parseRunInputs(raw) {
1615
- if (!raw) return void 0;
1616
- try {
1617
- const parsed = JSON.parse(raw);
1618
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1619
- } catch {
1620
- return void 0;
1621
- }
1622
- }
1623
- function mapRunRows(rows, nodesByRun) {
1624
- return rows.map((r) => {
1625
- const inputs = parseRunInputs(r.inputs);
1626
- const connectorItem = parseRunInputs(r.connector_item);
1627
- return {
1628
- runId: r.id,
1629
- workflowId: r.workflow_id,
1630
- startedAt: r.started_at,
1631
- ...r.completed_at != null && { completedAt: r.completed_at },
1632
- status: r.status,
1633
- ...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
1634
- ...inputs && { inputs },
1635
- ...connectorItem && { connectorItem },
1636
- ...r.connector_inbox_id != null && { connectorInboxId: r.connector_inbox_id },
1637
- ...r.connector_inbox_lease_token != null && {
1638
- connectorInboxLeaseToken: r.connector_inbox_lease_token
1639
- },
1640
- ...r.connector_inbox_disposition === "processed" || r.connector_inbox_disposition === "retry" ? { connectorInboxDisposition: r.connector_inbox_disposition } : {},
1641
- ...r.workflow_name != null && { workflowName: r.workflow_name },
1642
- nodeStates: nodesByRun.get(r.id) ?? []
1643
- };
1644
- });
1645
- }
1646
- function listWorkflowRuns(workflowId, limit = 20) {
1647
- const d = getDb();
1648
- const rows = d.prepare("SELECT * FROM workflow_runs WHERE workflow_id = ? ORDER BY started_at DESC LIMIT ?").all(workflowId, limit);
1649
- return mapRunRows(
1650
- rows,
1651
- fetchNodesByRunIds(
1652
- d,
1653
- rows.map((r) => r.id)
1654
- )
1655
- );
1656
- }
1657
- function listWorkflowRunsByTask(taskId, limit = 20) {
1658
- const d = getDb();
1659
- const rows = d.prepare(
1660
- `SELECT DISTINCT wr.*, w.name as workflow_name
1661
- FROM workflow_runs wr
1662
- LEFT JOIN workflows w ON w.id = wr.workflow_id
1663
- WHERE wr.trigger_task_id = ?
1664
- OR wr.id IN (SELECT run_id FROM workflow_run_nodes WHERE task_id = ?)
1665
- ORDER BY wr.started_at DESC
1666
- LIMIT ?`
1667
- ).all(taskId, taskId, limit);
1668
- return mapRunRows(
1669
- rows,
1670
- fetchNodesByRunIds(
1671
- d,
1672
- rows.map((r) => r.id)
1673
- )
1674
- );
1675
- }
1676
- function listAllWorkflowRuns(workspaceId, limit = 50) {
1677
- const d = getDb();
1678
- const cappedLimit = Math.max(1, Math.min(limit, 500));
1679
- const where = workspaceId ? `WHERE w.id IS NOT NULL AND COALESCE(w.workspace_id, 'personal') = ?` : "";
1680
- const sql = `SELECT wr.*, w.name as workflow_name
1681
- FROM workflow_runs wr
1682
- LEFT JOIN workflows w ON w.id = wr.workflow_id
1683
- ${where}
1684
- ORDER BY wr.started_at DESC
1685
- LIMIT ?`;
1686
- const params = workspaceId ? [workspaceId, cappedLimit] : [cappedLimit];
1687
- const rows = d.prepare(sql).all(...params);
1688
- return mapRunRows(
1689
- rows,
1690
- fetchNodesByRunIds(
1691
- d,
1692
- rows.map((r) => r.id)
1693
- )
1694
- );
1695
- }
1696
1498
 
1697
1499
  // ../server/src/config-manager.ts
1698
- var DB_DIR = path3.join(os2.homedir(), ".vorn");
1699
1500
  var ConfigManager = class {
1700
1501
  changeCallbacks = [];
1701
1502
  dbWatcher = null;
1702
1503
  debounceTimer = null;
1703
1504
  cachedConfig = null;
1704
- init() {
1705
- initDatabase();
1505
+ init(dataDir) {
1506
+ initDatabase(dataDir);
1706
1507
  }
1707
1508
  close() {
1708
1509
  this.stopWatchingDb();
@@ -1715,7 +1516,7 @@ var ConfigManager = class {
1715
1516
  this.cachedConfig = config;
1716
1517
  return config;
1717
1518
  } catch (err) {
1718
- logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
1519
+ logger_default.error({ err }, "[config-manager] loadConfig failed, returning defaults:");
1719
1520
  return {
1720
1521
  version: 1,
1721
1522
  defaults: {
@@ -1735,7 +1536,7 @@ var ConfigManager = class {
1735
1536
  saveConfig(config);
1736
1537
  this.cachedConfig = null;
1737
1538
  } catch (err) {
1738
- logger_default.error("[config-manager] saveConfig failed:", err);
1539
+ logger_default.error({ err }, "[config-manager] saveConfig failed:");
1739
1540
  throw err;
1740
1541
  }
1741
1542
  }
@@ -1759,7 +1560,7 @@ var ConfigManager = class {
1759
1560
  if (this.dbWatcher) return;
1760
1561
  const WATCH_SUFFIXES = [".db-signal", ".db-wal", ".db"];
1761
1562
  try {
1762
- this.dbWatcher = fs3.watch(DB_DIR, (eventType, filename) => {
1563
+ this.dbWatcher = fs3.watch(getDataDir(), (eventType, filename) => {
1763
1564
  if (!filename || !WATCH_SUFFIXES.some((s) => filename.endsWith(s))) return;
1764
1565
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
1765
1566
  this.debounceTimer = setTimeout(() => {
@@ -1795,30 +1596,336 @@ import crypto from "crypto";
1795
1596
  import path4 from "path";
1796
1597
  import { z as z2 } from "zod";
1797
1598
 
1798
- // src/validation.ts
1799
- import { z } from "zod";
1800
- 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("\\"), {
1801
- message: "Name must not contain path traversal characters (.. / \\)"
1802
- });
1803
- var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
1804
- var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
1805
- var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
1806
- var safeShortText = z.string().max(200, "Value must be 200 characters or less");
1807
- var safeUrl = z.string().min(1, "URL must not be empty").max(2048, "URL is too long");
1808
- var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
1809
- 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 /)" });
1810
- var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
1811
- var V = {
1812
- name: safeName,
1813
- id: safeId,
1814
- title: safeTitle,
1815
- description: safeDescription,
1816
- shortText: safeShortText,
1817
- prompt: safePrompt,
1818
- absolutePath: safeAbsolutePath,
1819
- hexColor: safeHexColor,
1820
- url: safeUrl
1821
- };
1599
+ // src/validation.ts
1600
+ import { z } from "zod";
1601
+ 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("\\"), {
1602
+ message: "Name must not contain path traversal characters (.. / \\)"
1603
+ });
1604
+ var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
1605
+ var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
1606
+ var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
1607
+ var safeShortText = z.string().max(200, "Value must be 200 characters or less");
1608
+ var safeUrl = z.string().min(1, "URL must not be empty").max(2048, "URL is too long");
1609
+ var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
1610
+ 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 /)" });
1611
+ var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
1612
+ var V = {
1613
+ name: safeName,
1614
+ id: safeId,
1615
+ title: safeTitle,
1616
+ description: safeDescription,
1617
+ shortText: safeShortText,
1618
+ prompt: safePrompt,
1619
+ absolutePath: safeAbsolutePath,
1620
+ hexColor: safeHexColor,
1621
+ url: safeUrl
1622
+ };
1623
+
1624
+ // src/ws-client.ts
1625
+ import fs4 from "fs";
1626
+ import path3 from "path";
1627
+ import os2 from "os";
1628
+ import { execFileSync as execFileSync2 } from "child_process";
1629
+ import { WebSocket } from "ws";
1630
+ var DATA_DIR = process.env.VORN_DATA_DIR || path3.join(os2.homedir(), ".vorn");
1631
+ var PORT_FILE = path3.join(DATA_DIR, "ws-port");
1632
+ var LOCAL_TOKEN_FILE = path3.join(DATA_DIR, LOCAL_TOKEN_FILENAME);
1633
+ var TOKEN_FILE_MISSING_MSG = `Vorn local credential not found (${LOCAL_TOKEN_FILE}).
1634
+ The server writes it on startup and removes it on shutdown, so this usually means
1635
+ Vorn is not running. Start Vorn (or \`vorn-server serve\`) and try again.
1636
+ If the server runs with --data-dir, set VORN_DATA_DIR to the same directory.`;
1637
+ function readLocalToken() {
1638
+ try {
1639
+ const token = fs4.readFileSync(LOCAL_TOKEN_FILE, "utf-8").trim();
1640
+ if (!token) throw new Error("empty");
1641
+ return token;
1642
+ } catch {
1643
+ throw new Error(TOKEN_FILE_MISSING_MSG);
1644
+ }
1645
+ }
1646
+ function connection() {
1647
+ const result = readPort();
1648
+ if (!result.port) {
1649
+ const reason = "reason" in result ? result.reason : "missing";
1650
+ throw new Error(reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1651
+ }
1652
+ return {
1653
+ url: `ws://127.0.0.1:${result.port}/ws`,
1654
+ options: { headers: { Authorization: `Bearer ${readLocalToken()}` } }
1655
+ };
1656
+ }
1657
+ var TIMEOUT_MS = 1e4;
1658
+ var IS_WIN = process.platform === "win32";
1659
+ var PORT_FILE_MISSING_MSG = IS_WIN ? `Vorn port file not found (~/.vorn/ws-port).
1660
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1661
+ To fix, find the Vorn process and its listening port:
1662
+ powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort"
1663
+ Then write the WS port to the file:
1664
+ echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vorn\\ws-port
1665
+ Or restart Vorn to regenerate it.` : `Vorn port file not found (~/.vorn/ws-port).
1666
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1667
+ To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
1668
+ Then write the WS port (the one on *:<port>) to the file:
1669
+ echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
1670
+ Or restart Vorn to regenerate it.`;
1671
+ var PORT_FILE_INVALID_MSG = `Vorn port file exists but contains invalid data (~/.vorn/ws-port).
1672
+ Delete it and restart Vorn, or overwrite it with the correct port:
1673
+ ${IS_WIN ? "del %USERPROFILE%\\.vorn\\ws-port" : "rm ~/.vorn/ws-port"}`;
1674
+ var rpcId = 0;
1675
+ var cachedPort = null;
1676
+ var cacheTimestamp = 0;
1677
+ var CACHE_TTL_MS = 5e3;
1678
+ var EXEC_OPTS = {
1679
+ encoding: "utf-8",
1680
+ timeout: 5e3,
1681
+ stdio: ["pipe", "pipe", "pipe"]
1682
+ };
1683
+ function discoverPort() {
1684
+ try {
1685
+ if (IS_WIN) {
1686
+ const taskOut = execFileSync2(
1687
+ "tasklist",
1688
+ ["/FI", "IMAGENAME eq Vorn.exe", "/FO", "CSV", "/NH"],
1689
+ EXEC_OPTS
1690
+ );
1691
+ const pidMatch = taskOut.match(/"Vorn\.exe","(\d+)"/);
1692
+ if (!pidMatch) return null;
1693
+ const pid = pidMatch[1];
1694
+ const lines = execFileSync2("netstat", ["-ano"], EXEC_OPTS).split("\n");
1695
+ let fallback = null;
1696
+ for (const line of lines) {
1697
+ if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
1698
+ const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
1699
+ if (!m) continue;
1700
+ if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
1701
+ fallback ??= parseInt(m[1], 10);
1702
+ }
1703
+ return fallback;
1704
+ } else {
1705
+ const lines = execFileSync2("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
1706
+ "\n"
1707
+ );
1708
+ let fallback = null;
1709
+ for (const line of lines) {
1710
+ if (!line.includes("Vorn")) continue;
1711
+ if (line.includes("*:")) {
1712
+ const m = line.match(/\*:(\d+)/);
1713
+ if (m) return parseInt(m[1], 10);
1714
+ }
1715
+ if (!fallback) {
1716
+ const m = line.match(/:(\d+)\s/);
1717
+ if (m) fallback = parseInt(m[1], 10);
1718
+ }
1719
+ }
1720
+ return fallback;
1721
+ }
1722
+ } catch {
1723
+ }
1724
+ return null;
1725
+ }
1726
+ function discoverAndHeal() {
1727
+ const now = Date.now();
1728
+ if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1729
+ const discovered = discoverPort();
1730
+ cachedPort = discovered;
1731
+ cacheTimestamp = now;
1732
+ if (discovered) {
1733
+ try {
1734
+ fs4.mkdirSync(path3.dirname(PORT_FILE), { recursive: true });
1735
+ fs4.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
1736
+ } catch {
1737
+ }
1738
+ return { port: discovered };
1739
+ }
1740
+ return { port: null, reason: "missing" };
1741
+ }
1742
+ function readPort() {
1743
+ try {
1744
+ const raw = fs4.readFileSync(PORT_FILE, "utf-8").trim();
1745
+ if (!raw) return { port: null, reason: "invalid" };
1746
+ if (raw.startsWith("{")) {
1747
+ const parsed = JSON.parse(raw);
1748
+ const p2 = parsed?.port;
1749
+ const pid = parsed?.pid;
1750
+ if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
1751
+ return { port: null, reason: "invalid" };
1752
+ }
1753
+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
1754
+ try {
1755
+ process.kill(pid, 0);
1756
+ } catch (err) {
1757
+ if (err.code === "EPERM") return { port: p2 };
1758
+ return discoverAndHeal();
1759
+ }
1760
+ }
1761
+ return { port: p2 };
1762
+ }
1763
+ const p = parseInt(raw, 10);
1764
+ return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
1765
+ } catch {
1766
+ return discoverAndHeal();
1767
+ }
1768
+ }
1769
+ async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
1770
+ const { url, options } = connection();
1771
+ return new Promise((resolve, reject) => {
1772
+ const ws = new WebSocket(url, options);
1773
+ const id = ++rpcId;
1774
+ const timer = setTimeout(() => {
1775
+ ws.close();
1776
+ reject(new Error(`RPC call "${method}" timed out after ${timeoutMs}ms`));
1777
+ }, timeoutMs);
1778
+ ws.on("open", () => {
1779
+ ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
1780
+ });
1781
+ ws.on("message", (raw) => {
1782
+ try {
1783
+ const msg = JSON.parse(raw.toString());
1784
+ if (msg.id !== id) return;
1785
+ clearTimeout(timer);
1786
+ ws.close();
1787
+ if (msg.error) {
1788
+ reject(new Error(msg.error.message));
1789
+ } else {
1790
+ resolve(msg.result);
1791
+ }
1792
+ } catch {
1793
+ }
1794
+ });
1795
+ ws.on("error", (err) => {
1796
+ clearTimeout(timer);
1797
+ reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
1798
+ });
1799
+ });
1800
+ }
1801
+ async function rpcNotify(method, params) {
1802
+ const { url, options } = connection();
1803
+ return new Promise((resolve, reject) => {
1804
+ const ws = new WebSocket(url, options);
1805
+ ws.on("open", () => {
1806
+ ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
1807
+ ws.close();
1808
+ resolve();
1809
+ });
1810
+ ws.on("error", (err) => {
1811
+ reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
1812
+ });
1813
+ });
1814
+ }
1815
+
1816
+ // src/data-access.ts
1817
+ async function loadConfig2() {
1818
+ return rpcCall("config:load");
1819
+ }
1820
+ async function saveConfig2(config) {
1821
+ await rpcCall("config:save", config);
1822
+ }
1823
+ async function mutate(change) {
1824
+ const config = await loadConfig2();
1825
+ await saveConfig2(change(config));
1826
+ }
1827
+ async function dbListProjects() {
1828
+ return (await loadConfig2()).projects ?? [];
1829
+ }
1830
+ async function dbGetProject(name) {
1831
+ return (await dbListProjects()).find((p) => p.name === name) ?? null;
1832
+ }
1833
+ async function dbInsertProject(project) {
1834
+ await mutate((config) => ({ ...config, projects: [...config.projects ?? [], project] }));
1835
+ }
1836
+ async function dbUpdateProject(name, updates) {
1837
+ await mutate((config) => ({
1838
+ ...config,
1839
+ projects: (config.projects ?? []).map((p) => p.name === name ? { ...p, ...updates } : p)
1840
+ }));
1841
+ }
1842
+ async function dbDeleteProject(name) {
1843
+ await mutate((config) => ({
1844
+ ...config,
1845
+ projects: (config.projects ?? []).filter((p) => p.name !== name)
1846
+ }));
1847
+ }
1848
+ async function dbListWorkspaces() {
1849
+ return (await loadConfig2()).workspaces ?? [];
1850
+ }
1851
+ async function dbInsertWorkspace(workspace) {
1852
+ await mutate((config) => ({ ...config, workspaces: [...config.workspaces ?? [], workspace] }));
1853
+ }
1854
+ async function dbUpdateWorkspace(id, updates) {
1855
+ await mutate((config) => ({
1856
+ ...config,
1857
+ workspaces: (config.workspaces ?? []).map((w) => w.id === id ? { ...w, ...updates } : w)
1858
+ }));
1859
+ }
1860
+ async function dbDeleteWorkspace(id) {
1861
+ await mutate((config) => ({
1862
+ ...config,
1863
+ workspaces: (config.workspaces ?? []).filter((w) => w.id !== id)
1864
+ }));
1865
+ }
1866
+ async function dbListWorkflows() {
1867
+ return (await loadConfig2()).workflows ?? [];
1868
+ }
1869
+ async function dbInsertWorkflow(workflow) {
1870
+ await mutate((config) => ({ ...config, workflows: [...config.workflows ?? [], workflow] }));
1871
+ }
1872
+ async function dbUpdateWorkflow(id, updates) {
1873
+ await mutate((config) => ({
1874
+ ...config,
1875
+ workflows: (config.workflows ?? []).map((w) => w.id === id ? { ...w, ...updates } : w)
1876
+ }));
1877
+ }
1878
+ async function dbDeleteWorkflow(id) {
1879
+ await mutate((config) => ({
1880
+ ...config,
1881
+ workflows: (config.workflows ?? []).filter((w) => w.id !== id)
1882
+ }));
1883
+ }
1884
+ async function dbListTasks(projectName, status) {
1885
+ const tasks = (await loadConfig2()).tasks ?? [];
1886
+ return tasks.filter(
1887
+ (t) => (projectName === void 0 || t.projectName === projectName) && (status === void 0 || t.status === status)
1888
+ );
1889
+ }
1890
+ async function dbGetTask(id) {
1891
+ return ((await loadConfig2()).tasks ?? []).find((t) => t.id === id) ?? null;
1892
+ }
1893
+ async function dbInsertTask(task) {
1894
+ await mutate((config) => ({ ...config, tasks: [...config.tasks ?? [], task] }));
1895
+ }
1896
+ async function dbUpdateTask(id, updates) {
1897
+ await mutate((config) => ({
1898
+ ...config,
1899
+ tasks: (config.tasks ?? []).map((t) => t.id === id ? { ...t, ...updates } : t)
1900
+ }));
1901
+ }
1902
+ async function dbDeleteTask(id) {
1903
+ await mutate((config) => ({
1904
+ ...config,
1905
+ tasks: (config.tasks ?? []).filter((t) => t.id !== id)
1906
+ }));
1907
+ }
1908
+ async function dbGetMaxTaskOrder(projectName) {
1909
+ const tasks = await dbListTasks(projectName);
1910
+ return tasks.reduce((max, t) => Math.max(max, t.order ?? 0), 0);
1911
+ }
1912
+ function dbSignalChange() {
1913
+ }
1914
+ async function listWorkflowRuns(workflowId, limit = 20) {
1915
+ return rpcCall("workflowRun:list", { workflowId, limit });
1916
+ }
1917
+ async function listWorkflowRunsByTask(taskId, limit = 20) {
1918
+ return rpcCall("workflowRun:listByTask", {
1919
+ taskId,
1920
+ limit
1921
+ });
1922
+ }
1923
+ async function listAllWorkflowRuns(workspaceId, limit = 50) {
1924
+ return rpcCall("workflowRun:listAll", {
1925
+ workspaceId,
1926
+ limit
1927
+ });
1928
+ }
1822
1929
 
1823
1930
  // src/tools/tasks.ts
1824
1931
  var TASK_STATUSES = ["todo", "in_progress", "in_review", "done", "cancelled"];
@@ -1841,12 +1948,12 @@ function registerTaskTools(server) {
1841
1948
  include_archived: z2.boolean().optional().describe("Include archived tasks in the result (default: false)")
1842
1949
  },
1843
1950
  async (args) => {
1844
- let tasks = dbListTasks(args.project_name, args.status);
1951
+ let tasks = await dbListTasks(args.project_name, args.status);
1845
1952
  if (!args.include_archived) {
1846
1953
  tasks = tasks.filter((t) => !t.archivedAt);
1847
1954
  }
1848
1955
  if (args.workspace_id) {
1849
- const projects = dbListProjects();
1956
+ const projects = await dbListProjects();
1850
1957
  const wsProjectNames = new Set(
1851
1958
  projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id).map((p) => p.name)
1852
1959
  );
@@ -1871,14 +1978,14 @@ function registerTaskTools(server) {
1871
1978
  assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assign to an agent type")
1872
1979
  },
1873
1980
  async (args) => {
1874
- const project = dbGetProject(args.project_name);
1981
+ const project = await dbGetProject(args.project_name);
1875
1982
  if (!project) {
1876
1983
  return {
1877
1984
  content: [{ type: "text", text: `Error: project "${args.project_name}" not found` }],
1878
1985
  isError: true
1879
1986
  };
1880
1987
  }
1881
- const maxOrder = dbGetMaxTaskOrder(args.project_name);
1988
+ const maxOrder = await dbGetMaxTaskOrder(args.project_name);
1882
1989
  const now = (/* @__PURE__ */ new Date()).toISOString();
1883
1990
  const status = args.status ?? "todo";
1884
1991
  const task = {
@@ -1895,13 +2002,13 @@ function registerTaskTools(server) {
1895
2002
  ...args.assigned_agent && { assignedAgent: args.assigned_agent },
1896
2003
  ...(status === "done" || status === "cancelled") && { completedAt: now }
1897
2004
  };
1898
- dbInsertTask(task);
2005
+ await dbInsertTask(task);
1899
2006
  dbSignalChange();
1900
2007
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1901
2008
  }
1902
2009
  );
1903
2010
  server.tool("get_task", "Get a task by ID", { id: V.id.describe("Task ID") }, async (args) => {
1904
- const task = dbGetTask(args.id);
2011
+ const task = await dbGetTask(args.id);
1905
2012
  if (!task) {
1906
2013
  return {
1907
2014
  content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
@@ -1924,7 +2031,7 @@ function registerTaskTools(server) {
1924
2031
  order: z2.number().optional().describe("Queue order")
1925
2032
  },
1926
2033
  async (args) => {
1927
- const task = dbGetTask(args.id);
2034
+ const task = await dbGetTask(args.id);
1928
2035
  if (!task) {
1929
2036
  return {
1930
2037
  content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
@@ -1950,9 +2057,9 @@ function registerTaskTools(server) {
1950
2057
  updates.archivedAt = void 0;
1951
2058
  }
1952
2059
  }
1953
- dbUpdateTask(args.id, updates);
2060
+ await dbUpdateTask(args.id, updates);
1954
2061
  dbSignalChange();
1955
- const updated = dbGetTask(args.id);
2062
+ const updated = await dbGetTask(args.id);
1956
2063
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
1957
2064
  }
1958
2065
  );
@@ -1961,14 +2068,14 @@ function registerTaskTools(server) {
1961
2068
  "Delete a task by ID",
1962
2069
  { id: V.id.describe("Task ID") },
1963
2070
  async (args) => {
1964
- const task = dbGetTask(args.id);
2071
+ const task = await dbGetTask(args.id);
1965
2072
  if (!task) {
1966
2073
  return {
1967
2074
  content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
1968
2075
  isError: true
1969
2076
  };
1970
2077
  }
1971
- dbDeleteTask(args.id);
2078
+ await dbDeleteTask(args.id);
1972
2079
  dbSignalChange();
1973
2080
  return { content: [{ type: "text", text: `Deleted task: ${task.title}` }] };
1974
2081
  }
@@ -1978,7 +2085,7 @@ function registerTaskTools(server) {
1978
2085
  "Archive a finished task (status must be done or cancelled). Archived tasks are hidden from default views but preserved and restorable.",
1979
2086
  { id: V.id.describe("Task ID") },
1980
2087
  async (args) => {
1981
- const task = dbGetTask(args.id);
2088
+ const task = await dbGetTask(args.id);
1982
2089
  if (!task) {
1983
2090
  return {
1984
2091
  content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
@@ -1997,9 +2104,9 @@ function registerTaskTools(server) {
1997
2104
  };
1998
2105
  }
1999
2106
  const now = (/* @__PURE__ */ new Date()).toISOString();
2000
- dbUpdateTask(args.id, { archivedAt: now, updatedAt: now });
2107
+ await dbUpdateTask(args.id, { archivedAt: now, updatedAt: now });
2001
2108
  dbSignalChange();
2002
- const updated = dbGetTask(args.id);
2109
+ const updated = await dbGetTask(args.id);
2003
2110
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
2004
2111
  }
2005
2112
  );
@@ -2008,7 +2115,7 @@ function registerTaskTools(server) {
2008
2115
  "Restore an archived task so it shows in default views again.",
2009
2116
  { id: V.id.describe("Task ID") },
2010
2117
  async (args) => {
2011
- const task = dbGetTask(args.id);
2118
+ const task = await dbGetTask(args.id);
2012
2119
  if (!task) {
2013
2120
  return {
2014
2121
  content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
@@ -2016,9 +2123,9 @@ function registerTaskTools(server) {
2016
2123
  };
2017
2124
  }
2018
2125
  const now = (/* @__PURE__ */ new Date()).toISOString();
2019
- dbUpdateTask(args.id, { archivedAt: void 0, updatedAt: now });
2126
+ await dbUpdateTask(args.id, { archivedAt: void 0, updatedAt: now });
2020
2127
  dbSignalChange();
2021
- const updated = dbGetTask(args.id);
2128
+ const updated = await dbGetTask(args.id);
2022
2129
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
2023
2130
  }
2024
2131
  );
@@ -2033,15 +2140,15 @@ function registerTaskTools(server) {
2033
2140
  },
2034
2141
  async (args) => {
2035
2142
  if (args.task_id) {
2036
- const task = dbGetTask(args.task_id);
2143
+ const task = await dbGetTask(args.task_id);
2037
2144
  if (!task) {
2038
2145
  return {
2039
2146
  content: [{ type: "text", text: `Error: task "${args.task_id}" not found` }],
2040
2147
  isError: true
2041
2148
  };
2042
2149
  }
2043
- const project = dbGetProject(task.projectName);
2044
- const siblingTasks = dbListTasks(task.projectName);
2150
+ const project = await dbGetProject(task.projectName);
2151
+ const siblingTasks = await dbListTasks(task.projectName);
2045
2152
  return {
2046
2153
  content: [
2047
2154
  {
@@ -2066,7 +2173,7 @@ function registerTaskTools(server) {
2066
2173
  }
2067
2174
  const cwd = args.cwd || process.cwd();
2068
2175
  const normalizedCwd = path4.resolve(cwd);
2069
- const projects = dbListProjects();
2176
+ const projects = await dbListProjects();
2070
2177
  let matchedProject = null;
2071
2178
  let matchLen = 0;
2072
2179
  for (const p of projects) {
@@ -2094,7 +2201,7 @@ function registerTaskTools(server) {
2094
2201
  ]
2095
2202
  };
2096
2203
  }
2097
- const projectTasks = dbListTasks(matchedProject.name);
2204
+ const projectTasks = await dbListTasks(matchedProject.name);
2098
2205
  let matchedTask = null;
2099
2206
  for (const t of projectTasks) {
2100
2207
  if (t.worktreePath) {
@@ -2150,7 +2257,7 @@ function registerProjectTools(server) {
2150
2257
  workspace_id: V.id.optional().describe('Filter by workspace ID (e.g. "personal")')
2151
2258
  },
2152
2259
  async (args) => {
2153
- let projects = dbListProjects();
2260
+ let projects = await dbListProjects();
2154
2261
  if (args.workspace_id) {
2155
2262
  projects = projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id);
2156
2263
  }
@@ -2168,7 +2275,7 @@ function registerProjectTools(server) {
2168
2275
  icon_color: V.hexColor.optional().describe("Hex color for icon")
2169
2276
  },
2170
2277
  async (args) => {
2171
- if (dbGetProject(args.name)) {
2278
+ if (await dbGetProject(args.name)) {
2172
2279
  return {
2173
2280
  content: [{ type: "text", text: `Error: project "${args.name}" already exists` }],
2174
2281
  isError: true
@@ -2181,7 +2288,7 @@ function registerProjectTools(server) {
2181
2288
  ...args.icon && { icon: args.icon },
2182
2289
  ...args.icon_color && { iconColor: args.icon_color }
2183
2290
  };
2184
- dbInsertProject(project);
2291
+ await dbInsertProject(project);
2185
2292
  dbSignalChange();
2186
2293
  return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }] };
2187
2294
  }
@@ -2197,7 +2304,7 @@ function registerProjectTools(server) {
2197
2304
  icon_color: V.hexColor.optional().describe("Hex color for icon")
2198
2305
  },
2199
2306
  async (args) => {
2200
- if (!dbGetProject(args.name)) {
2307
+ if (!await dbGetProject(args.name)) {
2201
2308
  return {
2202
2309
  content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
2203
2310
  isError: true
@@ -2209,9 +2316,9 @@ function registerProjectTools(server) {
2209
2316
  updates.preferredAgents = args.preferred_agents;
2210
2317
  if (args.icon !== void 0) updates.icon = args.icon;
2211
2318
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
2212
- dbUpdateProject(args.name, updates);
2319
+ await dbUpdateProject(args.name, updates);
2213
2320
  dbSignalChange();
2214
- const updated = dbGetProject(args.name);
2321
+ const updated = await dbGetProject(args.name);
2215
2322
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
2216
2323
  }
2217
2324
  );
@@ -2220,13 +2327,13 @@ function registerProjectTools(server) {
2220
2327
  "Delete a project and all its tasks",
2221
2328
  { name: V.name.describe("Project name") },
2222
2329
  async (args) => {
2223
- if (!dbGetProject(args.name)) {
2330
+ if (!await dbGetProject(args.name)) {
2224
2331
  return {
2225
2332
  content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
2226
2333
  isError: true
2227
2334
  };
2228
2335
  }
2229
- dbDeleteProject(args.name);
2336
+ await dbDeleteProject(args.name);
2230
2337
  dbSignalChange();
2231
2338
  return { content: [{ type: "text", text: `Deleted project: ${args.name}` }] };
2232
2339
  }
@@ -2235,180 +2342,6 @@ function registerProjectTools(server) {
2235
2342
 
2236
2343
  // src/tools/sessions.ts
2237
2344
  import { z as z4 } from "zod";
2238
-
2239
- // src/ws-client.ts
2240
- import fs4 from "fs";
2241
- import path5 from "path";
2242
- import os3 from "os";
2243
- import { execFileSync as execFileSync2 } from "child_process";
2244
- import { WebSocket } from "ws";
2245
- var PORT_FILE = path5.join(os3.homedir(), ".vorn", "ws-port");
2246
- var TIMEOUT_MS = 1e4;
2247
- var IS_WIN = process.platform === "win32";
2248
- var PORT_FILE_MISSING_MSG = IS_WIN ? `Vorn port file not found (~/.vorn/ws-port).
2249
- The app may be running but the port file was deleted (e.g. by another instance shutting down).
2250
- To fix, find the Vorn process and its listening port:
2251
- powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort"
2252
- Then write the WS port to the file:
2253
- echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vorn\\ws-port
2254
- Or restart Vorn to regenerate it.` : `Vorn port file not found (~/.vorn/ws-port).
2255
- The app may be running but the port file was deleted (e.g. by another instance shutting down).
2256
- To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
2257
- Then write the WS port (the one on *:<port>) to the file:
2258
- echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
2259
- Or restart Vorn to regenerate it.`;
2260
- var PORT_FILE_INVALID_MSG = `Vorn port file exists but contains invalid data (~/.vorn/ws-port).
2261
- Delete it and restart Vorn, or overwrite it with the correct port:
2262
- ${IS_WIN ? "del %USERPROFILE%\\.vorn\\ws-port" : "rm ~/.vorn/ws-port"}`;
2263
- var rpcId = 0;
2264
- var cachedPort = null;
2265
- var cacheTimestamp = 0;
2266
- var CACHE_TTL_MS = 5e3;
2267
- var EXEC_OPTS = {
2268
- encoding: "utf-8",
2269
- timeout: 5e3,
2270
- stdio: ["pipe", "pipe", "pipe"]
2271
- };
2272
- function discoverPort() {
2273
- try {
2274
- if (IS_WIN) {
2275
- const taskOut = execFileSync2(
2276
- "tasklist",
2277
- ["/FI", "IMAGENAME eq Vorn.exe", "/FO", "CSV", "/NH"],
2278
- EXEC_OPTS
2279
- );
2280
- const pidMatch = taskOut.match(/"Vorn\.exe","(\d+)"/);
2281
- if (!pidMatch) return null;
2282
- const pid = pidMatch[1];
2283
- const lines = execFileSync2("netstat", ["-ano"], EXEC_OPTS).split("\n");
2284
- let fallback = null;
2285
- for (const line of lines) {
2286
- if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
2287
- const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
2288
- if (!m) continue;
2289
- if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
2290
- fallback ??= parseInt(m[1], 10);
2291
- }
2292
- return fallback;
2293
- } else {
2294
- const lines = execFileSync2("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
2295
- "\n"
2296
- );
2297
- let fallback = null;
2298
- for (const line of lines) {
2299
- if (!line.includes("Vorn")) continue;
2300
- if (line.includes("*:")) {
2301
- const m = line.match(/\*:(\d+)/);
2302
- if (m) return parseInt(m[1], 10);
2303
- }
2304
- if (!fallback) {
2305
- const m = line.match(/:(\d+)\s/);
2306
- if (m) fallback = parseInt(m[1], 10);
2307
- }
2308
- }
2309
- return fallback;
2310
- }
2311
- } catch {
2312
- }
2313
- return null;
2314
- }
2315
- function discoverAndHeal() {
2316
- const now = Date.now();
2317
- if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
2318
- const discovered = discoverPort();
2319
- cachedPort = discovered;
2320
- cacheTimestamp = now;
2321
- if (discovered) {
2322
- try {
2323
- fs4.mkdirSync(path5.dirname(PORT_FILE), { recursive: true });
2324
- fs4.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
2325
- } catch {
2326
- }
2327
- return { port: discovered };
2328
- }
2329
- return { port: null, reason: "missing" };
2330
- }
2331
- function readPort() {
2332
- try {
2333
- const raw = fs4.readFileSync(PORT_FILE, "utf-8").trim();
2334
- if (!raw) return { port: null, reason: "invalid" };
2335
- if (raw.startsWith("{")) {
2336
- const parsed = JSON.parse(raw);
2337
- const p2 = parsed?.port;
2338
- const pid = parsed?.pid;
2339
- if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
2340
- return { port: null, reason: "invalid" };
2341
- }
2342
- if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
2343
- try {
2344
- process.kill(pid, 0);
2345
- } catch (err) {
2346
- if (err.code === "EPERM") return { port: p2 };
2347
- return discoverAndHeal();
2348
- }
2349
- }
2350
- return { port: p2 };
2351
- }
2352
- const p = parseInt(raw, 10);
2353
- return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
2354
- } catch {
2355
- return discoverAndHeal();
2356
- }
2357
- }
2358
- async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
2359
- const result = readPort();
2360
- if (!result.port) {
2361
- throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
2362
- }
2363
- return new Promise((resolve, reject) => {
2364
- const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
2365
- const id = ++rpcId;
2366
- const timer = setTimeout(() => {
2367
- ws.close();
2368
- reject(new Error(`RPC call "${method}" timed out after ${timeoutMs}ms`));
2369
- }, timeoutMs);
2370
- ws.on("open", () => {
2371
- ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
2372
- });
2373
- ws.on("message", (raw) => {
2374
- try {
2375
- const msg = JSON.parse(raw.toString());
2376
- if (msg.id !== id) return;
2377
- clearTimeout(timer);
2378
- ws.close();
2379
- if (msg.error) {
2380
- reject(new Error(msg.error.message));
2381
- } else {
2382
- resolve(msg.result);
2383
- }
2384
- } catch {
2385
- }
2386
- });
2387
- ws.on("error", (err) => {
2388
- clearTimeout(timer);
2389
- reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
2390
- });
2391
- });
2392
- }
2393
- async function rpcNotify(method, params) {
2394
- const result = readPort();
2395
- if (!result.port) {
2396
- throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
2397
- }
2398
- return new Promise((resolve, reject) => {
2399
- const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
2400
- ws.on("open", () => {
2401
- ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
2402
- ws.close();
2403
- resolve();
2404
- });
2405
- ws.on("error", (err) => {
2406
- reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
2407
- });
2408
- });
2409
- }
2410
-
2411
- // src/tools/sessions.ts
2412
2345
  var AGENT_TYPES3 = [
2413
2346
  "claude",
2414
2347
  "copilot",
@@ -3138,7 +3071,7 @@ function registerWorkflowTools(server) {
3138
3071
  workspace_id: V.id.optional().describe("Filter by workspace ID")
3139
3072
  },
3140
3073
  async (args) => {
3141
- let workflows = dbListWorkflows();
3074
+ let workflows = await dbListWorkflows();
3142
3075
  if (args.workspace_id) {
3143
3076
  workflows = workflows.filter((w) => (w.workspaceId ?? "personal") === args.workspace_id);
3144
3077
  }
@@ -3191,7 +3124,7 @@ function registerWorkflowTools(server) {
3191
3124
  enabled: args.enabled ?? true,
3192
3125
  ...args.stagger_delay_ms && { staggerDelayMs: args.stagger_delay_ms }
3193
3126
  };
3194
- dbInsertWorkflow(workflow);
3127
+ await dbInsertWorkflow(workflow);
3195
3128
  dbSignalChange();
3196
3129
  return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
3197
3130
  }
@@ -3215,7 +3148,7 @@ function registerWorkflowTools(server) {
3215
3148
  if ("error" in resolved) {
3216
3149
  return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
3217
3150
  }
3218
- const workflows = dbListWorkflows();
3151
+ const workflows = await dbListWorkflows();
3219
3152
  const workflow = workflows.find((w) => w.id === resolved.id);
3220
3153
  if (!workflow) {
3221
3154
  return {
@@ -3242,7 +3175,7 @@ function registerWorkflowTools(server) {
3242
3175
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
3243
3176
  if (args.enabled !== void 0) updates.enabled = args.enabled;
3244
3177
  if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
3245
- dbUpdateWorkflow(resolved.id, updates);
3178
+ await dbUpdateWorkflow(resolved.id, updates);
3246
3179
  dbSignalChange();
3247
3180
  return {
3248
3181
  content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
@@ -3261,7 +3194,7 @@ function registerWorkflowTools(server) {
3261
3194
  if ("error" in resolved) {
3262
3195
  return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
3263
3196
  }
3264
- const workflows = dbListWorkflows();
3197
+ const workflows = await dbListWorkflows();
3265
3198
  const workflow = workflows.find((w) => w.id === resolved.id);
3266
3199
  if (!workflow) {
3267
3200
  return {
@@ -3269,7 +3202,7 @@ function registerWorkflowTools(server) {
3269
3202
  isError: true
3270
3203
  };
3271
3204
  }
3272
- dbDeleteWorkflow(resolved.id);
3205
+ await dbDeleteWorkflow(resolved.id);
3273
3206
  dbSignalChange();
3274
3207
  return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
3275
3208
  }
@@ -3290,11 +3223,11 @@ function registerWorkflowTools(server) {
3290
3223
  };
3291
3224
  }
3292
3225
  if (args.task_id) {
3293
- const runs = listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
3226
+ const runs = await listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
3294
3227
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3295
3228
  }
3296
3229
  if (args.workflow_id) {
3297
- const runs = listWorkflowRuns(args.workflow_id, args.limit ?? 20);
3230
+ const runs = await listWorkflowRuns(args.workflow_id, args.limit ?? 20);
3298
3231
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3299
3232
  }
3300
3233
  return {
@@ -3310,7 +3243,7 @@ function registerWorkflowTools(server) {
3310
3243
  run_id: V.id.describe("Run ID (from list_workflow_runs)")
3311
3244
  },
3312
3245
  async (args) => {
3313
- const run = listAllWorkflowRuns(void 0, 500).find((r) => r.runId === args.run_id);
3246
+ const run = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === args.run_id);
3314
3247
  if (!run) {
3315
3248
  return {
3316
3249
  content: [
@@ -3401,7 +3334,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3401
3334
  inputs: z5.record(z5.string(), z5.union([z5.string(), z5.number(), z5.boolean()])).optional().describe("Values for the declared parameters, keyed by input key ({{inputs.<key>}})")
3402
3335
  },
3403
3336
  async (args) => {
3404
- const workflow = dbListWorkflows().find((w) => w.id === args.workflow_id);
3337
+ const workflow = (await dbListWorkflows()).find((w) => w.id === args.workflow_id);
3405
3338
  if (!workflow) {
3406
3339
  return {
3407
3340
  content: [{ type: "text", text: `Error: workflow "${args.workflow_id}" not found` }],
@@ -3462,7 +3395,7 @@ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3462
3395
  if ("error" in resolved) {
3463
3396
  return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
3464
3397
  }
3465
- const workflow = dbListWorkflows().find((w) => w.id === resolved.id);
3398
+ const workflow = (await dbListWorkflows()).find((w) => w.id === resolved.id);
3466
3399
  if (!workflow) {
3467
3400
  return {
3468
3401
  content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
@@ -3481,7 +3414,7 @@ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3481
3414
  isError: true
3482
3415
  };
3483
3416
  }
3484
- const projects = dbListProjects();
3417
+ const projects = await dbListProjects();
3485
3418
  const projectName = workflow.nodes.map((n) => n.config.projectName).find((name) => typeof name === "string" && name.length > 0);
3486
3419
  const project = projects.find((p) => p.name === projectName);
3487
3420
  if (!project) {
@@ -3549,7 +3482,7 @@ Warning: these still hold a machine-specific path and will not travel: ${residua
3549
3482
  isError: true
3550
3483
  };
3551
3484
  }
3552
- const project = dbListProjects().find((p) => p.name === args.project_name);
3485
+ const project = (await dbListProjects()).find((p) => p.name === args.project_name);
3553
3486
  if (!project) {
3554
3487
  return {
3555
3488
  content: [
@@ -3591,11 +3524,11 @@ Warning: these still hold a machine-specific path and will not travel: ${residua
3591
3524
  isError: true
3592
3525
  };
3593
3526
  }
3594
- const existing = dbListWorkflows().find((w) => w.id === definition.id);
3527
+ const existing = (await dbListWorkflows()).find((w) => w.id === definition.id);
3595
3528
  if (existing) {
3596
- dbUpdateWorkflow(definition.id, definition);
3529
+ await dbUpdateWorkflow(definition.id, definition);
3597
3530
  } else {
3598
- dbInsertWorkflow(definition);
3531
+ await dbInsertWorkflow(definition);
3599
3532
  }
3600
3533
  dbSignalChange();
3601
3534
  return {
@@ -3627,7 +3560,7 @@ import crypto3 from "crypto";
3627
3560
  import { z as z6 } from "zod";
3628
3561
  function registerWorkspaceTools(server) {
3629
3562
  server.tool("list_workspaces", "List all workspaces", async () => {
3630
- const workspaces = dbListWorkspaces();
3563
+ const workspaces = await dbListWorkspaces();
3631
3564
  return { content: [{ type: "text", text: JSON.stringify(workspaces, null, 2) }] };
3632
3565
  });
3633
3566
  server.tool(
@@ -3639,7 +3572,7 @@ function registerWorkspaceTools(server) {
3639
3572
  icon_color: V.hexColor.optional().describe("Hex color for icon")
3640
3573
  },
3641
3574
  async (args) => {
3642
- const existing = dbListWorkspaces();
3575
+ const existing = await dbListWorkspaces();
3643
3576
  const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
3644
3577
  const workspace = {
3645
3578
  id: crypto3.randomUUID(),
@@ -3648,7 +3581,7 @@ function registerWorkspaceTools(server) {
3648
3581
  ...args.icon && { icon: args.icon },
3649
3582
  ...args.icon_color && { iconColor: args.icon_color }
3650
3583
  };
3651
- dbInsertWorkspace(workspace);
3584
+ await dbInsertWorkspace(workspace);
3652
3585
  dbSignalChange();
3653
3586
  return { content: [{ type: "text", text: JSON.stringify(workspace, null, 2) }] };
3654
3587
  }
@@ -3664,7 +3597,7 @@ function registerWorkspaceTools(server) {
3664
3597
  order: z6.number().int().min(0).optional().describe("Sort order")
3665
3598
  },
3666
3599
  async (args) => {
3667
- const existing = dbListWorkspaces();
3600
+ const existing = await dbListWorkspaces();
3668
3601
  if (!existing.find((w) => w.id === args.id)) {
3669
3602
  return {
3670
3603
  content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
@@ -3676,9 +3609,9 @@ function registerWorkspaceTools(server) {
3676
3609
  if (args.icon !== void 0) updates.icon = args.icon;
3677
3610
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
3678
3611
  if (args.order !== void 0) updates.order = args.order;
3679
- dbUpdateWorkspace(args.id, updates);
3612
+ await dbUpdateWorkspace(args.id, updates);
3680
3613
  dbSignalChange();
3681
- const updated = dbListWorkspaces().find((w) => w.id === args.id);
3614
+ const updated = (await dbListWorkspaces()).find((w) => w.id === args.id);
3682
3615
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
3683
3616
  }
3684
3617
  );
@@ -3693,7 +3626,7 @@ function registerWorkspaceTools(server) {
3693
3626
  isError: true
3694
3627
  };
3695
3628
  }
3696
- const existing = dbListWorkspaces();
3629
+ const existing = await dbListWorkspaces();
3697
3630
  const workspace = existing.find((w) => w.id === args.id);
3698
3631
  if (!workspace) {
3699
3632
  return {
@@ -3701,7 +3634,7 @@ function registerWorkspaceTools(server) {
3701
3634
  isError: true
3702
3635
  };
3703
3636
  }
3704
- dbDeleteWorkspace(args.id);
3637
+ await dbDeleteWorkspace(args.id);
3705
3638
  dbSignalChange();
3706
3639
  return { content: [{ type: "text", text: `Deleted workspace: ${workspace.name}` }] };
3707
3640
  }
@@ -3884,7 +3817,7 @@ function registerConnectorTools(server) {
3884
3817
  );
3885
3818
  }
3886
3819
  const launch = typeof target === "string" ? parseLaunch(target) : target;
3887
- const connection = await rpcCall("connection:create", {
3820
+ const connection2 = await rpcCall("connection:create", {
3888
3821
  connectorId: "mcp",
3889
3822
  name: args.name ?? (trigger ? `${manifest.name}: ${trigger.label}` : manifest.name),
3890
3823
  filters: {
@@ -3902,7 +3835,7 @@ function registerConnectorTools(server) {
3902
3835
  });
3903
3836
  return json({
3904
3837
  installed: manifest.name,
3905
- connectionId: connection.id,
3838
+ connectionId: connection2.id,
3906
3839
  trigger: trigger?.type,
3907
3840
  note: "Poll it now with backfill_connection, or reference it from a workflow."
3908
3841
  });
@@ -3986,7 +3919,7 @@ function errorResult(err) {
3986
3919
  };
3987
3920
  }
3988
3921
  function source(label) {
3989
- return label.includes("WEB PAGE") ? "page" : "device";
3922
+ return label.includes("WEB PAGE") || label.includes("BROWSER") ? "page" : "device";
3990
3923
  }
3991
3924
  function pageResult(data, label = "WEB PAGE CONTENT") {
3992
3925
  const nonce = crypto4.randomUUID();
@@ -4116,7 +4049,7 @@ function registerBrowserTools(server) {
4116
4049
  );
4117
4050
  server.tool(
4118
4051
  "browser_interact",
4119
- 'Act on your session browser pane: click, hover, type, press a key, or scroll. Address the target by "ref" from read_page where possible \u2014 refs survive reflow, coordinates do not. A ref from before a navigation is refused rather than guessed at; re-read the page.',
4052
+ 'Act on your session browser pane: click, hover, type, press a key, or scroll. Address the target by "ref" from read_page where possible \u2014 refs survive reflow, coordinates do not. A ref from before a navigation is refused rather than guessed at; re-read the page. A ref target is scrolled into view first, so it does not need to be on screen already. "ok" means the input was dispatched, not that the page did what you expected \u2014 read the state back after anything that matters.',
4120
4053
  {
4121
4054
  action: z8.enum(["click", "hover", "type", "key", "scroll"]).describe('What to do. "type" clicks the target first when one is given.'),
4122
4055
  ref: V.shortText.optional().describe("Element ref from read_page"),
@@ -4147,13 +4080,19 @@ function registerBrowserTools(server) {
4147
4080
  );
4148
4081
  server.tool(
4149
4082
  "browser_tabs",
4150
- 'Add, close, or switch tabs in your session browser pane. "close" and "select" take a zero-based index; closing the last remaining tab closes the pane.',
4083
+ 'List, add, close, or switch tabs in your session browser pane. "close" and "select" take a zero-based index \u2014 call "list" first to see what those indices name, since a tab that redirected or followed a link is no longer on the page it was opened with. Closing the last remaining tab closes the pane.',
4151
4084
  {
4152
- action: z8.enum(["add", "close", "select"]).describe("What to do with tabs"),
4085
+ action: z8.enum(["list", "add", "close", "select"]).describe("What to do with tabs"),
4153
4086
  url: V.url.optional().describe('URL for "add"'),
4154
4087
  index: z8.number().int().min(0).optional().describe("Zero-based tab index for close/select")
4155
4088
  },
4156
4089
  async (args) => withSession(async (id) => {
4090
+ if (args.action === "list") {
4091
+ const result = await rpcCall("browser:listTabs", {
4092
+ sessionId: id
4093
+ });
4094
+ return pageResult(result.tabs, "BROWSER TAB LIST");
4095
+ }
4157
4096
  await rpcCall("browser:tabs", {
4158
4097
  sessionId: id,
4159
4098
  action: args.action,
@@ -4165,7 +4104,7 @@ function registerBrowserTools(server) {
4165
4104
  );
4166
4105
  server.tool(
4167
4106
  "browser_navigate",
4168
- "Navigate your session browser pane to a URL. Opens the pane first if none is open. Only http and https are allowed \u2014 the same restriction the address bar enforces for the person using the app.",
4107
+ "Navigate your session browser pane to a URL. Opens the pane first if none is open. http and https, plus file: urls inside your session's own project or worktree \u2014 anything outside it is refused, so serve files from elsewhere over http instead. The pane carries no claude.ai login, so artifact URLs will not load in it.",
4169
4108
  { url: V.url.describe("URL to open") },
4170
4109
  async (args) => withSession(async (id) => {
4171
4110
  const result = await rpcCall("browser:navigate", {
@@ -4175,6 +4114,20 @@ function registerBrowserTools(server) {
4175
4114
  return { content: [{ type: "text", text: `Navigated to ${result.url}` }] };
4176
4115
  })
4177
4116
  );
4117
+ server.tool(
4118
+ "browser_history",
4119
+ "Step back or forward through your session browser pane's own history \u2014 the same thing the pane's back and forward buttons do. Fails when there is no page to move to, rather than quietly staying put.",
4120
+ { direction: z8.enum(["back", "forward"]).describe("Which way to step") },
4121
+ async (args) => withSession(async (id) => {
4122
+ const result = await rpcCall("browser:history", {
4123
+ sessionId: id,
4124
+ direction: args.direction
4125
+ });
4126
+ return {
4127
+ content: [{ type: "text", text: `Went ${args.direction} to ${result.url}` }]
4128
+ };
4129
+ })
4130
+ );
4178
4131
  }
4179
4132
 
4180
4133
  // src/tools/device.ts
@@ -4397,14 +4350,14 @@ Screen is ${r.screen.width}x${r.screen.height} points. Image pixels are ${r.scal
4397
4350
  function createMcpServer(version) {
4398
4351
  const server = new McpServer({ name: "vorn", version }, { capabilities: { tools: {} } });
4399
4352
  registerConfigTools(server);
4400
- registerProjectTools(server);
4401
- registerTaskTools(server);
4402
4353
  registerSessionTools(server);
4403
- registerWorkflowTools(server);
4404
- registerWorkspaceTools(server);
4405
4354
  registerConnectorTools(server);
4406
4355
  registerBrowserTools(server);
4407
4356
  registerDeviceTools(server);
4357
+ registerProjectTools(server);
4358
+ registerTaskTools(server);
4359
+ registerWorkflowTools(server);
4360
+ registerWorkspaceTools(server);
4408
4361
  return server;
4409
4362
  }
4410
4363
 
@@ -4417,7 +4370,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4417
4370
  console.error = (...args) => _origError("[mcp:error]", ...args);
4418
4371
  async function main() {
4419
4372
  configManager.init();
4420
- const version = true ? "0.6.0" : createRequire(import.meta.url)("../package.json").version;
4373
+ const version = true ? "0.6.1-beta.1" : createRequire(import.meta.url)("../package.json").version;
4421
4374
  const server = createMcpServer(version);
4422
4375
  const transport = new StdioServerTransport();
4423
4376
  await server.connect(transport);