@vornrun/mcp 0.7.0-beta.9 → 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 +506 -87
  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,
@@ -451,6 +508,10 @@ function createSchema() {
451
508
  output TEXT,
452
509
  structured_output TEXT,
453
510
  iteration INTEGER,
511
+ worktree_path TEXT,
512
+ worktree_name TEXT,
513
+ worktree_origin TEXT,
514
+ waiting_for TEXT,
454
515
  FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
455
516
  );
456
517
 
@@ -691,7 +752,9 @@ function migrateSchema(d) {
691
752
  last_sync_at TEXT,
692
753
  last_sync_error TEXT,
693
754
  sync_cursor TEXT,
694
- created_at TEXT NOT NULL
755
+ created_at TEXT NOT NULL,
756
+ signed_in_as TEXT,
757
+ signed_in_at TEXT
695
758
  )
696
759
  `);
697
760
  d.exec(`
@@ -937,11 +1000,52 @@ function migrateSchema(d) {
937
1000
  })();
938
1001
  logger_default.info("[database] migrated schema to version 17 (packaged connector task ids)");
939
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
+ }
940
1043
  }
941
1044
  var REVISIONED_TABLES = [
942
1045
  "projects",
943
1046
  "tasks",
944
1047
  "workspaces",
1048
+ "session_groups",
945
1049
  "remote_hosts",
946
1050
  "agent_commands"
947
1051
  ];
@@ -978,9 +1082,15 @@ function verifySchema(d) {
978
1082
  column: "sort_order",
979
1083
  ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
980
1084
  },
1085
+ { column: "group_id", ddl: "ALTER TABLE sessions ADD COLUMN group_id TEXT" },
981
1086
  { column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
982
1087
  { 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" }
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
+ }
984
1094
  ],
