@vornrun/mcp 0.7.0-beta.8 → 0.7.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 +568 -108
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -50,7 +50,34 @@ import fs from "fs";
50
50
  import path from "path";
51
51
 
52
52
  // ../shared/src/protocol.ts
53
+ var CLOSE_UNAUTHENTICATED = 4001;
54
+ var CLOSE_CREDENTIAL_REJECTED = 4002;
55
+ var BOOTSTRAP_ENV_VAR = "SECRET_VORN_BOOTSTRAP_TOKEN";
53
56
  var LOCAL_TOKEN_FILENAME = "local-token";
57
+ var WS_PORT_FILENAME = "ws-port";
58
+
59
+ // ../shared/src/types.ts
60
+ var DEFAULT_WORKSPACE = {
61
+ id: "personal",
62
+ name: "Personal",
63
+ icon: "User",
64
+ iconColor: "#6b7280",
65
+ order: 0
66
+ };
67
+ function isTerminalTaskStatus(status) {
68
+ return status === "done" || status === "cancelled";
69
+ }
70
+ var SDK_FILTER_KEYS = {
71
+ connectorId: "sdkConnectorId",
72
+ version: "sdkVersion",
73
+ icon: "sdkIcon",
74
+ implicit: "implicit"
75
+ };
76
+ var NEVER_BORROWED_ENV = { keys: ["CLAUDECODE"], prefixes: ["CLAUDE_CODE_"] };
77
+ function connectionConnectorId(connection2) {
78
+ const packaged = connection2.filters?.[SDK_FILTER_KEYS.connectorId];
79
+ return typeof packaged === "string" && packaged !== "" ? packaged : connection2.connectorId;
80
+ }
54
81
 
55
82
  // ../server/src/process-utils.ts
