@vornrun/mcp 0.7.0-beta.13 → 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 +428 -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,9 +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
- { column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd TEXT" }
1054
+ { column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd 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
+ }
984
1060
  ],
985
1061
  agent_commands: [
986
1062
  {
@@ -1093,6 +1169,7 @@ function loadConfig() {
1093
1169
  const remoteHosts = loadRemoteHosts(d);
1094
1170
  const tasks = loadTasks(d);
1095
1171
  const workspaces = loadWorkspaces(d);
1172
+ const sessionGroups = loadSessionGroups(d);
1096
1173
  return {
1097
1174
  version: 1,
1098
1175
  revision: readConfigRevision(d),
@@ -1102,7 +1179,8 @@ function loadConfig() {
1102
1179
  workflows,
1103
1180
  remoteHosts,
1104
1181
  tasks,
1105
- workspaces
1182
+ workspaces,
1183
+ sessionGroups
1106
1184
  };
1107
1185
  }
1108
1186
  function loadDefaults(d) {
@@ -1135,6 +1213,8 @@ function loadDefaults(d) {
1135
1213
  // terminal drew and waits. There is nothing to ask, and leaving it off made
1136
1214
  // the whole thing invisible unless somebody went looking for a toggle.
1137
1215
  reopenSessions: map.reopenSessions ?? true,
1216
+ // Off by default: nothing starts itself because someone installed an app.
1217
+ startAtLogin: map.startAtLogin ?? false,
1138
1218
  // Saving iterates over every key in defaults, but loading is this explicit
1139
1219
  // list — so a key missing here round-trips to nothing and its feature is
1140
1220
  // silently inert.
@@ -1187,6 +1267,9 @@ function loadDefaults(d) {
1187
1267
  ...map.headlessRetentionMinutes !== void 0 && {
1188
1268
  headlessRetentionMinutes: map.headlessRetentionMinutes
1189
1269
  },
1270
+ ...map.hasSeededDevServerWorkflow !== void 0 && {
1271
+ hasSeededDevServerWorkflow: map.hasSeededDevServerWorkflow
1272
+ },
1190
1273
  ...map.hasSeededDefaultTaskWorkflow !== void 0 && {
1191
1274
  hasSeededDefaultTaskWorkflow: map.hasSeededDefaultTaskWorkflow
1192
1275
  },
@@ -1250,6 +1333,10 @@ function loadTasks(d) {
1250
1333
  const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
1251
1334
  return rows.map(rowToTask);
1252
1335
  }
1336
+ function loadSessionGroups(d) {
1337
+ const rows = d.prepare('SELECT * FROM session_groups ORDER BY "order"').all();
1338
+ return rows.map(rowToSessionGroup);
1339
+ }
1253
1340
  function loadWorkspaces(d) {
1254
1341
  const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
1255
1342
  return rows.map(rowToWorkspace);
@@ -1489,6 +1576,36 @@ function saveConfig(config) {
1489
1576
  for (const ws of workspaces) {
1490
1577
  insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order, revision);
1491
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
+ }
1492
1609
  d.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)").run(
1493
1610
  CONFIG_REVISION_KEY,
1494
1611
  String(revision)
@@ -1546,6 +1663,16 @@ function rowToWorkflow(r) {
1546
1663
  workspaceId: r.workspace_id ?? "personal"
1547
1664
  };
1548
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
+ }
1549
1676
  function rowToWorkspace(r) {
1550
1677
  return {
1551
1678
  id: r.id,
@@ -1562,8 +1689,8 @@ var ConfigManager = class {
1562
1689
  dbWatcher = null;
1563
1690
  debounceTimer = null;
1564
1691
  cachedConfig = null;
1565
- init(dataDir) {
1566
- initDatabase(dataDir);
1692
+ init(dataDir2) {
1693
+ initDatabase(dataDir2);
1567
1694
  }
1568
1695
  close() {
1569
1696
  this.stopWatchingDb();
@@ -1681,33 +1808,47 @@ var V = {
1681
1808
  url: safeUrl
1682
1809
  };
1683
1810
 
1684
- // src/ws-client.ts
1811
+ // ../server/src/rpc-client.ts
1685
1812
  import fs4 from "fs";
1686
1813
  import path3 from "path";
1687
1814
  import os2 from "os";
1688
1815
  import { execFileSync as execFileSync2 } from "child_process";
1689
1816
  import { WebSocket } from "ws";
1690
- var DATA_DIR = process.env.VORN_DATA_DIR || path3.join(os2.homedir(), ".vorn");
1691
- var PORT_FILE = path3.join(DATA_DIR, "ws-port");
1692
- var LOCAL_TOKEN_FILE = path3.join(DATA_DIR, LOCAL_TOKEN_FILENAME);
1693
- 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()}).
1694
1832
  The server writes it on startup and removes it on shutdown, so this usually means
1695
- Vorn is not running. Start Vorn (or \`vorn-server serve\`) and try again.
1696
- 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
+ }
1697
1838
  function readLocalToken() {
1698
1839
  try {
1699
- const token = fs4.readFileSync(LOCAL_TOKEN_FILE, "utf-8").trim();
1840
+ const token = fs4.readFileSync(localTokenFile(), "utf-8").trim();
1700
1841
  if (!token) throw new Error("empty");
1701
1842
  return token;
1702
1843
  } catch {
1703
- throw new Error(TOKEN_FILE_MISSING_MSG);
1844
+ throw new Error(tokenFileMissingMessage());
1704
1845
  }
1705
1846
  }
1706
1847
  function connection() {
1707
1848
  const result = readPort();
1708
1849
  if (!result.port) {
1709
1850
  const reason = "reason" in result ? result.reason : "missing";
1710
- throw new Error(reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1851
+ throw new Error(reason === "invalid" ? portFileInvalidMessage() : portFileMissingMessage());
1711
1852
  }
1712
1853
  return {
1713
1854
  url: `ws://127.0.0.1:${result.port}/ws`,
@@ -1716,21 +1857,42 @@ function connection() {
1716
1857
  }
1717
1858
  var TIMEOUT_MS = 1e4;
1718
1859
  var IS_WIN = process.platform === "win32";
1719
- 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}).
1720
1863
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1721
- To fix, find the Vorn process and its listening port:
1722
- 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
1723
1866
  Then write the WS port to the file:
1724
- echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vorn\\ws-port
1725
- 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}).
1726
1869
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1727
1870
  To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
1728
1871
  Then write the WS port (the one on *:<port>) to the file:
1729
- echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
1872
+ echo '{"port":<PORT>,"pid":<PID>}' > "${file}"
1730
1873
  Or restart Vorn to regenerate it.`;
1731
- 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}).
1732
1878
  Delete it and restart Vorn, or overwrite it with the correct port:
1733
- ${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
+ }
1734
1896
  var rpcId = 0;
1735
1897
  var cachedPort = null;
1736
1898
  var cacheTimestamp = 0;
@@ -1783,7 +1945,11 @@ function discoverPort() {
1783
1945
  }
1784
1946
  return null;
1785
1947
  }
1948
+ function discoveryAllowed() {
1949
+ return dataDirOverride === void 0 && named(process.env.VORN_DATA_DIR) === void 0;
1950
+ }
1786
1951
  function discoverAndHeal() {
1952
+ if (!discoveryAllowed()) return { port: null, reason: "missing" };
1787
1953
  const now = Date.now();
1788
1954
  if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1789
1955
  const discovered = discoverPort();
@@ -1791,8 +1957,8 @@ function discoverAndHeal() {
1791
1957
  cacheTimestamp = now;
1792
1958
  if (discovered) {
1793
1959
  try {
1794
- fs4.mkdirSync(path3.dirname(PORT_FILE), { recursive: true });
1795
- 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");
1796
1962
  } catch {
1797
1963
  }
1798
1964
  return { port: discovered };
@@ -1801,7 +1967,7 @@ function discoverAndHeal() {
1801
1967
  }
1802
1968
  function readPort() {
1803
1969
  try {
1804
- const raw = fs4.readFileSync(PORT_FILE, "utf-8").trim();
1970
+ const raw = fs4.readFileSync(portFile(), "utf-8").trim();
1805
1971
  if (!raw) return { port: null, reason: "invalid" };
1806
1972
  if (raw.startsWith("{")) {
1807
1973
  const parsed = JSON.parse(raw);
@@ -1843,15 +2009,19 @@ async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
1843
2009
  const msg = JSON.parse(raw.toString());
1844
2010
  if (msg.id !== id) return;
1845
2011
  clearTimeout(timer);
1846
- ws.close();
1847
2012
  if (msg.error) {
1848
- reject(new Error(msg.error.message));
2013
+ reject(new Error(explain(msg.error.message)));
1849
2014
  } else {
1850
2015
  resolve(msg.result);
1851
2016
  }
2017
+ ws.close();
1852
2018
  } catch {
1853
2019
  }
1854
2020
  });
2021
+ ws.on("close", (code) => {
2022
+ clearTimeout(timer);
2023
+ reject(new Error(closedBeforeAnswering(code)));
2024
+ });
1855
2025
  ws.on("error", (err) => {
1856
2026
  clearTimeout(timer);
1857
2027
  reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
@@ -1980,6 +2150,9 @@ async function listWorkflowRunsByTask(taskId, limit = 20) {
1980
2150
  limit
1981
2151
  });
1982
2152
  }
2153
+ async function listRunsWithWaitingGates() {
2154
+ return rpcCall("workflowRun:listWaiting");
2155
+ }
1983
2156
  async function listAllWorkflowRuns(workspaceId, limit = 50) {
1984
2157
  return rpcCall("workflowRun:listAll", {
1985
2158
  workspaceId,
@@ -2790,8 +2963,8 @@ function resolveRequirement(requirement, connections) {
2790
2963
  );
2791
2964
  if (candidates.length === 0) return void 0;
2792
2965
  if (requirement.name !== "") {
2793
- const named = candidates.filter((connection2) => connection2.name === requirement.name);
2794
- 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;
2795
2968
  }
2796
2969
  return candidates.length === 1 ? candidates[0].id : void 0;
2797
2970
  }
@@ -3205,6 +3378,46 @@ function resolveWorkflowId(args) {
3205
3378
  }
3206
3379
  return { id };
3207
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
+ }
3208
3421
  async function listPortableConnections() {
3209
3422
  try {
3210
3423
  return await rpcCall("connection:list", { connectorId: void 0 });
@@ -3358,7 +3571,7 @@ function registerWorkflowTools(server) {
3358
3571
  );
3359
3572
  server.tool(
3360
3573
  "list_workflow_runs",
3361
- "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.",
3362
3575
  {
3363
3576
  workflow_id: V.id.optional().describe("Filter by workflow ID"),
3364
3577
  task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
@@ -3371,28 +3584,28 @@ function registerWorkflowTools(server) {
3371
3584
  isError: true
3372
3585
  };
3373
3586
  }
3587
+ const withGates = async (runs) => runs.some((r) => r.nodeStates.some((n) => n.status === "waiting")) ? annotateWaitingGates(runs, await dbListWorkflows()) : runs;
3374
3588
  if (args.task_id) {
3375
- const runs = await listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
3589
+ const runs = await withGates(await listWorkflowRunsByTask(args.task_id, args.limit ?? 20));
3376
3590
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3377
3591
  }
3378
3592
  if (args.workflow_id) {
3379
- const runs = await listWorkflowRuns(args.workflow_id, args.limit ?? 20);
3593
+ const runs = await withGates(await listWorkflowRuns(args.workflow_id, args.limit ?? 20));
3380
3594
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3381
3595
  }
3382
- return {
3383
- content: [{ type: "text", text: "Error: provide either workflow_id or task_id" }],
3384
- isError: true
3385
- };
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) }] };
3386
3599
  }
3387
3600
  );
3388
3601
  server.tool(
3389
3602
  "stop_workflow_run",
3390
- "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.",
3391
3604
  {
3392
3605
  run_id: V.id.describe("Run ID (from list_workflow_runs)")
3393
3606
  },
3394
3607
  async (args) => {
3395
- const run = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === args.run_id);
3608
+ const run = await runById(args.run_id);
3396
3609
  if (!run) {
3397
3610
  return {
3398
3611
  content: [
@@ -3437,6 +3650,74 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3437
3650
  };
3438
3651
  }
3439
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
+ );
3440
3721
  server.tool(
3441
3722
  "get_workflow_schedule",
3442
3723
  "Get scheduler info for a workflow: execution log or next scheduled run. Requires the Vorn app to be running.",
@@ -3477,7 +3758,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3477
3758
  );
3478
3759
  server.tool(
3479
3760
  "execute_workflow",
3480
- "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.",
3481
3762
  {
3482
3763
  workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
3483
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>}})")
@@ -3796,27 +4077,43 @@ var failure = (message) => ({
3796
4077
  function summarize(entry) {
3797
4078
  return { type: entry.type, label: entry.label };
3798
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
+ }
3799
4092
  function registerConnectorTools(server) {
3800
4093
  server.tool(
3801
4094
  "list_connectors",
3802
- "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.",
3803
4096
  {
3804
- 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")
3805
4099
  },
3806
4100
  async (args) => {
3807
- const [builtIns, snapshot, connections, statuses] = await Promise.all([
4101
+ const [builtIns, snapshot, connections, statuses, packs] = await Promise.all([
3808
4102
  rpcCall("connector:list"),
3809
4103
  rpcCall("connector:catalog"),
3810
4104
  rpcCall("connection:list", { connectorId: void 0 }),
3811
- rpcCall("connector:status")
4105
+ rpcCall("connector:status"),
4106
+ rpcCall("connector:listPacks")
3812
4107
  ]);
3813
4108
  const countFor = (id) => connections.filter((conn) => connectionConnectorId(conn) === id).length;
3814
4109
  const statusFor = (id) => statuses.find((s) => s.connectorId === id);
4110
+ const packFor = (id) => packs.find((pack) => pack.id === id);
3815
4111
  const entries = [
3816
4112
  ...builtIns.map((c) => ({
3817
4113
  id: c.id,
3818
4114
  name: c.name,
3819
4115
  source: "built-in",
4116
+ kind: "connector",
3820
4117
  capabilities: c.capabilities,
3821
4118
  connections: countFor(c.id),
3822
4119
  // Only meaningful for connectors that authenticate up front; the
@@ -3826,25 +4123,49 @@ function registerConnectorTools(server) {
3826
4123
  ...statusFor(c.id).message && { authMessage: statusFor(c.id).message }
3827
4124
  }
3828
4125
  })),
3829
- ...snapshot.items.map((entry) => ({
3830
- id: entry.id,
3831
- name: entry.name,
3832
- source: "package",
3833
- description: entry.description,
3834
- package: entry.packageName,
3835
- ...entry.version && { version: entry.version },
3836
- capabilities: entry.capabilities,
3837
- connections: countFor(entry.id),
3838
- ...entry.auth && { auth: entry.auth },
3839
- // Generated upstream from the connector's own manifest, so an agent
3840
- // can tell whether a connector is worth installing without launching
3841
- // it — which for a list of twenty would be twenty npx processes.
3842
- ...entry.triggers && { triggers: entry.triggers.map(summarize) },
3843
- ...entry.actions && { actions: entry.actions.map(summarize) },
3844
- ...entry.env && { env: entry.env.map((e) => e.name) }
3845
- }))
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
+ })
3846
4162
  ];
3847
- 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
+ );
3848
4169
  }
3849
4170
  );
3850
4171
  server.tool(
@@ -3894,7 +4215,7 @@ function registerConnectorTools(server) {
3894
4215
  );
3895
4216
  server.tool(
3896
4217
  "inspect_connector_package",
3897
- "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.",
3898
4219
  {
3899
4220
  package: V.shortText.describe(
3900
4221
  'npm package name, or a command to run a local build (e.g. "node /path/to/dist/index.js")'
@@ -3908,10 +4229,10 @@ function registerConnectorTools(server) {
3908
4229
  );
3909
4230
  server.tool(
3910
4231
  "install_connector",
3911
- "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.",
3912
4233
  {
3913
4234
  connector_id: V.id.optional().describe("Catalog connector id (from list_connectors). Use this or package."),
3914
- 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"),
3915
4236
  pack_path: V.shortText.optional().describe(
3916
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."
3917
4238
  ),
@@ -3937,6 +4258,38 @@ function registerConnectorTools(server) {
3937
4258
  });
3938
4259
  if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
3939
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
+ });
3940
4293
  }
3941
4294
  const target = installed ? packLaunch(installed) : entry?.launch ?? args.package;
3942
4295
  if (!target) return failure("Provide either connector_id, package, or pack_path.");
@@ -4529,7 +4882,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4529
4882
  console.error = (...args) => _origError("[mcp:error]", ...args);
4530
4883
  async function main() {
4531
4884
  configManager.init();
4532
- const version = true ? "0.7.0-beta.13" : createRequire(import.meta.url)("../package.json").version;
4885
+ const version = true ? "0.7.0-beta.15" : createRequire(import.meta.url)("../package.json").version;
4533
4886
  const server = createMcpServer(version);
4534
4887
  const transport = new StdioServerTransport();
4535
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.13",
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"