985
1095
  agent_commands: [
986
1096
  {
@@ -1014,6 +1124,7 @@ function verifySchema(d) {
1014
1124
  }
1015
1125
  ],
1016
1126
  workflow_run_nodes: [
1127
+ { column: "waiting_for", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN waiting_for TEXT" },
1017
1128
  { column: "agent_type", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN agent_type TEXT" },
1018
1129
  {
1019
1130
  column: "project_name",
@@ -1093,6 +1204,7 @@ function loadConfig() {
1093
1204
  const remoteHosts = loadRemoteHosts(d);
1094
1205
  const tasks = loadTasks(d);
1095
1206
  const workspaces = loadWorkspaces(d);
1207
+ const sessionGroups = loadSessionGroups(d);
1096
1208
  return {
1097
1209
  version: 1,
1098
1210
  revision: readConfigRevision(d),
@@ -1102,7 +1214,8 @@ function loadConfig() {
1102
1214
  workflows,
1103
1215
  remoteHosts,
1104
1216
  tasks,
1105
- workspaces
1217
+ workspaces,
1218
+ sessionGroups
1106
1219
  };
1107
1220
  }
1108
1221
  function loadDefaults(d) {
@@ -1135,6 +1248,8 @@ function loadDefaults(d) {
1135
1248
  // terminal drew and waits. There is nothing to ask, and leaving it off made
1136
1249
  // the whole thing invisible unless somebody went looking for a toggle.
1137
1250
  reopenSessions: map.reopenSessions ?? true,
1251
+ // Off by default: nothing starts itself because someone installed an app.
1252
+ startAtLogin: map.startAtLogin ?? false,
1138
1253
  // Saving iterates over every key in defaults, but loading is this explicit
1139
1254
  // list — so a key missing here round-trips to nothing and its feature is
1140
1255
  // silently inert.
@@ -1187,6 +1302,9 @@ function loadDefaults(d) {
1187
1302
  ...map.headlessRetentionMinutes !== void 0 && {
1188
1303
  headlessRetentionMinutes: map.headlessRetentionMinutes
1189
1304
  },
1305
+ ...map.hasSeededDevServerWorkflow !== void 0 && {
1306
+ hasSeededDevServerWorkflow: map.hasSeededDevServerWorkflow
1307
+ },
1190
1308
  ...map.hasSeededDefaultTaskWorkflow !== void 0 && {
1191
1309
  hasSeededDefaultTaskWorkflow: map.hasSeededDefaultTaskWorkflow
1192
1310
  },
@@ -1250,6 +1368,10 @@ function loadTasks(d) {
1250
1368
  const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
1251
1369
  return rows.map(rowToTask);
1252
1370
  }
1371
+ function loadSessionGroups(d) {
1372
+ const rows = d.prepare('SELECT * FROM session_groups ORDER BY "order"').all();
1373
+ return rows.map(rowToSessionGroup);
1374
+ }
1253
1375
  function loadWorkspaces(d) {
1254
1376
  const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
1255
1377
  return rows.map(rowToWorkspace);
@@ -1489,6 +1611,36 @@ function saveConfig(config) {
1489
1611
  for (const ws of workspaces) {
1490
1612
  insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order, revision);
1491
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
+ }
1492
1644
  d.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)").run(
1493
1645
  CONFIG_REVISION_KEY,
1494
1646
  String(revision)
@@ -1546,6 +1698,16 @@ function rowToWorkflow(r) {
1546
1698
  workspaceId: r.workspace_id ?? "personal"
1547
1699
  };
1548
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
+ }
1549
1711
  function rowToWorkspace(r) {
1550
1712
  return {
1551
1713
  id: r.id,
@@ -1562,8 +1724,8 @@ var ConfigManager = class {
1562
1724
  dbWatcher = null;
1563
1725
  debounceTimer = null;
1564
1726
  cachedConfig = null;
1565
- init(dataDir) {
1566
- initDatabase(dataDir);
1727
+ init(dataDir2) {
1728
+ initDatabase(dataDir2);
1567
1729
  }
1568
1730
  close() {
1569
1731
  this.stopWatchingDb();
@@ -1681,33 +1843,47 @@ var V = {
1681
1843
  url: safeUrl
1682
1844
  };
1683
1845
 
1684
- // src/ws-client.ts
1846
+ // ../server/src/rpc-client.ts
1685
1847
  import fs4 from "fs";
1686
1848
  import path3 from "path";
1687
1849
  import os2 from "os";
1688
1850
  import { execFileSync as execFileSync2 } from "child_process";
1689
1851
  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}).
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()}).
1694
1867
  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.`;
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
+ }
1697
1873
  function readLocalToken() {
1698
1874
  try {
1699
- const token = fs4.readFileSync(LOCAL_TOKEN_FILE, "utf-8").trim();
1875
+ const token = fs4.readFileSync(localTokenFile(), "utf-8").trim();
1700
1876
  if (!token) throw new Error("empty");
1701
1877
  return token;
1702
1878
  } catch {
1703
- throw new Error(TOKEN_FILE_MISSING_MSG);
1879
+ throw new Error(tokenFileMissingMessage());
1704
1880
  }
1705
1881
  }
1706
1882
  function connection() {
1707
1883
  const result = readPort();
1708
1884
  if (!result.port) {
1709
1885
  const reason = "reason" in result ? result.reason : "missing";
1710
- throw new Error(reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1886
+ throw new Error(reason === "invalid" ? portFileInvalidMessage() : portFileMissingMessage());
1711
1887
  }
1712
1888
  return {
1713
1889
  url: `ws://127.0.0.1:${result.port}/ws`,
@@ -1716,21 +1892,42 @@ function connection() {
1716
1892
  }
1717
1893
  var TIMEOUT_MS = 1e4;
1718
1894
  var IS_WIN = process.platform === "win32";
1719
- 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}).
1720
1898
  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"
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
1723
1901
  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).
1902
+ '{"port":<PORT>,"pid":<PID>}' | Set-Content -Path "${file}"
1903
+ Or restart Vorn to regenerate it.` : `Vorn port file not found (${file}).
1726
1904
  The app may be running but the port file was deleted (e.g. by another instance shutting down).
1727
1905
  To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep Vorn
1728
1906
  Then write the WS port (the one on *:<port>) to the file:
1729
- echo '{"port":<PORT>,"pid":<PID>}' > ~/.vorn/ws-port
1907
+ echo '{"port":<PORT>,"pid":<PID>}' > "${file}"
1730
1908
  Or restart Vorn to regenerate it.`;
1731
- 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}).
1732
1913
  Delete it and restart Vorn, or overwrite it with the correct port:
1733
- ${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
+ }
1734
1931
  var rpcId = 0;
1735
1932
  var cachedPort = null;
1736
1933
  var cacheTimestamp = 0;
@@ -1783,7 +1980,11 @@ function discoverPort() {
1783
1980
  }
1784
1981
  return null;
1785
1982
  }
1983
+ function discoveryAllowed() {
1984
+ return dataDirOverride === void 0 && named(process.env.VORN_DATA_DIR) === void 0;
1985
+ }
1786
1986
  function discoverAndHeal() {
1987
+ if (!discoveryAllowed()) return { port: null, reason: "missing" };
1787
1988
  const now = Date.now();
1788
1989
  if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1789
1990
  const discovered = discoverPort();
@@ -1791,8 +1992,8 @@ function discoverAndHeal() {
1791
1992
  cacheTimestamp = now;
1792
1993
  if (discovered) {
1793
1994
  try {
1794
- fs4.mkdirSync(path3.dirname(PORT_FILE), { recursive: true });
1795
- 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");
1796
1997
  } catch {
1797
1998
  }
1798
1999
  return { port: discovered };
@@ -1801,7 +2002,7 @@ function discoverAndHeal() {
1801
2002
  }
1802
2003
  function readPort() {
1803
2004
  try {
1804
- const raw = fs4.readFileSync(PORT_FILE, "utf-8").trim();
2005
+ const raw = fs4.readFileSync(portFile(), "utf-8").trim();
1805
2006
  if (!raw) return { port: null, reason: "invalid" };
1806
2007
  if (raw.startsWith("{")) {
1807
2008
  const parsed = JSON.parse(raw);
@@ -1843,15 +2044,19 @@ async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
1843
2044
  const msg = JSON.parse(raw.toString());
1844
2045
  if (msg.id !== id) return;
1845
2046
  clearTimeout(timer);
1846
- ws.close();
1847
2047
  if (msg.error) {
1848
- reject(new Error(msg.error.message));
2048
+ reject(new Error(explain(msg.error.message)));
1849
2049
  } else {
1850
2050
  resolve(msg.result);
1851
2051
  }
2052
+ ws.close();
1852
2053
  } catch {
1853
2054
  }
1854
2055
  });
2056
+ ws.on("close", (code) => {
2057
+ clearTimeout(timer);
2058
+ reject(new Error(closedBeforeAnswering(code)));
2059
+ });
1855
2060
  ws.on("error", (err) => {
1856
2061
  clearTimeout(timer);
1857
2062
  reject(new Error(`Cannot connect to Vorn server: ${err.message}. Is the app running?`));
@@ -1980,6 +2185,9 @@ async function listWorkflowRunsByTask(taskId, limit = 20) {
1980
2185
  limit
1981
2186
  });
1982
2187
  }
2188
+ async function listRunsWithWaitingGates() {
2189
+ return rpcCall("workflowRun:listWaiting");
2190
+ }
1983
2191
  async function listAllWorkflowRuns(workspaceId, limit = 50) {
1984
2192
  return rpcCall("workflowRun:listAll", {
1985
2193
  workspaceId,
@@ -2750,6 +2958,13 @@ function registerSessionTools(server) {
2750
2958
 
2751
2959
  // src/tools/workflows.ts
2752
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
2753
2968
  import { z as z5 } from "zod";
2754
2969
 
2755
2970
  // ../shared/src/workflow-portability.ts
@@ -2780,16 +2995,18 @@ function boundConnectionKey(node, config) {
2780
2995
  if (node.type === "trigger" && config.triggerType === "connectorPoll") return "connectionId";
2781
2996
  if (node.type === "callConnectorAction") return "connectionId";
2782
2997
  if (node.type === "httpRequest") return "profileConnectionId";
2998
+ if (node.type === "script") return "secretsFrom";
2783
2999
  return null;
2784
3000
  }
3001
+ var OPTIONAL_CONNECTION_KEYS = /* @__PURE__ */ new Set(["profileConnectionId", "secretsFrom"]);
2785
3002
  function resolveRequirement(requirement, connections) {
2786
3003
  const candidates = connections.filter(
2787
3004
  (connection2) => requirement.kind === "httpProfile" ? connectorOf(connection2) === HTTP_PROFILE_CONNECTOR : requirement.connectorId !== "" && connectorOf(connection2) === requirement.connectorId
2788
3005
  );
2789
3006
  if (candidates.length === 0) return void 0;
2790
3007
  if (requirement.name !== "") {
2791
- const named = candidates.filter((connection2) => connection2.name === requirement.name);
2792
- 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;
2793
3010
  }
2794
3011
  return candidates.length === 1 ? candidates[0].id : void 0;
2795
3012
  }
@@ -2810,11 +3027,10 @@ function toPortable(workflow, projectPath, connections = []) {
2810
3027
  config[`projectName`] = PROJECT_NAME_TOKEN;
2811
3028
  }
2812
3029
  delete config.remoteHostId;
2813
- delete config.secretsFrom;
2814
3030
  }
2815
3031
  const key = boundConnectionKey(node, config);
2816
3032
  const bound = key === null ? "" : config[key];
2817
- const unbound = key !== null && key !== "profileConnectionId" && bound === "";
3033
+ const unbound = key !== null && !OPTIONAL_CONNECTION_KEYS.has(key) && bound === "";
2818
3034
  if (key !== null && (typeof bound === "string" && bound !== "" || unbound)) {
2819
3035
  const source2 = connections.find((connection2) => connection2.id === bound);
2820
3036
  const event = config.event;
@@ -2825,10 +3041,11 @@ function toPortable(workflow, projectPath, connections = []) {
2825
3041
  nodeId: node.id,
2826
3042
  connectorId: source2 ? connectorOf(source2) : typeof declared === "string" ? declared : "",
2827
3043
  name: source2?.name ?? "",
2828
- ...typeof event === "string" && event !== "" && { event }
3044
+ ...typeof event === "string" && event !== "" && { event },
3045
+ ...key === "secretsFrom" && { key }
2829
3046
  }
2830
3047
  );
2831
- if (key === "profileConnectionId") delete config[key];
3048
+ if (OPTIONAL_CONNECTION_KEYS.has(key)) delete config[key];
2832
3049
  else config[key] = "";
2833
3050
  }
2834
3051
  return { ...node, config };
@@ -2859,9 +3076,12 @@ function replacePath(value, projectPath) {
2859
3076
  function unresolvedRequirements(portable, connections) {
2860
3077
  const present = new Set(portable.nodes.map((node) => node.id));
2861
3078
  return (portable.requires ?? []).filter(
2862
- (requirement) => present.has(requirement.nodeId) && resolveRequirement(requirement, connections) === void 0
3079
+ (requirement) => present.has(requirement.nodeId) && (bindsOnlyByHand(requirement) || resolveRequirement(requirement, connections) === void 0)
2863
3080
  );
2864
3081
  }
3082
+ function bindsOnlyByHand(requirement) {
3083
+ return requirement.kind === "connection" && requirement.key === "secretsFrom";
3084
+ }
2865
3085
  function fromPortable(portable, bundle, project, connections = [], mintToken = () => crypto.randomUUID()) {
2866
3086
  const bindings = /* @__PURE__ */ new Map();
2867
3087
  for (const requirement of portable.requires ?? []) {
@@ -2869,16 +3089,16 @@ function fromPortable(portable, bundle, project, connections = [], mintToken = (
2869
3089
  }
2870
3090
  const nodes = portable.nodes.map((node) => {
2871
3091
  const config = { ...node.config };
2872
- delete config.secretsFrom;
2873
- 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)) {
2874
3095
  if (typeof value !== "string") continue;
2875
- 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);
2876
3097
  }
2877
3098
  for (const requirement of bindings.get(node.id) ?? []) {
3099
+ if (bindsOnlyByHand(requirement) || key === null) continue;
2878
3100
  const resolved = resolveRequirement(requirement, connections);
2879
- if (resolved === void 0) continue;
2880
- if (requirement.kind === "httpProfile") config.profileConnectionId = resolved;
2881
- else config.connectionId = resolved;
3101
+ if (resolved !== void 0) config[key] = resolved;
2882
3102
  }
2883
3103
  if (node.type === "trigger" && config.triggerType === "webhook" && !config.token) {
2884
3104
  config.token = mintToken();
@@ -3200,6 +3420,64 @@ function resolveWorkflowId(args) {
3200
3420
  }
3201
3421
  return { id };
3202
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
+ }
3203
3481
  async function listPortableConnections() {
3204
3482
  try {
3205
3483
  return await rpcCall("connection:list", { connectorId: void 0 });
@@ -3353,7 +3631,7 @@ function registerWorkflowTools(server) {
3353
3631
  );
3354
3632
  server.tool(
3355
3633
  "list_workflow_runs",
3356
- "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.",
3357
3635
  {
3358
3636
  workflow_id: V.id.optional().describe("Filter by workflow ID"),
3359
3637
  task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
@@ -3366,28 +3644,28 @@ function registerWorkflowTools(server) {
3366
3644
  isError: true
3367
3645
  };
3368
3646
  }
3647
+ const withGates = async (runs) => runs.some((r) => r.nodeStates.some((n) => n.status === "waiting")) ? annotateWaitingGates(runs, await dbListWorkflows()) : runs;
3369
3648
  if (args.task_id) {
3370
- const runs = await listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
3649
+ const runs = await withGates(await listWorkflowRunsByTask(args.task_id, args.limit ?? 20));
3371
3650
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3372
3651
  }
3373
3652
  if (args.workflow_id) {
3374
- const runs = await listWorkflowRuns(args.workflow_id, args.limit ?? 20);
3653
+ const runs = await withGates(await listWorkflowRuns(args.workflow_id, args.limit ?? 20));
3375
3654
  return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
3376
3655
  }
3377
- return {
3378
- content: [{ type: "text", text: "Error: provide either workflow_id or task_id" }],
3379
- isError: true
3380
- };
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) }] };
3381
3659
  }
3382
3660
  );
3383
3661
  server.tool(
3384
3662
  "stop_workflow_run",
3385
- "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.",
3386
3664
  {
3387
3665
  run_id: V.id.describe("Run ID (from list_workflow_runs)")
3388
3666
  },
3389
3667
  async (args) => {
3390
- const run = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === args.run_id);
3668
+ const run = await runById(args.run_id);
3391
3669
  if (!run) {
3392
3670
  return {
3393
3671
  content: [
@@ -3432,6 +3710,74 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3432
3710
  };
3433
3711
  }
3434
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
+ );
3435
3781
  server.tool(
3436
3782
  "get_workflow_schedule",
3437
3783
  "Get scheduler info for a workflow: execution log or next scheduled run. Requires the Vorn app to be running.",
@@ -3472,7 +3818,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3472
3818
  );
3473
3819
  server.tool(
3474
3820
  "execute_workflow",
3475
- "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.",
3476
3822
  {
3477
3823
  workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
3478
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>}})")
@@ -3791,27 +4137,43 @@ var failure = (message) => ({
3791
4137
  function summarize(entry) {
3792
4138
  return { type: entry.type, label: entry.label };
3793
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
+ }
3794
4152
  function registerConnectorTools(server) {
3795
4153
  server.tool(
3796
4154
  "list_connectors",
3797
- "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.",
3798
4156
  {
3799
- 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")
3800
4159
  },
3801
4160
  async (args) => {
3802
- const [builtIns, snapshot, connections, statuses] = await Promise.all([
4161
+ const [builtIns, snapshot, connections, statuses, packs] = await Promise.all([
3803
4162
  rpcCall("connector:list"),
3804
4163
  rpcCall("connector:catalog"),
3805
4164
  rpcCall("connection:list", { connectorId: void 0 }),
3806
- rpcCall("connector:status")
4165
+ rpcCall("connector:status"),
4166
+ rpcCall("connector:listPacks")
3807
4167
  ]);
3808
4168
  const countFor = (id) => connections.filter((conn) => connectionConnectorId(conn) === id).length;
3809
4169
  const statusFor = (id) => statuses.find((s) => s.connectorId === id);
4170
+ const packFor = (id) => packs.find((pack) => pack.id === id);
3810
4171
  const entries = [
3811
4172
  ...builtIns.map((c) => ({
3812
4173
  id: c.id,
3813
4174
  name: c.name,
3814
4175
  source: "built-in",
4176
+ kind: "connector",
3815
4177
  capabilities: c.capabilities,
3816
4178
  connections: countFor(c.id),
3817
4179
  // Only meaningful for connectors that authenticate up front; the
@@ -3821,25 +4183,49 @@ function registerConnectorTools(server) {
3821
4183
  ...statusFor(c.id).message && { authMessage: statusFor(c.id).message }
3822
4184
  }
3823
4185
  })),
3824
- ...snapshot.items.map((entry) => ({
3825
- id: entry.id,
3826
- name: entry.name,
3827
- source: "package",
3828
- description: entry.description,
3829
- package: entry.packageName,
3830
- ...entry.version && { version: entry.version },
3831
- capabilities: entry.capabilities,
3832
- connections: countFor(entry.id),
3833
- ...entry.auth && { auth: entry.auth },
3834
- // Generated upstream from the connector's own manifest, so an agent
3835
- // can tell whether a connector is worth installing without launching
3836
- // it — which for a list of twenty would be twenty npx processes.
3837
- ...entry.triggers && { triggers: entry.triggers.map(summarize) },
3838
- ...entry.actions && { actions: entry.actions.map(summarize) },
3839
- ...entry.env && { env: entry.env.map((e) => e.name) }
3840
- }))
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
+ })
3841
4222
  ];
3842
- 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
+ );
3843
4229
  }
3844
4230
  );
3845
4231
  server.tool(
@@ -3889,7 +4275,7 @@ function registerConnectorTools(server) {
3889
4275
  );
3890
4276
  server.tool(
3891
4277
  "inspect_connector_package",
3892
- "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.",
3893
4279
  {
3894
4280
  package: V.shortText.describe(
3895
4281
  'npm package name, or a command to run a local build (e.g. "node /path/to/dist/index.js")'
@@ -3903,10 +4289,10 @@ function registerConnectorTools(server) {
3903
4289
  );
3904
4290
  server.tool(
3905
4291
  "install_connector",
3906
- "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.",
3907
4293
  {
3908
4294
  connector_id: V.id.optional().describe("Catalog connector id (from list_connectors). Use this or package."),
3909
- 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"),
3910
4296
  pack_path: V.shortText.optional().describe(
3911
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."
3912
4298
  ),
@@ -3932,6 +4318,38 @@ function registerConnectorTools(server) {
3932
4318
  });
3933
4319
  if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
3934
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
+ });
3935
4353
  }
3936
4354
  const target = installed ? packLaunch(installed) : entry?.launch ?? args.package;
3937
4355
  if (!target) return failure("Provide either connector_id, package, or pack_path.");
@@ -4330,6 +4748,7 @@ function registerDeviceTools(server) {
4330
4748
  sessionId: id,
4331
4749
  udid: args.udid
4332
4750
  });
4751
+ if (!r.ok) throw new Error(r.message);
4333
4752
  return {
4334
4753
  content: [{ type: "text", text: `Claimed ${r.name} (${r.udid}).` }]
4335
4754
  };
@@ -4523,7 +4942,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4523
4942
  console.error = (...args) => _origError("[mcp:error]", ...args);
4524
4943
  async function main() {
4525
4944
  configManager.init();
4526
- const version = true ? "0.7.0-beta.9" : createRequire(import.meta.url)("../package.json").version;
4945
+ const version = true ? "0.7.0" : createRequire(import.meta.url)("../package.json").version;
4527
4946
  const server = createMcpServer(version);
4528
4947
  const transport = new StdioServerTransport();
4529
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.9",
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.9",
42
- "@vornrun/shared": "0.7.0-beta.9",
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"