56
83
  function getDefaultShell(configured) {
@@ -76,32 +103,13 @@ function findWindowsShell() {
76
103
  if (fs.existsSync(windowsPowerShell)) return windowsPowerShell;
77
104
  return process.env.COMSPEC || "cmd.exe";
78
105
  }
79
- var STRIP_ENV_KEYS = ["CLAUDECODE"];
106
+ var STRIP_ENV_KEYS = NEVER_BORROWED_ENV.keys;
107
+ var STRIP_ENV_PREFIXES = [...NEVER_BORROWED_ENV.prefixes, BOOTSTRAP_ENV_VAR];
80
108
  var STRIP_ENV_KEYS_UPPER = STRIP_ENV_KEYS.map((k) => k.toUpperCase());
81
109
 
82
- // ../shared/src/types.ts
83
- var DEFAULT_WORKSPACE = {
84
- id: "personal",
85
- name: "Personal",
86
- icon: "User",
87
- iconColor: "#6b7280",
88
- order: 0
89
- };
90
- function isTerminalTaskStatus(status) {
91
- return status === "done" || status === "cancelled";
92
- }
93
- var SDK_FILTER_KEYS = {
94
- connectorId: "sdkConnectorId",
95
- version: "sdkVersion",
96
- icon: "sdkIcon"
97
- };
98
- function connectionConnectorId(connection2) {
99
- const packaged = connection2.filters?.[SDK_FILTER_KEYS.connectorId];
100
- return typeof packaged === "string" && packaged !== "" ? packaged : connection2.connectorId;
101
- }
102
-
103
110
  // ../server/src/default-workflows.ts
104
111
  var DEFAULT_TASK_WORKFLOW_ID = "system:default-task-workflow";
112
+ var DEV_SERVER_WORKFLOW_ID = "system:dev-server-on-restore";
105
113
  function buildDefaultTaskWorkflow() {
106
114
  const triggerConfig = {
107
115
  triggerType: "taskStatusChanged",
@@ -141,6 +149,39 @@ function buildDefaultTaskWorkflow() {
141
149
  edges: [{ id: "e1", source: "trigger-1", target: "launch-1" }]
142
150
  };
143
151
  }
152
+ function buildDevServerWorkflow() {
153
+ const triggerConfig = { triggerType: "sessionRestored" };
154
+ const scriptConfig = {
155
+ scriptType: "bash",
156
+ scriptContent: "yarn dev",
157
+ cwd: "{{context.projectPath}}"
158
+ };
159
+ return {
160
+ id: DEV_SERVER_WORKFLOW_ID,
161
+ name: "Bring the dev server back",
162
+ icon: "RotateCcw",
163
+ iconColor: "#c9972a",
164
+ enabled: false,
165
+ workspaceId: "personal",
166
+ nodes: [
167
+ {
168
+ id: "trigger-1",
169
+ type: "trigger",
170
+ label: "When a session is restored",
171
+ position: { x: 0, y: 0 },
172
+ config: triggerConfig
173
+ },
174
+ {
175
+ id: "script-1",
176
+ type: "script",
177
+ label: "Start the dev server",
178
+ position: { x: 0, y: 120 },
179
+ config: scriptConfig
180
+ }
181
+ ],
182
+ edges: [{ id: "e1", source: "trigger-1", target: "script-1" }]
183
+ };
184
+ }
144
185
 
145
186
  // ../server/src/database.ts
146
187
  var DEFAULT_DATA_DIR = path2.join(os.homedir(), ".vorn");
@@ -159,8 +200,8 @@ function getDataDir() {
159
200
  }
160
201
  return resolvedDataDir;
161
202
  }
162
- function initDatabase(dataDir) {
163
- resolvedDataDir = dataDir ?? DEFAULT_DATA_DIR;
203
+ function initDatabase(dataDir2) {
204
+ resolvedDataDir = dataDir2 ?? DEFAULT_DATA_DIR;
164
205
  if (!fs2.existsSync(getDataDir())) {
165
206
  fs2.mkdirSync(getDataDir(), { recursive: true, mode: 448 });
166
207
  }
@@ -185,17 +226,25 @@ function initDatabase(dataDir) {
185
226
  }
186
227
  }
187
228
  function seedSystemDefaults() {
229
+ seedWorkflowOnce(
230
+ "hasSeededDefaultTaskWorkflow",
231
+ DEFAULT_TASK_WORKFLOW_ID,
232
+ buildDefaultTaskWorkflow
233
+ );
234
+ seedWorkflowOnce("hasSeededDevServerWorkflow", DEV_SERVER_WORKFLOW_ID, buildDevServerWorkflow);
235
+ }
236
+ function seedWorkflowOnce(flag, id, build) {
188
237
  const d = getDb();
189
- const flagRow = d.prepare("SELECT value FROM defaults WHERE key = 'hasSeededDefaultTaskWorkflow'").get();
238
+ const flagRow = d.prepare("SELECT value FROM defaults WHERE key = ?").get(flag);
190
239
  if (flagRow) {
191
240
  try {
192
241
  if (JSON.parse(flagRow.value) === true) return;
193
242
  } catch {
194
243
  }
195
244
  }
196
- const existing = d.prepare("SELECT id FROM workflows WHERE id = ?").get(DEFAULT_TASK_WORKFLOW_ID);
245
+ const existing = d.prepare("SELECT id FROM workflows WHERE id = ?").get(id);
197
246
  if (!existing) {
198
- const w = buildDefaultTaskWorkflow();
247
+ const w = build();
199
248
  d.prepare(
200
249
  `INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
201
250
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -212,11 +261,12 @@ function seedSystemDefaults() {
212
261
  w.staggerDelayMs ?? null,
213
262
  w.workspaceId ?? "personal"
214
263
  );
215
- logger_default.info(`[database] Seeded default task workflow (${DEFAULT_TASK_WORKFLOW_ID})`);
264
+ logger_default.info(`[database] Seeded workflow ${id}`);
216
265
  }
217
- d.prepare(
218
- "INSERT OR REPLACE INTO defaults (key, value) VALUES ('hasSeededDefaultTaskWorkflow', ?)"
219
- ).run(JSON.stringify(true));
266
+ d.prepare("INSERT OR REPLACE INTO defaults (key, value) VALUES (?, ?)").run(
267
+ flag,
268
+ JSON.stringify(true)
269
+ );
220
270
  }
221
271
  function recoverCorruptDatabase() {
222
272
  try {
@@ -389,7 +439,8 @@ function createSchema() {
389
439
  saved_at INTEGER,
390
440
  sort_order INTEGER NOT NULL DEFAULT 0,
391
441
  worktree_name TEXT,
392
- agent_session_id TEXT
442
+ agent_session_id TEXT,
443
+ renamed_by_person INTEGER
393
444
  );
394
445
 
395
446
  CREATE TABLE IF NOT EXISTS schedule_log (
@@ -413,6 +464,16 @@ function createSchema() {
413
464
  "order" INTEGER NOT NULL DEFAULT 0
414
465
  );
415
466
 
467
+ CREATE TABLE IF NOT EXISTS session_groups (
468
+ id TEXT PRIMARY KEY,
469
+ name TEXT NOT NULL,
470
+ icon TEXT,
471
+ icon_color TEXT,
472
+ "order" INTEGER NOT NULL DEFAULT 0,
473
+ workspace_id TEXT NOT NULL DEFAULT 'personal',
474
+ row_revision INTEGER NOT NULL DEFAULT 0
475
+ );
476
+
416
477
  CREATE TABLE IF NOT EXISTS workflow_runs (
417
478
  id TEXT PRIMARY KEY,
418
479
  workflow_id TEXT NOT NULL,
@@ -447,6 +508,10 @@ function createSchema() {
447
508
  output TEXT,
448
509
  structured_output TEXT,
449
510
  iteration INTEGER,
511
+ worktree_path TEXT,
512
+ worktree_name TEXT,
513
+ worktree_origin TEXT,
514
+ waiting_for TEXT,
450
515
  FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
451
516
  );
452
517
 
@@ -687,7 +752,9 @@ function migrateSchema(d) {
687
752
  last_sync_at TEXT,
688
753
  last_sync_error TEXT,
689
754
  sync_cursor TEXT,
690
- created_at TEXT NOT NULL
755
+ created_at TEXT NOT NULL,
756
+ signed_in_as TEXT,
757
+ signed_in_at TEXT
691
758
  )
692
759
  `);
693
760
  d.exec(`
@@ -900,11 +967,85 @@ function migrateSchema(d) {
900
967
  })();
901
968
  logger_default.info("[database] migrated schema to version 16 (shell working directory)");
902
969
  }
970
+ if (version < 17) {
971
+ d.transaction(() => {
972
+ const derived = `(
973
+ SELECT json_extract(sc.filters, '$.sdkConnectorId')
974
+ FROM task_source_links tsl
975
+ JOIN source_connections sc ON sc.id = tsl.connection_id
976
+ WHERE tsl.task_id = tasks.id
977
+ )`;
978
+ d.exec(`
979
+ UPDATE tasks
980
+ SET source_connector_id = ${derived}
981
+ WHERE source_connector_id = 'mcp' AND ${derived} IS NOT NULL
982
+ `);
983
+ d.exec(`
984
+ UPDATE task_source_links
985
+ SET connector_id = (
986
+ SELECT json_extract(sc.filters, '$.sdkConnectorId')
987
+ FROM source_connections sc
988
+ WHERE sc.id = task_source_links.connection_id
989
+ )
990
+ WHERE connector_id = 'mcp'
991
+ AND (
992
+ SELECT json_extract(sc.filters, '$.sdkConnectorId')
993
+ FROM source_connections sc
994
+ WHERE sc.id = task_source_links.connection_id
995
+ ) IS NOT NULL
996
+ `);
997
+ d.prepare(
998
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '17')"
999
+ ).run();
1000
+ })();
1001
+ logger_default.info("[database] migrated schema to version 17 (packaged connector task ids)");
1002
+ }
1003
+ if (version < 18) {
1004
+ d.transaction(() => {
1005
+ const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
1006
+ if (!sessionCols.some((c) => c.name === "group_id")) {
1007
+ d.exec("ALTER TABLE sessions ADD COLUMN group_id TEXT");
1008
+ }
1009
+ d.prepare(
1010
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '18')"
1011
+ ).run();
1012
+ })();
1013
+ logger_default.info("[database] migrated schema to version 18 (session groups)");
1014
+ }
1015
+ if (version < 19) {
1016
+ d.transaction(() => {
1017
+ const cols = d.prepare("PRAGMA table_info(workflow_run_nodes)").all();
1018
+ for (const column of ["worktree_path", "worktree_name", "worktree_origin"]) {
1019
+ if (!cols.some((c) => c.name === column)) {
1020
+ d.exec(`ALTER TABLE workflow_run_nodes ADD COLUMN ${column} TEXT`);
1021
+ }
1022
+ }
1023
+ d.prepare(
1024
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '19')"
1025
+ ).run();
1026
+ })();
1027
+ logger_default.info("[database] migrated schema to version 19 (worktrees on run steps)");
1028
+ }
1029
+ if (version < 20) {
1030
+ d.transaction(() => {
1031
+ const cols = d.prepare("PRAGMA table_info(source_connections)").all();
1032
+ for (const column of ["signed_in_as", "signed_in_at"]) {
1033
+ if (!cols.some((c) => c.name === column)) {
1034
+ d.exec(`ALTER TABLE source_connections ADD COLUMN ${column} TEXT`);
1035
+ }
1036
+ }
1037
+ d.prepare(
1038
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '20')"
1039
+ ).run();
1040
+ })();
1041
+ logger_default.info("[database] migrated schema to version 20 (who a connection is signed in as)");
1042
+ }
903
1043
  }
904
1044
  var REVISIONED_TABLES = [
905
1045
  "projects",
906
1046
  "tasks",
907
1047
  "workspaces",
1048
+ "session_groups",
908
1049
  "remote_hosts",
909
1050
  "agent_commands"
910
1051
  ];
@@ -941,9 +1082,15 @@ function verifySchema(d) {
941
1082
  column: "sort_order",
942
1083
  ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
943
1084
  },
1085
+ { column: "group_id", ddl: "ALTER TABLE sessions ADD COLUMN group_id TEXT" },
944
1086
  { column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
945
1087
  { column: "agent_session_id", ddl: "ALTER TABLE sessions ADD COLUMN agent_session_id TEXT" },
946
- { column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd TEXT" }
1088
+ { column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd TEXT" },
1089
+ { column: "head_commit", ddl: "ALTER TABLE sessions ADD COLUMN head_commit TEXT" },
1090
+ {
1091
+ column: "renamed_by_person",
1092
+ ddl: "ALTER TABLE sessions ADD COLUMN renamed_by_person INTEGER"
1093
+ }
947
1094
  ],
948
1095
  agent_commands: [
949
1096
  {
@@ -977,6 +1124,7 @@ function verifySchema(d) {
977
1124
  }
978
1125
  ],
979
1126
  workflow_run_nodes: [
1127
+ { column: "waiting_for", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN waiting_for TEXT" },
980
1128
  { column: "agent_type", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN agent_type TEXT" },
981
1129
  {
982
1130
  column: "project_name",
@@ -1056,6 +1204,7 @@ function loadConfig() {
1056
1204
  const remoteHosts = loadRemoteHosts(d);
1057
1205
  const tasks = loadTasks(d);
1058
1206
  const workspaces = loadWorkspaces(d);
1207
+ const sessionGroups = loadSessionGroups(d);
1059
1208
  return {
1060
1209
  version: 1,
1061
1210
  revision: readConfigRevision(d),
@@ -1065,7 +1214,8 @@ function loadConfig() {
1065
1214
  workflows,
1066
1215
  remoteHosts,
1067
1216
  tasks,
1068
- workspaces
1217
+ workspaces,
1218
+ sessionGroups
1069
1219
  };
1070
1220
  }
1071
1221
  function loadDefaults(d) {
@@ -1098,6 +1248,8 @@ function loadDefaults(d) {
1098
1248
  // terminal drew and waits. There is nothing to ask, and leaving it off made
1099
1249
  // the whole thing invisible unless somebody went looking for a toggle.
1100
1250
  reopenSessions: map.reopenSessions ?? true,
1251
+ // Off by default: nothing starts itself because someone installed an app.
1252
+ startAtLogin: map.startAtLogin ?? false,
1101
1253
  // Saving iterates over every key in defaults, but loading is this explicit
1102
1254
  // list — so a key missing here round-trips to nothing and its feature is
1103
1255
  // silently inert.
@@ -1150,6 +1302,9 @@ function loadDefaults(d) {
1150
1302
  ...map.headlessRetentionMinutes !== void 0 && {
1151
1303
  headlessRetentionMinutes: map.headlessRetentionMinutes
1152
1304
  },
1305
+ ...map.hasSeededDevServerWorkflow !== void 0 && {
1306
+ hasSeededDevServerWorkflow: map.hasSeededDevServerWorkflow
1307
+ },
1153
1308
  ...map.hasSeededDefaultTaskWorkflow !== void 0 && {
1154
1309
  hasSeededDefaultTaskWorkflow: map.hasSeededDefaultTaskWorkflow
1155
1310
  },
@@ -1213,6 +1368,10 @@ function loadTasks(d) {
1213
1368
  const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
1214
1369
  return rows.map(rowToTask);
1215
1370
  }
1371
+ function loadSessionGroups(d) {
1372
+ const rows = d.prepare('SELECT * FROM session_groups ORDER BY "order"').all();
1373
+ return rows.map(rowToSessionGroup);
1374
+ }
1216
1375
  function loadWorkspaces(d) {
1217
1376
  const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
1218
1377
  return rows.map(rowToWorkspace);
@@ -1452,6 +1611,36 @@ function saveConfig(config) {
1452
1611
  for (const ws of workspaces) {
1453
1612
  insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order, revision);
1454
1613
  }
1614
+ const sessionGroups = config.sessionGroups ?? [];
1615
+ pruneMissing(
1616
+ d,
1617
+ "session_groups",
1618
+ "id",
1619
+ sessionGroups.map((g) => g.id),
1620
+ baseRevision
1621
+ );
1622
+ const insertSessionGroup = d.prepare(
1623
+ `INSERT INTO session_groups (id, name, icon, icon_color, "order", workspace_id, row_revision)
1624
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1625
+ ON CONFLICT(id) DO UPDATE SET
1626
+ row_revision = excluded.row_revision,
1627
+ name = excluded.name,
1628
+ icon = excluded.icon,
1629
+ icon_color = excluded.icon_color,
1630
+ "order" = excluded."order",
1631
+ workspace_id = excluded.workspace_id`
1632
+ );
1633
+ for (const g of sessionGroups) {
1634
+ insertSessionGroup.run(
1635
+ g.id,
1636
+ g.name,
1637
+ g.icon ?? null,
1638
+ g.iconColor ?? null,
1639
+ g.order,
1640
+ g.workspaceId,
1641
+ revision
1642
+ );
1643
+ }
1455
1644
  d.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)").run(
1456
1645
  CONFIG_REVISION_KEY,
1457
1646
  String(revision)
@@ -1509,6 +1698,16 @@ function rowToWorkflow(r) {
1509
1698
  workspaceId: r.workspace_id ?? "personal"
1510
1699
  };
1511
1700
  }
1701
+ function rowToSessionGroup(r) {
1702
+ return {
1703
+ id: r.id,
1704
+ name: r.name,
1705
+ ...r.icon != null && { icon: r.icon },
1706
+ ...r.icon_color != null && { iconColor: r.icon_color },
1707
+ order: r.order,
1708
+ workspaceId: r.workspace_id ?? "personal"
1709
+ };
1710
+ }
1512
1711
  function rowToWorkspace(r) {
1513
1712
  return {
1514
1713
  id: r.id,
@@ -1525,8 +1724,8 @@ var ConfigManager = class {
1525
1724
  dbWatcher = null;
1526
1725
  debounceTimer = null;
1527
1726
  cachedConfig = null;
1528
- init(dataDir) {
1529
- initDatabase(dataDir);
1727
+ init(dataDir2) {
1728
+ initDatabase(dataDir2);
1530
1729
  }
1531
1730
  close() {
1532
1731
  this.stopWatchingDb();
@@ -1644,33 +1843,47 @@ var V = {
1644
1843
  url: safeUrl
1645
1844
  };
1646
1845
 
1647
- // src/ws-client.ts
1846
+ // ../server/src/rpc-client.ts
1648
1847
  import fs4 from "fs";
1649
1848
  import path3 from "path";
1650
1849
  import os2 from "os";
1651
1850
  import { execFileSync as execFileSync2 } from "child_process";
1652
1851
  import { WebSocket } from "ws";
1653
- var DATA_DIR = process.env.VORN_DATA_DIR || path3.join(os2.homedir(), ".vorn");
1654
- var PORT_FILE = path3.join(DATA_DIR, "ws-port");
1655
- var LOCAL_TOKEN_FILE = path3.join(DATA_DIR, LOCAL_TOKEN_FILENAME);
1656
- var TOKEN_FILE_MISSING_MSG = `Vorn local credential not found (${LOCAL_TOKEN_FILE}).
1852
+ var dataDirOverride;
1853
+ function named(value) {
1854
+ return value?.trim() ? value : void 0;
1855
+ }
1856
+ function dataDir() {
1857
+ return dataDirOverride ?? named(process.env.VORN_DATA_DIR) ?? path3.join(os2.homedir(), ".vorn");
1858
+ }
1859
+ function portFile() {
1860
+ return path3.join(dataDir(), WS_PORT_FILENAME);
1861
+ }
1862
+ function localTokenFile() {
1863
+ return path3.join(dataDir(), LOCAL_TOKEN_FILENAME);
1864
+ }
1865
+ function tokenFileMissingMessage() {
1866
+ return `Vorn local credential not found (${localTokenFile()}).
1657
1867
  The server writes it on startup and removes it on shutdown, so this usually means
1658
- Vorn is not running. Start Vorn (or \`vorn-server serve\`) and try again.
1659
- If the server runs with --data-dir, set VORN_DATA_DIR to the same directory.`;
1868
+ Vorn is not running. Start Vorn (or \`vorn server serve\`) and try again.
1869
+ If the server runs with --data-dir, pass the same --data-dir here, or set
1870
+ VORN_DATA_DIR to that directory -- which is how anything that is not the CLI,
1871
+ MCP included, reaches a server that moved.`;
1872
+ }
1660
1873
  function readLocalToken() {
1661
1874
  try {
1662
- const token = fs4.readFileSync(LOCAL_TOKEN_FILE, "utf-8").trim();
1875
+ const token = fs4.readFileSync(localTokenFile(), "utf-8").trim();
1663
1876
  if (!token) throw new Error("empty");
1664
1877
  return token;
1665
1878
  } catch {
1666
- throw new Error(TOKEN_FILE_MISSING_MSG);
1879
+ throw new Error(tokenFileMissingMessage());
1667
1880
  }
1668
1881
  }
1669
1882
  function connection() {
1670
1883
  const result = readPort();
1671
1884
  if (!result.port) {
1672
1885
  const reason = "reason" in result ? result.reason : "missing";
1673
- throw new Error(reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1886
+ throw new Error(reason === "invalid" ? portFileInvalidMessage() : portFileMissingMessage());
1674
1887
  }
1675
1888
  return {
1676
1889
  url: `ws://127.0.0.1:${result.port}/ws`,
@@ -1679,21 +1892,42 @@ function connection() {
1679
1892
  }
1680
1893
  var TIMEOUT_MS = 1e4;
1681
1894
  var IS_WIN = process.platform === "win32";
1682
- var PORT_FILE_MISSING_MSG = IS_WIN ? `Vorn port file not found (~/.vorn/ws-port).
1895
+ function portFileMissingMessage() {
1896
+ const file = portFile();
1897
+ return IS_WIN ? `Vorn port file not found (${file}).
1683
1898
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1684
- To fix, find the Vorn process and its listening port:
1685
- powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort"
1899
+ To fix, in PowerShell, find the Vorn process and its listening port:
1900
+ Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort
1686
1901
  Then write the WS port to the file:
1687
- echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vorn\\ws-port
1688
- Or restart Vorn to regenerate it.` : `Vorn port file not found (~/.vorn/ws-port).
1902
+ '{"port":<PORT>,"pid":<PID>}' | Set-Content -Path "${file}"
1903
+ Or restart Vorn to regenerate it.` : `Vorn port file not found (${file}).
1689
1904
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1690
1905
  To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
1691
1906
  Then write the WS port (the one on *:<port>) to the file:
1692
- echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
1907
+ echo '{"port":<PORT>,"pid":<PID>}' > "${file}"
1693
1908
  Or restart Vorn to regenerate it.`;
1694
- var PORT_FILE_INVALID_MSG = `Vorn port file exists but contains invalid data (~/.vorn/ws-port).
1909
+ }
1910
+ function portFileInvalidMessage() {
1911
+ const file = portFile();
1912
+ return `Vorn port file exists but contains invalid data (${file}).
1695
1913
  Delete it and restart Vorn, or overwrite it with the correct port:
1696
- ${IS_WIN ? "del %USERPROFILE%\\.vorn\\ws-port" : "rm ~/.vorn/ws-port"}`;
1914
+ ${IS_WIN ? `Remove-Item "${file}"` : `rm "${file}"`}`;
1915
+ }
1916
+ function closedBeforeAnswering(code) {
1917
+ if (code === CLOSE_UNAUTHENTICATED || code === CLOSE_CREDENTIAL_REJECTED) {
1918
+ return `A Vorn server on this port refused the credential in ${localTokenFile()}.
1919
+ Another server is listening on it with its own data directory, which a dev build
1920
+ running beside the app does. Point at that one with --data-dir (or VORN_DATA_DIR),
1921
+ or stop it.`;
1922
+ }
1923
+ return `The server closed the connection before answering (code ${code}).`;
1924
+ }
1925
+ function explain(message) {
1926
+ if (!message.startsWith("Method not found:")) return message;
1927
+ const method = message.slice("Method not found:".length).trim();
1928
+ return `This server does not have ${method}, so it is older than the vorn command asking for it.
1929
+ Restart Vorn to pick up the newer server, or run this against the matching build.`;
1930
+ }
1697
1931
  var rpcId = 0;
1698
1932
  var cachedPort = null;
1699
1933
  var cacheTimestamp = 0;
@@ -1746,7 +1980,11 @@ function discoverPort() {
1746
1980
  }
1747
1981
  return null;
1748
1982
  }
1983
+ function discoveryAllowed() {
1984
+ return dataDirOverride === void 0 && named(process.env.VORN_DATA_DIR) === void 0;
1985
+ }
1749
1986
  function discoverAndHeal() {
1987
+ if (!discoveryAllowed()) return { port: null, reason: "missing" };
1750
1988
  const now = Date.now();
1751
1989
  if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1752
1990
  const discovered = discoverPort();
@@ -1754,8 +1992,8 @@ function discoverAndHeal() {
1754
1992
  cacheTimestamp = now;
1755
1993
  if (discovered) {
1756
1994
  try {
1757
- fs4.mkdirSync(path3.dirname(PORT_FILE), { recursive: true });
1758
- fs4.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
1995
+ fs4.mkdirSync(dataDir(), { recursive: true });
1996
+ fs4.writeFileSync(portFile(), JSON.stringify({ port: discovered }), "utf-8");
1759
1997
  } catch {
1760
1998
  }
1761
1999
  return { port: discovered };
@@ -1764,7 +2002,7 @@ function discoverAndHeal() {
1764
2002
  }
1765
2003
  function readPort() {
1766
2004
  try {
1767
- const raw = fs4.readFileSync(PORT_FILE, "utf-8").trim();
2005
+ const raw = fs4.readFileSync(portFile(), "utf-8").trim();
1768
2006
  if (!raw) return { port: null, reason: "invalid" };
1769
2007
  if (raw.startsWith("{")) {
1770
2008
  const parsed = JSON.parse(raw);
@@ -1806,15 +2044,19 @@ async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
1806
2044
  const msg = JSON.parse(raw.toString());
1807
2045
  if (msg.id !== id) return;
1808
2046
  clearTimeout(timer);
1809
- ws.close();
1810
2047
  if (msg.error) {
1811
- reject(new Error(msg.error.message));
2048
+ reject(new Error(explain(msg.error.message)));
1812
2049
  } else {
1813
2050
  resolve(msg.result);
1814
2051
  }
2052
+ ws.close();
1815
2053
  } catch {
1816
2054
  }
1817
2055
  });
2056
+ ws.on("close", (code) => {
2057
+ clearTimeout(timer);
2058
+ reject(new Error(closedBeforeAnswering(code)));
2059
+ });
1818
2060
  ws.on("error", (err) => {
1819
2061
  clearTimeout(timer);
1820
2062
  reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
@@ -1943,6 +2185,9 @@ async function listWorkflowRunsByTask(taskId, limit = 20) {
1943
2185
  limit
1944
2186
  });
1945
2187
  }
2188
+ async function listRunsWithWaitingGates() {
2189
+ return rpcCall("workflowRun:listWaiting");
2190
+ }
1946
2191
  async function listAllWorkflowRuns(workspaceId, limit = 50) {
1947
2192
  return rpcCall("workflowRun:listAll", {
1948
2193
  workspaceId,
@@ -2713,6 +2958,13 @@ function registerSessionTools(server) {
2713
2958
 
2714
2959
  // src/tools/workflows.ts
2715
2960
  import crypto3 from "crypto";
2961
+
2962
+ // ../shared/src/workflow-graph.ts
2963
+ function isSignInWait(state) {
2964
+ return state.status === "waiting" && state.waitingFor === "signIn";
2965
+ }
2966
+
2967
+ // src/tools/workflows.ts
2716
2968
  import { z as z5 } from "zod";
2717
2969
 
2718
2970
  // ../shared/src/workflow-portability.ts
@@ -2743,16 +2995,18 @@ function boundConnectionKey(node, config) {
2743
2995
  if (node.type === "trigger" && config.triggerType === "connectorPoll") return "connectionId";
2744
2996
  if (node.type === "callConnectorAction") return "connectionId";
2745
2997
  if (node.type === "httpRequest") return "profileConnectionId";
2998
+ if (node.type === "script") return "secretsFrom";
2746
2999
  return null;
2747
3000
  }
3001
+ var OPTIONAL_CONNECTION_KEYS = /* @__PURE__ */ new Set(["profileConnectionId", "secretsFrom"]);
2748
3002
  function resolveRequirement(requirement, connections) {
2749
3003
  const candidates = connections.filter(
2750
3004
  (connection2) => requirement.kind === "httpProfile" ? connectorOf(connection2) === HTTP_PROFILE_CONNECTOR : requirement.connectorId !== "" && connectorOf(connection2) === requirement.connectorId
2751
3005
  );
2752
3006
  if (candidates.length === 0) return void 0;
2753
3007
  if (requirement.name !== "") {
2754
- const named = candidates.filter((connection2) => connection2.name === requirement.name);
2755
- if (named.length === 1) return named[0].id;
3008
+ const named2 = candidates.filter((connection2) => connection2.name === requirement.name);
3009
+ if (named2.length === 1) return named2[0].id;
2756
3010
  }
2757
3011
  return candidates.length === 1 ? candidates[0].id : void 0;
2758
3012
  }
@@ -2776,19 +3030,22 @@ function toPortable(workflow, projectPath, connections = []) {
2776
3030
  }
2777
3031
  const key = boundConnectionKey(node, config);
2778
3032
  const bound = key === null ? "" : config[key];
2779
- if (key !== null && typeof bound === "string" && bound !== "") {
3033
+ const unbound = key !== null && !OPTIONAL_CONNECTION_KEYS.has(key) && bound === "";
3034
+ if (key !== null && (typeof bound === "string" && bound !== "" || unbound)) {
2780
3035
  const source2 = connections.find((connection2) => connection2.id === bound);
2781
3036
  const event = config.event;
3037
+ const declared = config.connectorId;
2782
3038
  requires.push(
2783
3039
  key === "profileConnectionId" ? { kind: "httpProfile", nodeId: node.id, name: source2?.name ?? "" } : {
2784
3040
  kind: "connection",
2785
3041
  nodeId: node.id,
2786
- connectorId: source2 ? connectorOf(source2) : "",
3042
+ connectorId: source2 ? connectorOf(source2) : typeof declared === "string" ? declared : "",
2787
3043
  name: source2?.name ?? "",
2788
- ...typeof event === "string" && event !== "" && { event }
3044
+ ...typeof event === "string" && event !== "" && { event },
3045
+ ...key === "secretsFrom" && { key }
2789
3046
  }
2790
3047
  );
2791
- if (key === "profileConnectionId") delete config[key];
3048
+ if (OPTIONAL_CONNECTION_KEYS.has(key)) delete config[key];
2792
3049
  else config[key] = "";
2793
3050
  }
2794
3051
  return { ...node, config };
@@ -2819,9 +3076,12 @@ function replacePath(value, projectPath) {
2819
3076
  function unresolvedRequirements(portable, connections) {
2820
3077
  const present = new Set(portable.nodes.map((node) => node.id));
2821
3078
  return (portable.requires ?? []).filter(
2822
- (requirement) => present.has(requirement.nodeId) && resolveRequirement(requirement, connections) === void 0
3079
+ (requirement) => present.has(requirement.nodeId) && (bindsOnlyByHand(requirement) || resolveRequirement(requirement, connections) === void 0)
2823
3080
  );
2824
3081
  }
3082
+ function bindsOnlyByHand(requirement) {
3083
+ return requirement.kind === "connection" && requirement.key === "secretsFrom";
3084
+ }
2825
3085
  function fromPortable(portable, bundle, project, connections = [], mintToken = () => crypto.randomUUID()) {
2826
3086
  const bindings = /* @__PURE__ */ new Map();
2827
3087
  for (const requirement of portable.requires ?? []) {
@@ -2829,15 +3089,16 @@ function fromPortable(portable, bundle, project, connections = [], mintToken = (
2829
3089
  }
2830
3090
  const nodes = portable.nodes.map((node) => {
2831
3091
  const config = { ...node.config };
2832
- for (const [key, value] of Object.entries(config)) {
3092
+ const key = boundConnectionKey(node, config);
3093
+ if (key !== null && OPTIONAL_CONNECTION_KEYS.has(key)) delete config[key];
3094
+ for (const [key2, value] of Object.entries(config)) {
2833
3095
  if (typeof value !== "string") continue;
2834
- config[key] = value.split(PROJECT_PATH_TOKEN).join(project.path.replace(/[/\\]+$/, "")).split(PROJECT_NAME_TOKEN).join(project.name);
3096
+ config[key2] = value.split(PROJECT_PATH_TOKEN).join(project.path.replace(/[/\\]+$/, "")).split(PROJECT_NAME_TOKEN).join(project.name);
2835
3097
  }
2836
3098
  for (const requirement of bindings.get(node.id) ?? []) {
3099
+ if (bindsOnlyByHand(requirement) || key === null) continue;
2837
3100
  const resolved = resolveRequirement(requirement, connections);
2838
- if (resolved === void 0) continue;
2839
- if (requirement.kind === "httpProfile") config.profileConnectionId = resolved;
2840
- else config.connectionId = resolved;
3101
+ if (resolved !== void 0) config[key] = resolved;
2841
3102
  }
2842
3103
  if (node.type === "trigger" && config.triggerType === "webhook" && !config.token) {
2843
3104
  config.token = mintToken();
@@ -3159,6 +3420,64 @@ function resolveWorkflowId(args) {
3159
3420
  }
3160
3421
  return { id };
3161
3422
  }
3423
+ function approvalNode(workflow, nodeId) {
3424
+ return workflow?.nodes.find((n) => n.id === nodeId && n.type === "approval");
3425
+ }
3426
+ function askedBy(node) {
3427
+ return node?.config?.message?.trim() || void 0;
3428
+ }
3429
+ function gateMessage(workflow, nodeId) {
3430
+ return askedBy(approvalNode(workflow, nodeId));
3431
+ }
3432
+ function annotateWaitingGates(runs, workflows) {
3433
+ return runs.map((run) => {
3434
+ if (!run.nodeStates.some((n) => n.status === "waiting")) return run;
3435
+ const workflow = workflows.find((w) => w.id === run.workflowId);
3436
+ return {
3437
+ ...run,
3438
+ nodeStates: run.nodeStates.map((state) => {
3439
+ if (state.status !== "waiting") return state;
3440
+ if (isSignInWait(state)) {
3441
+ return {
3442
+ ...state,
3443
+ asks: "Sign in to its connection in the Vorn app, and this step runs again"
3444
+ };
3445
+ }
3446
+ const asks = gateMessage(workflow, state.nodeId);
3447
+ return asks ? { ...state, asks } : state;
3448
+ })
3449
+ };
3450
+ });
3451
+ }
3452
+ async function runById(runId) {
3453
+ const recent = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === runId);
3454
+ if (recent) return recent;
3455
+ return (await listRunsWithWaitingGates()).find((r) => r.runId === runId);
3456
+ }
3457
+ function resolveGateTarget(run, nodeId, decision = "approve") {
3458
+ const parked = run.nodeStates.filter((n) => n.status === "waiting");
3459
+ const answerable = parked.filter((n) => decision === "reject" || !isSignInWait(n));
3460
+ const waiting = answerable.map((n) => n.nodeId);
3461
+ const signIns = parked.filter((n) => !answerable.includes(n)).map((n) => n.nodeId);
3462
+ if (nodeId) {
3463
+ if (signIns.includes(nodeId)) {
3464
+ return {
3465
+ error: `node "${nodeId}" is waiting for a sign-in in the Vorn app, not for an approval`
3466
+ };
3467
+ }
3468
+ if (waiting.includes(nodeId)) return { nodeId };
3469
+ return {
3470
+ error: waiting.length ? `node "${nodeId}" is not waiting. Waiting: ${waiting.join(", ")}` : `node "${nodeId}" is not waiting, and neither is any other node in this run`
3471
+ };
3472
+ }
3473
+ if (waiting.length === 1) return { nodeId: waiting[0] };
3474
+ if (waiting.length === 0) {
3475
+ return {
3476
+ error: signIns.length ? "this run is waiting for a sign-in in the Vorn app, not for an approval" : "no node in this run is waiting on a gate"
3477
+ };
3478
+ }
3479
+ return { error: `${waiting.length} nodes are waiting \u2014 pass node_id: ${waiting.join(", ")}` };
3480
+ }
3162
3481
  async function listPortableConnections() {
3163
3482
  try {
3164
3483
  return await rpcCall("connection:list", { connectorId: void 0 });
@@ -3312,7 +3631,7 @@ function registerWorkflowTools(server) {
3312
3631
  );
3313
3632
  server.tool(
3314
3633
  "list_workflow_runs",
3315
- "List workflow execution history. Filter by workflow_id or task_id.",
3634
+ "List workflow execution history. Filter by workflow_id or task_id; with neither, lists the runs parked on an approval gate, each waiting node saying what it asks.",
3316
3635
  {
3317
3636
  workflow_id: V.id.optional().describe("Filter by workflow ID"),
3318
3637
  task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
@@ -3325,28 +3644,28 @@ function registerWorkflowTools(server) {
3325
3644
  isError: true
3326
3645
  };
3327
3646
  }
3647
+ const withGates = async (runs) => runs.some((r) => r.nodeStates.some((n) => n.status === "waiting")) ? annotateWaitingGates(runs, await dbListWorkflows()) : runs;
3328
3648
  if (args.task_id) {
3329
- const runs = await listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
3649
+ const runs = await withGates(await listWorkflowRunsByTask(args.task_id, args.limit ?? 20));
3330
3650
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3331
3651
  }
3332
3652
  if (args.workflow_id) {
3333
- const runs = await listWorkflowRuns(args.workflow_id, args.limit ?? 20);
3653
+ const runs = await withGates(await listWorkflowRuns(args.workflow_id, args.limit ?? 20));
3334
3654
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3335
3655
  }
3336
- return {
3337
- content: [{ type: "text", text: "Error: provide either workflow_id or task_id" }],
3338
- isError: true
3339
- };
3656
+ const parked = (await listRunsWithWaitingGates()).slice(0, args.limit ?? 20);
3657
+ const waiting = await withGates(parked);
3658
+ return { content: [{ type: "text", text: JSON.stringify(waiting, null, 2) }] };
3340
3659
  }
3341
3660
  );
3342
3661
  server.tool(
3343
3662
  "stop_workflow_run",
3344
- "Stop a workflow run that is still going, including one parked on an approval gate. Kills the agents it started, marks its unfinished nodes, and closes the run as cancelled. Requires the Vorn app to be running. A workflow will not start a new run while an old one sits waiting for approval, so this is how you clear that.",
3663
+ "Stop a workflow run that is still going, including one parked on an approval gate. Kills the agents it started, marks its unfinished nodes, and closes the run as cancelled. Requires the Vorn app to be running.",
3345
3664
  {
3346
3665
  run_id: V.id.describe("Run ID (from list_workflow_runs)")
3347
3666
  },
3348
3667
  async (args) => {
3349
- const run = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === args.run_id);
3668
+ const run = await runById(args.run_id);
3350
3669
  if (!run) {
3351
3670
  return {
3352
3671
  content: [
@@ -3391,6 +3710,74 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3391
3710
  };
3392
3711
  }
3393
3712
  );
3713
+ server.tool(
3714
+ "resolve_gate",
3715
+ "Approve or reject the approval gate a workflow run is parked on, the way the Vorn app does. Requires the Vorn app to be running: the decision is broadcast, and the instance holding the run is what resumes it. Read what is being approved first \u2014 list_workflow_runs names the waiting node and what it asks.",
3716
+ {
3717
+ run_id: V.id.describe("Run ID (from list_workflow_runs)"),
3718
+ decision: z5.enum(["approve", "reject"]).describe("approve lets the run go on; reject ends it"),
3719
+ node_id: V.id.optional().describe("The waiting node, when a run has more than one gate open")
3720
+ },
3721
+ async (args) => {
3722
+ const run = await runById(args.run_id);
3723
+ if (!run) {
3724
+ return {
3725
+ content: [
3726
+ {
3727
+ type: "text",
3728
+ text: `Error: no run "${args.run_id}" in the recent history, and none parked on a gate. Check list_workflow_runs.`
3729
+ }
3730
+ ],
3731
+ isError: true
3732
+ };
3733
+ }
3734
+ if (run.status !== "running") {
3735
+ return {
3736
+ content: [
3737
+ {
3738
+ type: "text",
3739
+ text: `Run ${args.run_id} already finished (${run.status}) \u2014 no gate to answer.`
3740
+ }
3741
+ ]
3742
+ };
3743
+ }
3744
+ const target = resolveGateTarget(run, args.node_id, args.decision);
3745
+ if ("error" in target) {
3746
+ return {
3747
+ content: [{ type: "text", text: `Error: ${target.error}` }],
3748
+ isError: true
3749
+ };
3750
+ }
3751
+ const workflow = (await dbListWorkflows()).find((w) => w.id === run.workflowId);
3752
+ const gateNode = approvalNode(workflow, target.nodeId);
3753
+ const asked = askedBy(gateNode);
3754
+ try {
3755
+ await rpcCall("workflow:resolveGate", {
3756
+ runId: args.run_id,
3757
+ nodeId: target.nodeId,
3758
+ decision: args.decision
3759
+ });
3760
+ } catch (err) {
3761
+ return {
3762
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
3763
+ isError: true
3764
+ };
3765
+ }
3766
+ const gate = gateNode?.label ?? target.nodeId;
3767
+ return {
3768
+ content: [
3769
+ {
3770
+ type: "text",
3771
+ text: `${args.decision === "approve" ? "Approved" : "Rejected"} "${gate}" on run ${args.run_id}${run.workflowName ? ` of "${run.workflowName}"` : ""}.${asked ? `
3772
+
3773
+ What it asked: ${asked}` : ""}
3774
+
3775
+ The decision went out; the instance holding the run acts on it, so a desktop has to be open. Confirm with list_workflow_runs.`
3776
+ }
3777
+ ]
3778
+ };
3779
+ }
3780
+ );
3394
3781
  server.tool(
3395
3782
  "get_workflow_schedule",
3396
3783
  "Get scheduler info for a workflow: execution log or next scheduled run. Requires the Vorn app to be running.",
@@ -3431,7 +3818,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3431
3818
  );
3432
3819
  server.tool(
3433
3820
  "execute_workflow",
3434
- "Run a workflow now, as if triggered manually. Supply values for any parameters the workflow declares (see the trigger node's inputs); declared defaults fill in anything omitted. Requires the Vorn app to be running. Returns as soon as the run is queued \u2014 poll list_workflow_runs for the outcome.",
3821
+ "Run a workflow now, as if triggered manually. Supply values for any parameters the workflow declares (see the trigger node's inputs); declared defaults fill in anything omitted. Requires the Vorn app to be running. Returns as soon as the run is queued \u2014 poll list_workflow_runs for the outcome. Runs of one workflow go side by side; only a run repeating one started in the last ten seconds with the same inputs is refused as a duplicate.",
3435
3822
  {
3436
3823
  workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
3437
3824
  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>}})")
@@ -3750,27 +4137,43 @@ var failure = (message) => ({
3750
4137
  function summarize(entry) {
3751
4138
  return { type: entry.type, label: entry.label };
3752
4139
  }
4140
+ function andList(values) {
4141
+ if (values.length <= 1) return values[0] ?? "";
4142
+ return `${values.slice(0, -1).join(", ")} and ${values[values.length - 1]}`;
4143
+ }
4144
+ function contributionSummary(contributes) {
4145
+ const named2 = (entries) => (entries ?? []).map((entry) => ({ id: entry.id, title: entry.title }));
4146
+ return {
4147
+ panes: named2(contributes?.panes),
4148
+ footers: named2(contributes?.footers),
4149
+ linkHandlers: named2(contributes?.linkHandlers)
4150
+ };
4151
+ }
3753
4152
  function registerConnectorTools(server) {
3754
4153
  server.tool(
3755
4154
  "list_connectors",
3756
- "List every connector: the ones built into Vorn, the ones installable from a package, and how many connections each already has. Use this before creating a workflow that calls a connector action, or to find the id of a connector to install.",
4155
+ "List every connector and extension: the ones built into Vorn, the ones installable from a package, and how many connections each already has. A connector polls a service; an extension adds footers and panes to a session card and says what it may touch. Use this before creating a workflow that calls a connector action, or to find an id to install.",
3757
4156
  {
3758
- installable_only: z7.boolean().optional().describe("Only connectors that are not set up yet")
4157
+ installable_only: z7.boolean().optional().describe("Only connectors that are not set up yet"),
4158
+ kind: z7.enum(["connector", "extension"]).optional().describe("Only one kind: what polls a service, or what shows on a card")
3759
4159
  },
3760
4160
  async (args) => {
3761
- const [builtIns, snapshot, connections, statuses] = await Promise.all([
4161
+ const [builtIns, snapshot, connections, statuses, packs] = await Promise.all([
3762
4162
  rpcCall("connector:list"),
3763
4163
  rpcCall("connector:catalog"),
3764
4164
  rpcCall("connection:list", { connectorId: void 0 }),
3765
- rpcCall("connector:status")
4165
+ rpcCall("connector:status"),
4166
+ rpcCall("connector:listPacks")
3766
4167
  ]);
3767
4168
  const countFor = (id) => connections.filter((conn) => connectionConnectorId(conn) === id).length;
3768
4169
  const statusFor = (id) => statuses.find((s) => s.connectorId === id);
4170
+ const packFor = (id) => packs.find((pack) => pack.id === id);
3769
4171
  const entries = [
3770
4172
  ...builtIns.map((c) => ({
3771
4173
  id: c.id,
3772
4174
  name: c.name,
3773
4175
  source: "built-in",
4176
+ kind: "connector",
3774
4177
  capabilities: c.capabilities,
3775
4178
  connections: countFor(c.id),
3776
4179
  // Only meaningful for connectors that authenticate up front; the
@@ -3780,25 +4183,49 @@ function registerConnectorTools(server) {
3780
4183
  ...statusFor(c.id).message && { authMessage: statusFor(c.id).message }
3781
4184
  }
3782
4185
  })),
3783
- ...snapshot.items.map((entry) => ({
3784
- id: entry.id,
3785
- name: entry.name,
3786
- source: "package",
3787
- description: entry.description,
3788
- package: entry.packageName,
3789
- ...entry.version && { version: entry.version },
3790
- capabilities: entry.capabilities,
3791
- connections: countFor(entry.id),
3792
- ...entry.auth && { auth: entry.auth },
3793
- // Generated upstream from the connector's own manifest, so an agent
3794
- // can tell whether a connector is worth installing without launching
3795
- // it — which for a list of twenty would be twenty npx processes.
3796
- ...entry.triggers && { triggers: entry.triggers.map(summarize) },
3797
- ...entry.actions && { actions: entry.actions.map(summarize) },
3798
- ...entry.env && { env: entry.env.map((e) => e.name) }
3799
- }))
4186
+ ...snapshot.items.map((entry) => {
4187
+ const pack = packFor(entry.id);
4188
+ const kind = pack?.kind ?? entry.kind ?? "connector";
4189
+ return {
4190
+ id: entry.id,
4191
+ name: entry.name,
4192
+ source: "package",
4193
+ kind,
4194
+ description: entry.description,
4195
+ package: entry.packageName,
4196
+ ...entry.version && { version: entry.version },
4197
+ capabilities: entry.capabilities,
4198
+ connections: countFor(entry.id),
4199
+ ...pack && { installed: pack.version },
4200
+ ...entry.auth && { auth: entry.auth },
4201
+ // Generated upstream from the connector's own manifest, so an agent
4202
+ // can tell whether a connector is worth installing without launching
4203
+ // it — which for a list of twenty would be twenty npx processes.
4204
+ ...entry.triggers && { triggers: entry.triggers.map(summarize) },
4205
+ ...entry.actions && { actions: entry.actions.map(summarize) },
4206
+ ...entry.env && { env: entry.env.map((e) => e.name) },
4207
+ // Only what was stated: an empty list would read as "adds nothing",
4208
+ // which is a claim an older catalog never made.
4209
+ ...kind === "extension" && {
4210
+ ...(pack?.contributes ?? entry.contributes) && {
4211
+ contributes: contributionSummary(pack?.contributes ?? entry.contributes)
4212
+ },
4213
+ ...(pack?.permissions ?? entry.permissions) && {
4214
+ permissions: pack?.permissions ?? entry.permissions
4215
+ },
4216
+ ...(pack?.activates ?? entry.activates) && {
4217
+ activates: pack?.activates ?? entry.activates
4218
+ }
4219
+ }
4220
+ };
4221
+ })
3800
4222
  ];
3801
- return json(args.installable_only ? entries.filter((e) => e.connections === 0) : entries);
4223
+ const ofKind = args.kind ? entries.filter((e) => e.kind === args.kind) : entries;
4224
+ return json(
4225
+ args.installable_only ? ofKind.filter(
4226
+ (e) => e.kind === "extension" ? !("installed" in e && e.installed) : e.connections === 0
4227
+ ) : ofKind
4228
+ );
3802
4229
  }
3803
4230
  );
3804
4231
  server.tool(
@@ -3848,7 +4275,7 @@ function registerConnectorTools(server) {
3848
4275
  );
3849
4276
  server.tool(
3850
4277
  "inspect_connector_package",
3851
- "Start a connector package and read what it offers \u2014 its triggers, actions and required environment variables \u2014 without installing it. Use this to review a connector before install_connector, or to check a local build.",
4278
+ "Start a connector package and read what it offers \u2014 its triggers, actions and required environment variables, or for an extension what it contributes to a card and what it asks to touch \u2014 without installing it. Use this to review one before install_connector, or to check a local build.",
3852
4279
  {
3853
4280
  package: V.shortText.describe(
3854
4281
  'npm package name, or a command to run a local build (e.g. "node /path/to/dist/index.js")'
@@ -3862,10 +4289,10 @@ function registerConnectorTools(server) {
3862
4289
  );
3863
4290
  server.tool(
3864
4291
  "install_connector",
3865
- "Install a connector from a pack file, from the catalog, or from an npm package, creating a connection ready to poll. Call list_connectors for catalog ids and inspect_connector_package to see which environment variables are needed. Secrets cannot be set this way \u2014 see the error it returns if the connector requires one.",
4292
+ "Install a connector from a pack file, from the catalog, or by a launch command, creating a connection ready to poll. Installing an extension is the whole of setting it up: it takes no trigger and makes no connection, and shows on the cards its activation names. Call list_connectors for catalog ids and inspect_connector_package to see which environment variables are needed. Secrets cannot be set this way \u2014 see the error it returns if the connector requires one.",
3866
4293
  {
3867
4294
  connector_id: V.id.optional().describe("Catalog connector id (from list_connectors). Use this or package."),
3868
- package: V.shortText.optional().describe("npm package name or launch command"),
4295
+ package: V.shortText.optional().describe("Launch command the connection runs, or a package name to run with npx"),
3869
4296
  pack_path: V.shortText.optional().describe(
3870
4297
  "Path to a .vorn.tgz pack to install first. It is verified and copied to disk, and the connection then launches those files rather than resolving a package."
3871
4298
  ),
@@ -3891,6 +4318,38 @@ function registerConnectorTools(server) {
3891
4318
  });
3892
4319
  if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
3893
4320
  installed = outcome.pack;
4321
+ } else if (entry?.kind === "extension") {
4322
+ if (!entry.packUrl) {
4323
+ return failure(
4324
+ `${entry.name} is in the catalog but no release has published a pack for it yet.`
4325
+ );
4326
+ }
4327
+ const outcome = await rpcCall("connector:installPack", {
4328
+ kind: "url",
4329
+ url: entry.packUrl,
4330
+ ...entry.sha256 && { sha256: entry.sha256 }
4331
+ });
4332
+ if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
4333
+ installed = outcome.pack;
4334
+ }
4335
+ if (installed?.kind === "extension") {
4336
+ const ignored = [
4337
+ args.trigger !== void 0 && "trigger",
4338
+ args.sync_interval_minutes !== void 0 && "sync_interval_minutes",
4339
+ args.env !== void 0 && "env",
4340
+ args.name !== void 0 && "name",
4341
+ args.project !== void 0 && "project"
4342
+ ].filter(Boolean);
4343
+ return json({
4344
+ installed: installed.name,
4345
+ kind: "extension",
4346
+ version: installed.version,
4347
+ path: installed.path,
4348
+ contributes: installed.contributes ?? {},
4349
+ permissions: installed.permissions ?? [],
4350
+ ...installed.activates && { activates: installed.activates },
4351
+ note: "Extensions have no connection: they show on the cards their activation names." + (ignored.length > 0 ? ` Ignored ${andList(ignored)}, which only a connection uses.` : "")
4352
+ });
3894
4353
  }
3895
4354
  const target = installed ? packLaunch(installed) : entry?.launch ?? args.package;
3896
4355
  if (!target) return failure("Provide either connector_id, package, or pack_path.");
@@ -4289,6 +4748,7 @@ function registerDeviceTools(server) {
4289
4748
  sessionId: id,
4290
4749
  udid: args.udid
4291
4750
  });
4751
+ if (!r.ok) throw new Error(r.message);
4292
4752
  return {
4293
4753
  content: [{ type: "text", text: `Claimed ${r.name} (${r.udid}).` }]
4294
4754
  };
@@ -4482,7 +4942,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4482
4942
  console.error = (...args) => _origError("[mcp:error]", ...args);
4483
4943
  async function main() {
4484
4944
  configManager.init();
4485
- const version = true ? "0.7.0-beta.8" : createRequire(import.meta.url)("../package.json").version;
4945
+ const version = true ? "0.7.0" : createRequire(import.meta.url)("../package.json").version;
4486
4946
  const server = createMcpServer(version);
4487
4947
  const transport = new StdioServerTransport();
4488
4948
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vornrun/mcp",
3
- "version": "0.7.0-beta.8",
3
+ "version": "0.7.0",
4
4
  "description": "Vorn MCP server — task management, git, and workflow tools for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,11 +35,11 @@
35
35
  "libsql": "^0.5.29",
36
36
  "pino": "^10.3.1",
37
37
  "ws": "^8.21.1",
38
- "zod": "^4.4.3"
38
+ "zod": "^4.5.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@vornrun/server": "0.7.0-beta.8",
42
- "@vornrun/shared": "0.7.0-beta.8",
41
+ "@vornrun/server": "0.7.0",
42
+ "@vornrun/shared": "0.7.0",
43
43
  "tsup": "^8.5.1",
44
44
  "tsx": "^4.23.1",
45
45
  "typescript": "^6.0.3"