@vornrun/mcp 0.7.0-beta.14 → 0.7.0-beta.15

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 +425 -75
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -50,8 +50,11 @@ 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;
53
55
  var BOOTSTRAP_ENV_VAR = "SECRET_VORN_BOOTSTRAP_TOKEN";
54
56
  var LOCAL_TOKEN_FILENAME = "local-token";
57
+ var WS_PORT_FILENAME = "ws-port";
55
58
 
56
59
  // ../shared/src/types.ts
57
60
  var DEFAULT_WORKSPACE = {
@@ -106,6 +109,7 @@ var STRIP_ENV_KEYS_UPPER = STRIP_ENV_KEYS.map((k) => k.toUpperCase());
106
109
 
107
110
  // ../server/src/default-workflows.ts
108
111
  var DEFAULT_TASK_WORKFLOW_ID = "system:default-task-workflow";
112
+ var DEV_SERVER_WORKFLOW_ID = "system:dev-server-on-restore";
109
113
  function buildDefaultTaskWorkflow() {
110
114
  const triggerConfig = {
111
115
  triggerType: "taskStatusChanged",
@@ -145,6 +149,39 @@ function buildDefaultTaskWorkflow() {
145
149
  edges: [{ id: "e1", source: "trigger-1", target: "launch-1" }]
146
150
  };
147
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
+ }
148
185
 
149
186
  // ../server/src/database.ts
150
187
  var DEFAULT_DATA_DIR = path2.join(os.homedir(), ".vorn");
@@ -163,8 +200,8 @@ function getDataDir() {
163
200
  }
164
201
  return resolvedDataDir;
165
202
  }
166
- function initDatabase(dataDir) {
167
- resolvedDataDir = dataDir ?? DEFAULT_DATA_DIR;
203
+ function initDatabase(dataDir2) {
204
+ resolvedDataDir = dataDir2 ?? DEFAULT_DATA_DIR;
168
205
  if (!fs2.existsSync(getDataDir())) {
169
206
  fs2.mkdirSync(getDataDir(), { recursive: true, mode: 448 });
170
207
  }
@@ -189,17 +226,25 @@ function initDatabase(dataDir) {
189
226
  }
190
227
  }
191
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) {
192
237
  const d = getDb();
193
- 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);
194
239
  if (flagRow) {
195
240
  try {
196
241
  if (JSON.parse(flagRow.value) === true) return;
197
242
  } catch {
198
243
  }
199
244
  }
200
- 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);
201
246
  if (!existing) {
202
- const w = buildDefaultTaskWorkflow();
247
+ const w = build();
203
248
  d.prepare(
204
249
  `INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
205
250
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -216,11 +261,12 @@ function seedSystemDefaults() {
216
261
  w.staggerDelayMs ?? null,
217
262
  w.workspaceId ?? "personal"
218
263
  );
219
- logger_default.info(`[database] Seeded default task workflow (${DEFAULT_TASK_WORKFLOW_ID})`);
264
+ logger_default.info(`[database] Seeded workflow ${id}`);
220
265
  }
221
- d.prepare(
222
- "INSERT OR REPLACE INTO defaults (key, value) VALUES ('hasSeededDefaultTaskWorkflow', ?)"
223
- ).run(JSON.stringify(true));
266
+ d.prepare("INSERT OR REPLACE INTO defaults (key, value) VALUES (?, ?)").run(
267
+ flag,
268
+ JSON.stringify(true)
269
+ );
224
270
  }
225
271
  function recoverCorruptDatabase() {
226
272
  try {
@@ -393,7 +439,8 @@ function createSchema() {
393
439
  saved_at INTEGER,
394
440
  sort_order INTEGER NOT NULL DEFAULT 0,
395
441
  worktree_name TEXT,
396
- agent_session_id TEXT
442
+ agent_session_id TEXT,
443
+ renamed_by_person INTEGER
397
444
  );
398
445
 
399
446
  CREATE TABLE IF NOT EXISTS schedule_log (
@@ -417,6 +464,16 @@ function createSchema() {
417
464
  "order" INTEGER NOT NULL DEFAULT 0
418
465
  );
419
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
+
420
477
  CREATE TABLE IF NOT EXISTS workflow_runs (
421
478
  id TEXT PRIMARY KEY,
422
479
  workflow_id TEXT NOT NULL,
@@ -937,11 +994,24 @@ function migrateSchema(d) {
937
994
  })();
938
995
  logger_default.info("[database] migrated schema to version 17 (packaged connector task ids)");
939
996
  }
997
+ if (version < 18) {
998
+ d.transaction(() => {
999
+ const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
1000
+ if (!sessionCols.some((c) => c.name === "group_id")) {
1001
+ d.exec("ALTER TABLE sessions ADD COLUMN group_id TEXT");
1002
+ }
1003
+ d.prepare(
1004
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '18')"
1005
+ ).run();
1006
+ })();
1007
+ logger_default.info("[database] migrated schema to version 18 (session groups)");
1008
+ }
940
1009
  }
941
1010
  var REVISIONED_TABLES = [
942
1011
  "projects",
943
1012
  "tasks",
944
1013
  "workspaces",
1014
+ "session_groups",
945
1015
  "remote_hosts",
946
1016
  "agent_commands"
947
1017
  ];
@@ -978,10 +1048,15 @@ function verifySchema(d) {
978
1048
  column: "sort_order",
979
1049
  ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
980
1050
  },
1051
+ { column: "group_id", ddl: "ALTER TABLE sessions ADD COLUMN group_id TEXT" },
981
1052
  { column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
982
1053
  { column: "agent_session_id", ddl: "ALTER TABLE sessions ADD COLUMN agent_session_id TEXT" },
983
1054
  { column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd TEXT" },
984
- { column: "head_commit", ddl: "ALTER TABLE sessions ADD COLUMN head_commit TEXT" }
1055
+ { column: "head_commit", ddl: "ALTER TABLE sessions ADD COLUMN head_commit TEXT" },
1056
+ {
1057
+ column: "renamed_by_person",
1058
+ ddl: "ALTER TABLE sessions ADD COLUMN renamed_by_person INTEGER"
1059
+ }
985
1060
  ],
986
1061
  agent_commands: [
987
1062
  {
@@ -1094,6 +1169,7 @@ function loadConfig() {
1094
1169
  const remoteHosts = loadRemoteHosts(d);
1095
1170
  const tasks = loadTasks(d);
1096
1171
  const workspaces = loadWorkspaces(d);
1172
+ const sessionGroups = loadSessionGroups(d);
1097
1173
  return {
1098
1174
  version: 1,
1099
1175
  revision: readConfigRevision(d),
@@ -1103,7 +1179,8 @@ function loadConfig() {
1103
1179
  workflows,
1104
1180
  remoteHosts,
1105
1181
  tasks,
1106
- workspaces
1182
+ workspaces,
1183
+ sessionGroups
1107
1184
  };
1108
1185
  }
1109
1186
  function loadDefaults(d) {
@@ -1190,6 +1267,9 @@ function loadDefaults(d) {
1190
1267
  ...map.headlessRetentionMinutes !== void 0 && {
1191
1268
  headlessRetentionMinutes: map.headlessRetentionMinutes
1192
1269
  },
1270
+ ...map.hasSeededDevServerWorkflow !== void 0 && {
1271
+ hasSeededDevServerWorkflow: map.hasSeededDevServerWorkflow
1272
+ },
1193
1273
  ...map.hasSeededDefaultTaskWorkflow !== void 0 && {
1194
1274
  hasSeededDefaultTaskWorkflow: map.hasSeededDefaultTaskWorkflow
1195
1275
  },
@@ -1253,6 +1333,10 @@ function loadTasks(d) {
1253
1333
  const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
1254
1334
  return rows.map(rowToTask);
1255
1335
  }
1336
+ function loadSessionGroups(d) {
1337
+ const rows = d.prepare('SELECT * FROM session_groups ORDER BY "order"').all();
1338
+ return rows.map(rowToSessionGroup);
1339
+ }
1256
1340
  function loadWorkspaces(d) {
1257
1341
  const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
1258
1342
  return rows.map(rowToWorkspace);
@@ -1492,6 +1576,36 @@ function saveConfig(config) {
1492
1576
  for (const ws of workspaces) {
1493
1577
  insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order, revision);
1494
1578
  }
1579
+ const sessionGroups = config.sessionGroups ?? [];
1580
+ pruneMissing(
1581
+ d,
1582
+ "session_groups",
1583
+ "id",
1584
+ sessionGroups.map((g) => g.id),
1585
+ baseRevision
1586
+ );
1587
+ const insertSessionGroup = d.prepare(
1588
+ `INSERT INTO session_groups (id, name, icon, icon_color, "order", workspace_id, row_revision)
1589
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1590
+ ON CONFLICT(id) DO UPDATE SET
1591
+ row_revision = excluded.row_revision,
1592
+ name = excluded.name,
1593
+ icon = excluded.icon,
1594
+ icon_color = excluded.icon_color,
1595
+ "order" = excluded."order",
1596
+ workspace_id = excluded.workspace_id`
1597
+ );
1598
+ for (const g of sessionGroups) {
1599
+ insertSessionGroup.run(
1600
+ g.id,
1601
+ g.name,
1602
+ g.icon ?? null,
1603
+ g.iconColor ?? null,
1604
+ g.order,
1605
+ g.workspaceId,
1606
+ revision
1607
+ );
1608
+ }
1495
1609
  d.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)").run(
1496
1610
  CONFIG_REVISION_KEY,
1497
1611
  String(revision)
@@ -1549,6 +1663,16 @@ function rowToWorkflow(r) {
1549
1663
  workspaceId: r.workspace_id ?? "personal"
1550
1664
  };
1551
1665
  }
1666
+ function rowToSessionGroup(r) {
1667
+ return {
1668
+ id: r.id,
1669
+ name: r.name,
1670
+ ...r.icon != null && { icon: r.icon },
1671
+ ...r.icon_color != null && { iconColor: r.icon_color },
1672
+ order: r.order,
1673
+ workspaceId: r.workspace_id ?? "personal"
1674
+ };
1675
+ }
1552
1676
  function rowToWorkspace(r) {
1553
1677
  return {
1554
1678
  id: r.id,
@@ -1565,8 +1689,8 @@ var ConfigManager = class {
1565
1689
  dbWatcher = null;
1566
1690
  debounceTimer = null;
1567
1691
  cachedConfig = null;
1568
- init(dataDir) {
1569
- initDatabase(dataDir);
1692
+ init(dataDir2) {
1693
+ initDatabase(dataDir2);
1570
1694
  }
1571
1695
  close() {
1572
1696
  this.stopWatchingDb();
@@ -1684,33 +1808,47 @@ var V = {
1684
1808
  url: safeUrl
1685
1809
  };
1686
1810
 
1687
- // src/ws-client.ts
1811
+ // ../server/src/rpc-client.ts
1688
1812
  import fs4 from "fs";
1689
1813
  import path3 from "path";
1690
1814
  import os2 from "os";
1691
1815
  import { execFileSync as execFileSync2 } from "child_process";
1692
1816
  import { WebSocket } from "ws";
1693
- var DATA_DIR = process.env.VORN_DATA_DIR || path3.join(os2.homedir(), ".vorn");
1694
- var PORT_FILE = path3.join(DATA_DIR, "ws-port");
1695
- var LOCAL_TOKEN_FILE = path3.join(DATA_DIR, LOCAL_TOKEN_FILENAME);
1696
- var TOKEN_FILE_MISSING_MSG = `Vorn local credential not found (${LOCAL_TOKEN_FILE}).
1817
+ var dataDirOverride;
1818
+ function named(value) {
1819
+ return value?.trim() ? value : void 0;
1820
+ }
1821
+ function dataDir() {
1822
+ return dataDirOverride ?? named(process.env.VORN_DATA_DIR) ?? path3.join(os2.homedir(), ".vorn");
1823
+ }
1824
+ function portFile() {
1825
+ return path3.join(dataDir(), WS_PORT_FILENAME);
1826
+ }
1827
+ function localTokenFile() {
1828
+ return path3.join(dataDir(), LOCAL_TOKEN_FILENAME);
1829
+ }
1830
+ function tokenFileMissingMessage() {
1831
+ return `Vorn local credential not found (${localTokenFile()}).
1697
1832
  The server writes it on startup and removes it on shutdown, so this usually means
1698
- Vorn is not running. Start Vorn (or \`vorn-server serve\`) and try again.
1699
- If the server runs with --data-dir, set VORN_DATA_DIR to the same directory.`;
1833
+ Vorn is not running. Start Vorn (or \`vorn server serve\`) and try again.
1834
+ If the server runs with --data-dir, pass the same --data-dir here, or set
1835
+ VORN_DATA_DIR to that directory -- which is how anything that is not the CLI,
1836
+ MCP included, reaches a server that moved.`;
1837
+ }
1700
1838
  function readLocalToken() {
1701
1839
  try {
1702
- const token = fs4.readFileSync(LOCAL_TOKEN_FILE, "utf-8").trim();
1840
+ const token = fs4.readFileSync(localTokenFile(), "utf-8").trim();
1703
1841
  if (!token) throw new Error("empty");
1704
1842
  return token;
1705
1843
  } catch {
1706
- throw new Error(TOKEN_FILE_MISSING_MSG);
1844
+ throw new Error(tokenFileMissingMessage());
1707
1845
  }
1708
1846
  }
1709
1847
  function connection() {
1710
1848
  const result = readPort();
1711
1849
  if (!result.port) {
1712
1850
  const reason = "reason" in result ? result.reason : "missing";
1713
- throw new Error(reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1851
+ throw new Error(reason === "invalid" ? portFileInvalidMessage() : portFileMissingMessage());
1714
1852
  }
1715
1853
  return {
1716
1854
  url: `ws://127.0.0.1:${result.port}/ws`,
@@ -1719,21 +1857,42 @@ function connection() {
1719
1857
  }
1720
1858
  var TIMEOUT_MS = 1e4;
1721
1859
  var IS_WIN = process.platform === "win32";
1722
- var PORT_FILE_MISSING_MSG = IS_WIN ? `Vorn port file not found (~/.vorn/ws-port).
1860
+ function portFileMissingMessage() {
1861
+ const file = portFile();
1862
+ return IS_WIN ? `Vorn port file not found (${file}).
1723
1863
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1724
- To fix, find the Vorn process and its listening port:
1725
- powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort"
1864
+ To fix, in PowerShell, find the Vorn process and its listening port:
1865
+ Get-NetTCPConnection -State Listen -OwningProcess (Get-Process Vorn).Id | Select LocalPort
1726
1866
  Then write the WS port to the file:
1727
- echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vorn\\ws-port
1728
- Or restart Vorn to regenerate it.` : `Vorn port file not found (~/.vorn/ws-port).
1867
+ '{"port":<PORT>,"pid":<PID>}' | Set-Content -Path "${file}"
1868
+ Or restart Vorn to regenerate it.` : `Vorn port file not found (${file}).
1729
1869
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1730
1870
  To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
1731
1871
  Then write the WS port (the one on *:<port>) to the file:
1732
- echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
1872
+ echo '{"port":<PORT>,"pid":<PID>}' > "${file}"
1733
1873
  Or restart Vorn to regenerate it.`;
1734
- var PORT_FILE_INVALID_MSG = `Vorn port file exists but contains invalid data (~/.vorn/ws-port).
1874
+ }
1875
+ function portFileInvalidMessage() {
1876
+ const file = portFile();
1877
+ return `Vorn port file exists but contains invalid data (${file}).
1735
1878
  Delete it and restart Vorn, or overwrite it with the correct port:
1736
- ${IS_WIN ? "del %USERPROFILE%\\.vorn\\ws-port" : "rm ~/.vorn/ws-port"}`;
1879
+ ${IS_WIN ? `Remove-Item "${file}"` : `rm "${file}"`}`;
1880
+ }
1881
+ function closedBeforeAnswering(code) {
1882
+ if (code === CLOSE_UNAUTHENTICATED || code === CLOSE_CREDENTIAL_REJECTED) {
1883
+ return `A Vorn server on this port refused the credential in ${localTokenFile()}.
1884
+ Another server is listening on it with its own data directory, which a dev build
1885
+ running beside the app does. Point at that one with --data-dir (or VORN_DATA_DIR),
1886
+ or stop it.`;
1887
+ }
1888
+ return `The server closed the connection before answering (code ${code}).`;
1889
+ }
1890
+ function explain(message) {
1891
+ if (!message.startsWith("Method not found:")) return message;
1892
+ const method = message.slice("Method not found:".length).trim();
1893
+ return `This server does not have ${method}, so it is older than the vorn command asking for it.
1894
+ Restart Vorn to pick up the newer server, or run this against the matching build.`;
1895
+ }
1737
1896
  var rpcId = 0;
1738
1897
  var cachedPort = null;
1739
1898
  var cacheTimestamp = 0;
@@ -1786,7 +1945,11 @@ function discoverPort() {
1786
1945
  }
1787
1946
  return null;
1788
1947
  }
1948
+ function discoveryAllowed() {
1949
+ return dataDirOverride === void 0 && named(process.env.VORN_DATA_DIR) === void 0;
1950
+ }
1789
1951
  function discoverAndHeal() {
1952
+ if (!discoveryAllowed()) return { port: null, reason: "missing" };
1790
1953
  const now = Date.now();
1791
1954
  if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1792
1955
  const discovered = discoverPort();
@@ -1794,8 +1957,8 @@ function discoverAndHeal() {
1794
1957
  cacheTimestamp = now;
1795
1958
  if (discovered) {
1796
1959
  try {
1797
- fs4.mkdirSync(path3.dirname(PORT_FILE), { recursive: true });
1798
- fs4.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
1960
+ fs4.mkdirSync(dataDir(), { recursive: true });
1961
+ fs4.writeFileSync(portFile(), JSON.stringify({ port: discovered }), "utf-8");
1799
1962
  } catch {
1800
1963
  }
1801
1964
  return { port: discovered };
@@ -1804,7 +1967,7 @@ function discoverAndHeal() {
1804
1967
  }
1805
1968
  function readPort() {
1806
1969
  try {
1807
- const raw = fs4.readFileSync(PORT_FILE, "utf-8").trim();
1970
+ const raw = fs4.readFileSync(portFile(), "utf-8").trim();
1808
1971
  if (!raw) return { port: null, reason: "invalid" };
1809
1972
  if (raw.startsWith("{")) {
1810
1973
  const parsed = JSON.parse(raw);
@@ -1846,15 +2009,19 @@ async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
1846
2009
  const msg = JSON.parse(raw.toString());
1847
2010
  if (msg.id !== id) return;
1848
2011
  clearTimeout(timer);
1849
- ws.close();
1850
2012
  if (msg.error) {
1851
- reject(new Error(msg.error.message));
2013
+ reject(new Error(explain(msg.error.message)));
1852
2014
  } else {
1853
2015
  resolve(msg.result);
1854
2016
  }
2017
+ ws.close();
1855
2018
  } catch {
1856
2019
  }
1857
2020
  });
2021
+ ws.on("close", (code) => {
2022
+ clearTimeout(timer);
2023
+ reject(new Error(closedBeforeAnswering(code)));
2024
+ });
1858
2025
  ws.on("error", (err) => {
1859
2026
  clearTimeout(timer);
1860
2027
  reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
@@ -1983,6 +2150,9 @@ async function listWorkflowRunsByTask(taskId, limit = 20) {
1983
2150
  limit
1984
2151
  });
1985
2152
  }
2153
+ async function listRunsWithWaitingGates() {
2154
+ return rpcCall("workflowRun:listWaiting");
2155
+ }
1986
2156
  async function listAllWorkflowRuns(workspaceId, limit = 50) {
1987
2157
  return rpcCall("workflowRun:listAll", {
1988
2158
  workspaceId,
@@ -2793,8 +2963,8 @@ function resolveRequirement(requirement, connections) {
2793
2963
  );
2794
2964
  if (candidates.length === 0) return void 0;
2795
2965
  if (requirement.name !== "") {
2796
- const named = candidates.filter((connection2) => connection2.name === requirement.name);
2797
- if (named.length === 1) return named[0].id;
2966
+ const named2 = candidates.filter((connection2) => connection2.name === requirement.name);
2967
+ if (named2.length === 1) return named2[0].id;
2798
2968
  }
2799
2969
  return candidates.length === 1 ? candidates[0].id : void 0;
2800
2970
  }
@@ -3208,6 +3378,46 @@ function resolveWorkflowId(args) {
3208
3378
  }
3209
3379
  return { id };
3210
3380
  }
3381
+ function approvalNode(workflow, nodeId) {
3382
+ return workflow?.nodes.find((n) => n.id === nodeId && n.type === "approval");
3383
+ }
3384
+ function askedBy(node) {
3385
+ return node?.config?.message?.trim() || void 0;
3386
+ }
3387
+ function gateMessage(workflow, nodeId) {
3388
+ return askedBy(approvalNode(workflow, nodeId));
3389
+ }
3390
+ function annotateWaitingGates(runs, workflows) {
3391
+ return runs.map((run) => {
3392
+ if (!run.nodeStates.some((n) => n.status === "waiting")) return run;
3393
+ const workflow = workflows.find((w) => w.id === run.workflowId);
3394
+ return {
3395
+ ...run,
3396
+ nodeStates: run.nodeStates.map((state) => {
3397
+ if (state.status !== "waiting") return state;
3398
+ const asks = gateMessage(workflow, state.nodeId);
3399
+ return asks ? { ...state, asks } : state;
3400
+ })
3401
+ };
3402
+ });
3403
+ }
3404
+ async function runById(runId) {
3405
+ const recent = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === runId);
3406
+ if (recent) return recent;
3407
+ return (await listRunsWithWaitingGates()).find((r) => r.runId === runId);
3408
+ }
3409
+ function resolveGateTarget(run, nodeId) {
3410
+ const waiting = run.nodeStates.filter((n) => n.status === "waiting").map((n) => n.nodeId);
3411
+ if (nodeId) {
3412
+ if (waiting.includes(nodeId)) return { nodeId };
3413
+ return {
3414
+ 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`
3415
+ };
3416
+ }
3417
+ if (waiting.length === 1) return { nodeId: waiting[0] };
3418
+ if (waiting.length === 0) return { error: "no node in this run is waiting on a gate" };
3419
+ return { error: `${waiting.length} nodes are waiting \u2014 pass node_id: ${waiting.join(", ")}` };
3420
+ }
3211
3421
  async function listPortableConnections() {
3212
3422
  try {
3213
3423
  return await rpcCall("connection:list", { connectorId: void 0 });
@@ -3361,7 +3571,7 @@ function registerWorkflowTools(server) {
3361
3571
  );
3362
3572
  server.tool(
3363
3573
  "list_workflow_runs",
3364
- "List workflow execution history. Filter by workflow_id or task_id.",
3574
+ "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.",
3365
3575
  {
3366
3576
  workflow_id: V.id.optional().describe("Filter by workflow ID"),
3367
3577
  task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
@@ -3374,28 +3584,28 @@ function registerWorkflowTools(server) {
3374
3584
  isError: true
3375
3585
  };
3376
3586
  }
3587
+ const withGates = async (runs) => runs.some((r) => r.nodeStates.some((n) => n.status === "waiting")) ? annotateWaitingGates(runs, await dbListWorkflows()) : runs;
3377
3588
  if (args.task_id) {
3378
- const runs = await listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
3589
+ const runs = await withGates(await listWorkflowRunsByTask(args.task_id, args.limit ?? 20));
3379
3590
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3380
3591
  }
3381
3592
  if (args.workflow_id) {
3382
- const runs = await listWorkflowRuns(args.workflow_id, args.limit ?? 20);
3593
+ const runs = await withGates(await listWorkflowRuns(args.workflow_id, args.limit ?? 20));
3383
3594
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3384
3595
  }
3385
- return {
3386
- content: [{ type: "text", text: "Error: provide either workflow_id or task_id" }],
3387
- isError: true
3388
- };
3596
+ const parked = (await listRunsWithWaitingGates()).slice(0, args.limit ?? 20);
3597
+ const waiting = await withGates(parked);
3598
+ return { content: [{ type: "text", text: JSON.stringify(waiting, null, 2) }] };
3389
3599
  }
3390
3600
  );
3391
3601
  server.tool(
3392
3602
  "stop_workflow_run",
3393
- "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.",
3603
+ "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.",
3394
3604
  {
3395
3605
  run_id: V.id.describe("Run ID (from list_workflow_runs)")
3396
3606
  },
3397
3607
  async (args) => {
3398
- const run = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === args.run_id);
3608
+ const run = await runById(args.run_id);
3399
3609
  if (!run) {
3400
3610
  return {
3401
3611
  content: [
@@ -3440,6 +3650,74 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3440
3650
  };
3441
3651
  }
3442
3652
  );
3653
+ server.tool(
3654
+ "resolve_gate",
3655
+ "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.",
3656
+ {
3657
+ run_id: V.id.describe("Run ID (from list_workflow_runs)"),
3658
+ decision: z5.enum(["approve", "reject"]).describe("approve lets the run go on; reject ends it"),
3659
+ node_id: V.id.optional().describe("The waiting node, when a run has more than one gate open")
3660
+ },
3661
+ async (args) => {
3662
+ const run = await runById(args.run_id);
3663
+ if (!run) {
3664
+ return {
3665
+ content: [
3666
+ {
3667
+ type: "text",
3668
+ text: `Error: no run "${args.run_id}" in the recent history, and none parked on a gate. Check list_workflow_runs.`
3669
+ }
3670
+ ],
3671
+ isError: true
3672
+ };
3673
+ }
3674
+ if (run.status !== "running") {
3675
+ return {
3676
+ content: [
3677
+ {
3678
+ type: "text",
3679
+ text: `Run ${args.run_id} already finished (${run.status}) \u2014 no gate to answer.`
3680
+ }
3681
+ ]
3682
+ };
3683
+ }
3684
+ const target = resolveGateTarget(run, args.node_id);
3685
+ if ("error" in target) {
3686
+ return {
3687
+ content: [{ type: "text", text: `Error: ${target.error}` }],
3688
+ isError: true
3689
+ };
3690
+ }
3691
+ const workflow = (await dbListWorkflows()).find((w) => w.id === run.workflowId);
3692
+ const gateNode = approvalNode(workflow, target.nodeId);
3693
+ const asked = askedBy(gateNode);
3694
+ try {
3695
+ await rpcCall("workflow:resolveGate", {
3696
+ runId: args.run_id,
3697
+ nodeId: target.nodeId,
3698
+ decision: args.decision
3699
+ });
3700
+ } catch (err) {
3701
+ return {
3702
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
3703
+ isError: true
3704
+ };
3705
+ }
3706
+ const gate = gateNode?.label ?? target.nodeId;
3707
+ return {
3708
+ content: [
3709
+ {
3710
+ type: "text",
3711
+ text: `${args.decision === "approve" ? "Approved" : "Rejected"} "${gate}" on run ${args.run_id}${run.workflowName ? ` of "${run.workflowName}"` : ""}.${asked ? `
3712
+
3713
+ What it asked: ${asked}` : ""}
3714
+
3715
+ The decision went out; the instance holding the run acts on it, so a desktop has to be open. Confirm with list_workflow_runs.`
3716
+ }
3717
+ ]
3718
+ };
3719
+ }
3720
+ );
3443
3721
  server.tool(
3444
3722
  "get_workflow_schedule",
3445
3723
  "Get scheduler info for a workflow: execution log or next scheduled run. Requires the Vorn app to be running.",
@@ -3480,7 +3758,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3480
3758
  );
3481
3759
  server.tool(
3482
3760
  "execute_workflow",
3483
- "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.",
3761
+ "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.",
3484
3762
  {
3485
3763
  workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
3486
3764
  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>}})")
@@ -3799,27 +4077,43 @@ var failure = (message) => ({
3799
4077
  function summarize(entry) {
3800
4078
  return { type: entry.type, label: entry.label };
3801
4079
  }
4080
+ function andList(values) {
4081
+ if (values.length <= 1) return values[0] ?? "";
4082
+ return `${values.slice(0, -1).join(", ")} and ${values[values.length - 1]}`;
4083
+ }
4084
+ function contributionSummary(contributes) {
4085
+ const named2 = (entries) => (entries ?? []).map((entry) => ({ id: entry.id, title: entry.title }));
4086
+ return {
4087
+ panes: named2(contributes?.panes),
4088
+ footers: named2(contributes?.footers),
4089
+ linkHandlers: named2(contributes?.linkHandlers)
4090
+ };
4091
+ }
3802
4092
  function registerConnectorTools(server) {
3803
4093
  server.tool(
3804
4094
  "list_connectors",
3805
- "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.",
4095
+ "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.",
3806
4096
  {
3807
- installable_only: z7.boolean().optional().describe("Only connectors that are not set up yet")
4097
+ installable_only: z7.boolean().optional().describe("Only connectors that are not set up yet"),
4098
+ kind: z7.enum(["connector", "extension"]).optional().describe("Only one kind: what polls a service, or what shows on a card")
3808
4099
  },
3809
4100
  async (args) => {
3810
- const [builtIns, snapshot, connections, statuses] = await Promise.all([
4101
+ const [builtIns, snapshot, connections, statuses, packs] = await Promise.all([
3811
4102
  rpcCall("connector:list"),
3812
4103
  rpcCall("connector:catalog"),
3813
4104
  rpcCall("connection:list", { connectorId: void 0 }),
3814
- rpcCall("connector:status")
4105
+ rpcCall("connector:status"),
4106
+ rpcCall("connector:listPacks")
3815
4107
  ]);
3816
4108
  const countFor = (id) => connections.filter((conn) => connectionConnectorId(conn) === id).length;
3817
4109
  const statusFor = (id) => statuses.find((s) => s.connectorId === id);
4110
+ const packFor = (id) => packs.find((pack) => pack.id === id);
3818
4111
  const entries = [
3819
4112
  ...builtIns.map((c) => ({
3820
4113
  id: c.id,
3821
4114
  name: c.name,
3822
4115
  source: "built-in",
4116
+ kind: "connector",
3823
4117
  capabilities: c.capabilities,
3824
4118
  connections: countFor(c.id),
3825
4119
  // Only meaningful for connectors that authenticate up front; the
@@ -3829,25 +4123,49 @@ function registerConnectorTools(server) {
3829
4123
  ...statusFor(c.id).message && { authMessage: statusFor(c.id).message }
3830
4124
  }
3831
4125
  })),
3832
- ...snapshot.items.map((entry) => ({
3833
- id: entry.id,
3834
- name: entry.name,
3835
- source: "package",
3836
- description: entry.description,
3837
- package: entry.packageName,
3838
- ...entry.version && { version: entry.version },
3839
- capabilities: entry.capabilities,
3840
- connections: countFor(entry.id),
3841
- ...entry.auth && { auth: entry.auth },
3842
- // Generated upstream from the connector's own manifest, so an agent
3843
- // can tell whether a connector is worth installing without launching
3844
- // it — which for a list of twenty would be twenty npx processes.
3845
- ...entry.triggers && { triggers: entry.triggers.map(summarize) },
3846
- ...entry.actions && { actions: entry.actions.map(summarize) },
3847
- ...entry.env && { env: entry.env.map((e) => e.name) }
3848
- }))
4126
+ ...snapshot.items.map((entry) => {
4127
+ const pack = packFor(entry.id);
4128
+ const kind = pack?.kind ?? entry.kind ?? "connector";
4129
+ return {
4130
+ id: entry.id,
4131
+ name: entry.name,
4132
+ source: "package",
4133
+ kind,
4134
+ description: entry.description,
4135
+ package: entry.packageName,
4136
+ ...entry.version && { version: entry.version },
4137
+ capabilities: entry.capabilities,
4138
+ connections: countFor(entry.id),
4139
+ ...pack && { installed: pack.version },
4140
+ ...entry.auth && { auth: entry.auth },
4141
+ // Generated upstream from the connector's own manifest, so an agent
4142
+ // can tell whether a connector is worth installing without launching
4143
+ // it — which for a list of twenty would be twenty npx processes.
4144
+ ...entry.triggers && { triggers: entry.triggers.map(summarize) },
4145
+ ...entry.actions && { actions: entry.actions.map(summarize) },
4146
+ ...entry.env && { env: entry.env.map((e) => e.name) },
4147
+ // Only what was stated: an empty list would read as "adds nothing",
4148
+ // which is a claim an older catalog never made.
4149
+ ...kind === "extension" && {
4150
+ ...(pack?.contributes ?? entry.contributes) && {
4151
+ contributes: contributionSummary(pack?.contributes ?? entry.contributes)
4152
+ },
4153
+ ...(pack?.permissions ?? entry.permissions) && {
4154
+ permissions: pack?.permissions ?? entry.permissions
4155
+ },
4156
+ ...(pack?.activates ?? entry.activates) && {
4157
+ activates: pack?.activates ?? entry.activates
4158
+ }
4159
+ }
4160
+ };
4161
+ })
3849
4162
  ];
3850
- return json(args.installable_only ? entries.filter((e) => e.connections === 0) : entries);
4163
+ const ofKind = args.kind ? entries.filter((e) => e.kind === args.kind) : entries;
4164
+ return json(
4165
+ args.installable_only ? ofKind.filter(
4166
+ (e) => e.kind === "extension" ? !("installed" in e && e.installed) : e.connections === 0
4167
+ ) : ofKind
4168
+ );
3851
4169
  }
3852
4170
  );
3853
4171
  server.tool(
@@ -3897,7 +4215,7 @@ function registerConnectorTools(server) {
3897
4215
  );
3898
4216
  server.tool(
3899
4217
  "inspect_connector_package",
3900
- "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.",
4218
+ "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.",
3901
4219
  {
3902
4220
  package: V.shortText.describe(
3903
4221
  'npm package name, or a command to run a local build (e.g. "node /path/to/dist/index.js")'
@@ -3911,10 +4229,10 @@ function registerConnectorTools(server) {
3911
4229
  );
3912
4230
  server.tool(
3913
4231
  "install_connector",
3914
- "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.",
4232
+ "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.",
3915
4233
  {
3916
4234
  connector_id: V.id.optional().describe("Catalog connector id (from list_connectors). Use this or package."),
3917
- package: V.shortText.optional().describe("npm package name or launch command"),
4235
+ package: V.shortText.optional().describe("Launch command the connection runs, or a package name to run with npx"),
3918
4236
  pack_path: V.shortText.optional().describe(
3919
4237
  "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."
3920
4238
  ),
@@ -3940,6 +4258,38 @@ function registerConnectorTools(server) {
3940
4258
  });
3941
4259
  if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
3942
4260
  installed = outcome.pack;
4261
+ } else if (entry?.kind === "extension") {
4262
+ if (!entry.packUrl) {
4263
+ return failure(
4264
+ `${entry.name} is in the catalog but no release has published a pack for it yet.`
4265
+ );
4266
+ }
4267
+ const outcome = await rpcCall("connector:installPack", {
4268
+ kind: "url",
4269
+ url: entry.packUrl,
4270
+ ...entry.sha256 && { sha256: entry.sha256 }
4271
+ });
4272
+ if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
4273
+ installed = outcome.pack;
4274
+ }
4275
+ if (installed?.kind === "extension") {
4276
+ const ignored = [
4277
+ args.trigger !== void 0 && "trigger",
4278
+ args.sync_interval_minutes !== void 0 && "sync_interval_minutes",
4279
+ args.env !== void 0 && "env",
4280
+ args.name !== void 0 && "name",
4281
+ args.project !== void 0 && "project"
4282
+ ].filter(Boolean);
4283
+ return json({
4284
+ installed: installed.name,
4285
+ kind: "extension",
4286
+ version: installed.version,
4287
+ path: installed.path,
4288
+ contributes: installed.contributes ?? {},
4289
+ permissions: installed.permissions ?? [],
4290
+ ...installed.activates && { activates: installed.activates },
4291
+ note: "Extensions have no connection: they show on the cards their activation names." + (ignored.length > 0 ? ` Ignored ${andList(ignored)}, which only a connection uses.` : "")
4292
+ });
3943
4293
  }
3944
4294
  const target = installed ? packLaunch(installed) : entry?.launch ?? args.package;
3945
4295
  if (!target) return failure("Provide either connector_id, package, or pack_path.");
@@ -4532,7 +4882,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4532
4882
  console.error = (...args) => _origError("[mcp:error]", ...args);
4533
4883
  async function main() {
4534
4884
  configManager.init();
4535
- const version = true ? "0.7.0-beta.14" : createRequire(import.meta.url)("../package.json").version;
4885
+ const version = true ? "0.7.0-beta.15" : createRequire(import.meta.url)("../package.json").version;
4536
4886
  const server = createMcpServer(version);
4537
4887
  const transport = new StdioServerTransport();
4538
4888
  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.14",
3
+ "version": "0.7.0-beta.15",
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.13",
42
- "@vornrun/shared": "0.7.0-beta.13",
41
+ "@vornrun/server": "0.7.0-beta.15",
42
+ "@vornrun/shared": "0.7.0-beta.15",
43
43
  "tsup": "^8.5.1",
44
44
  "tsx": "^4.23.1",
45
45
  "typescript": "^6.0.3"