@vibegrid/mcp 0.4.0-beta.5 → 0.4.0-beta.7

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 +217 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -177,6 +177,7 @@ function createSchema() {
177
177
  agent_type TEXT PRIMARY KEY,
178
178
  command TEXT NOT NULL,
179
179
  args TEXT NOT NULL DEFAULT '[]',
180
+ headless_args TEXT,
180
181
  fallback_command TEXT,
181
182
  fallback_args TEXT
182
183
  );
@@ -414,6 +415,18 @@ function migrateSchema(d) {
414
415
  })();
415
416
  logger_default.info("[database] migrated schema to version 4 (worktree name)");
416
417
  }
418
+ if (version < 5) {
419
+ d.transaction(() => {
420
+ const agentCols = d.prepare("PRAGMA table_info(agent_commands)").all();
421
+ if (!agentCols.some((c) => c.name === "headless_args")) {
422
+ d.exec("ALTER TABLE agent_commands ADD COLUMN headless_args TEXT");
423
+ }
424
+ d.prepare(
425
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '5')"
426
+ ).run();
427
+ })();
428
+ logger_default.info("[database] migrated schema to version 5 (headless args)");
429
+ }
417
430
  }
418
431
  function verifySchema(d) {
419
432
  const expectedByTable = {
@@ -443,6 +456,12 @@ function verifySchema(d) {
443
456
  ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
444
457
  },
445
458
  { column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" }
459
+ ],
460
+ agent_commands: [
461
+ {
462
+ column: "headless_args",
463
+ ddl: "ALTER TABLE agent_commands ADD COLUMN headless_args TEXT"
464
+ }
446
465
  ]
447
466
  };
448
467
  for (const [table, columns] of Object.entries(expectedByTable)) {
@@ -523,6 +542,12 @@ function loadDefaults(d) {
523
542
  },
524
543
  ...map.networkAccessEnabled !== void 0 && {
525
544
  networkAccessEnabled: map.networkAccessEnabled
545
+ },
546
+ ...map.showHeadlessAgents !== void 0 && {
547
+ showHeadlessAgents: map.showHeadlessAgents
548
+ },
549
+ ...map.headlessRetentionMinutes !== void 0 && {
550
+ headlessRetentionMinutes: map.headlessRetentionMinutes
526
551
  }
527
552
  };
528
553
  }
@@ -541,6 +566,7 @@ function loadAgentCommands(d) {
541
566
  result[r.agent_type] = {
542
567
  command: r.command,
543
568
  args: JSON.parse(r.args),
569
+ ...r.headless_args != null && { headlessArgs: JSON.parse(r.headless_args) },
544
570
  ...r.fallback_command != null && { fallbackCommand: r.fallback_command },
545
571
  ...r.fallback_args != null && { fallbackArgs: JSON.parse(r.fallback_args) }
546
572
  };
@@ -617,7 +643,7 @@ function saveConfig(config) {
617
643
  }
618
644
  d.prepare("DELETE FROM agent_commands").run();
619
645
  const insertAgent = d.prepare(
620
- "INSERT INTO agent_commands (agent_type, command, args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?)"
646
+ "INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?, ?)"
621
647
  );
622
648
  if (config.agentCommands) {
623
649
  for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
@@ -626,6 +652,7 @@ function saveConfig(config) {
626
652
  agentType,
627
653
  cmd.command,
628
654
  JSON.stringify(cmd.args),
655
+ cmd.headlessArgs ? JSON.stringify(cmd.headlessArgs) : null,
629
656
  cmd.fallbackCommand ?? null,
630
657
  cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
631
658
  );
@@ -1072,6 +1099,7 @@ var ConfigManager = class {
1072
1099
  changeCallbacks = [];
1073
1100
  dbWatcher = null;
1074
1101
  debounceTimer = null;
1102
+ cachedConfig = null;
1075
1103
  init() {
1076
1104
  initDatabase();
1077
1105
  }
@@ -1080,8 +1108,11 @@ var ConfigManager = class {
1080
1108
  closeDatabase();
1081
1109
  }
1082
1110
  loadConfig() {
1111
+ if (this.cachedConfig) return this.cachedConfig;
1083
1112
  try {
1084
- return loadConfig();
1113
+ const config = loadConfig();
1114
+ this.cachedConfig = config;
1115
+ return config;
1085
1116
  } catch (err) {
1086
1117
  logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
1087
1118
  return {
@@ -1101,6 +1132,7 @@ var ConfigManager = class {
1101
1132
  saveConfig(config) {
1102
1133
  try {
1103
1134
  saveConfig(config);
1135
+ this.cachedConfig = null;
1104
1136
  } catch (err) {
1105
1137
  logger_default.error("[config-manager] saveConfig failed:", err);
1106
1138
  throw err;
@@ -1112,6 +1144,7 @@ var ConfigManager = class {
1112
1144
  }
1113
1145
  /** Notify all registered callbacks (call after main-process config mutations) */
1114
1146
  notifyChanged() {
1147
+ this.cachedConfig = null;
1115
1148
  const config = this.loadConfig();
1116
1149
  for (const cb of this.changeCallbacks) {
1117
1150
  cb(config);
@@ -1548,32 +1581,128 @@ import { z as z4 } from "zod";
1548
1581
  import fs3 from "fs";
1549
1582
  import path4 from "path";
1550
1583
  import os3 from "os";
1584
+ import { execFileSync } from "child_process";
1551
1585
  import { WebSocket } from "ws";
1552
1586
  var PORT_FILE = path4.join(os3.homedir(), ".vibegrid", "ws-port");
1553
1587
  var TIMEOUT_MS = 1e4;
1588
+ var IS_WIN = process.platform === "win32";
1589
+ var PORT_FILE_MISSING_MSG = IS_WIN ? `VibeGrid port file not found (~/.vibegrid/ws-port).
1590
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1591
+ To fix, find the VibeGrid process and its listening port:
1592
+ powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process VibeGrid).Id | Select LocalPort"
1593
+ Then write the WS port to the file:
1594
+ echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vibegrid\\ws-port
1595
+ Or restart VibeGrid to regenerate it.` : `VibeGrid port file not found (~/.vibegrid/ws-port).
1596
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1597
+ To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep VibeGrid
1598
+ Then write the WS port (the one on *:<port>) to the file:
1599
+ echo '{"port":<PORT>,"pid":<PID>}' > ~/.vibegrid/ws-port
1600
+ Or restart VibeGrid to regenerate it.`;
1601
+ var PORT_FILE_INVALID_MSG = `VibeGrid port file exists but contains invalid data (~/.vibegrid/ws-port).
1602
+ Delete it and restart VibeGrid, or overwrite it with the correct port:
1603
+ ${IS_WIN ? "del %USERPROFILE%\\.vibegrid\\ws-port" : "rm ~/.vibegrid/ws-port"}`;
1554
1604
  var rpcId = 0;
1605
+ var cachedPort = null;
1606
+ var cacheTimestamp = 0;
1607
+ var CACHE_TTL_MS = 5e3;
1608
+ var EXEC_OPTS = {
1609
+ encoding: "utf-8",
1610
+ timeout: 5e3,
1611
+ stdio: ["pipe", "pipe", "pipe"]
1612
+ };
1613
+ function discoverPort() {
1614
+ try {
1615
+ if (IS_WIN) {
1616
+ const taskOut = execFileSync(
1617
+ "tasklist",
1618
+ ["/FI", "IMAGENAME eq VibeGrid.exe", "/FO", "CSV", "/NH"],
1619
+ EXEC_OPTS
1620
+ );
1621
+ const pidMatch = taskOut.match(/"VibeGrid\.exe","(\d+)"/);
1622
+ if (!pidMatch) return null;
1623
+ const pid = pidMatch[1];
1624
+ const lines = execFileSync("netstat", ["-ano"], EXEC_OPTS).split("\n");
1625
+ let fallback = null;
1626
+ for (const line of lines) {
1627
+ if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
1628
+ const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
1629
+ if (!m) continue;
1630
+ if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
1631
+ fallback ??= parseInt(m[1], 10);
1632
+ }
1633
+ return fallback;
1634
+ } else {
1635
+ const lines = execFileSync("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
1636
+ "\n"
1637
+ );
1638
+ let fallback = null;
1639
+ for (const line of lines) {
1640
+ if (!line.includes("VibeGrid")) continue;
1641
+ if (line.includes("*:")) {
1642
+ const m = line.match(/\*:(\d+)/);
1643
+ if (m) return parseInt(m[1], 10);
1644
+ }
1645
+ if (!fallback) {
1646
+ const m = line.match(/:(\d+)\s/);
1647
+ if (m) fallback = parseInt(m[1], 10);
1648
+ }
1649
+ }
1650
+ return fallback;
1651
+ }
1652
+ } catch {
1653
+ }
1654
+ return null;
1655
+ }
1656
+ function discoverAndHeal() {
1657
+ const now = Date.now();
1658
+ if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1659
+ const discovered = discoverPort();
1660
+ cachedPort = discovered;
1661
+ cacheTimestamp = now;
1662
+ if (discovered) {
1663
+ try {
1664
+ fs3.mkdirSync(path4.dirname(PORT_FILE), { recursive: true });
1665
+ fs3.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
1666
+ } catch {
1667
+ }
1668
+ return { port: discovered };
1669
+ }
1670
+ return { port: null, reason: "missing" };
1671
+ }
1555
1672
  function readPort() {
1556
1673
  try {
1557
1674
  const raw = fs3.readFileSync(PORT_FILE, "utf-8").trim();
1558
- if (!raw) return null;
1675
+ if (!raw) return { port: null, reason: "invalid" };
1559
1676
  if (raw.startsWith("{")) {
1560
1677
  const parsed = JSON.parse(raw);
1561
- const port2 = parsed?.port;
1562
- return typeof port2 === "number" && Number.isFinite(port2) && port2 > 0 ? port2 : null;
1678
+ const p2 = parsed?.port;
1679
+ const pid = parsed?.pid;
1680
+ if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
1681
+ return { port: null, reason: "invalid" };
1682
+ }
1683
+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
1684
+ try {
1685
+ process.kill(pid, 0);
1686
+ } catch (err) {
1687
+ if (err.code === "EPERM") return { port: p2 };
1688
+ return discoverAndHeal();
1689
+ }
1690
+ }
1691
+ return { port: p2 };
1563
1692
  }
1564
- const port = parseInt(raw, 10);
1565
- return Number.isFinite(port) && port > 0 ? port : null;
1693
+ const p = parseInt(raw, 10);
1694
+ return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
1566
1695
  } catch {
1567
- return null;
1696
+ return discoverAndHeal();
1568
1697
  }
1569
1698
  }
1570
1699
  async function rpcCall(method, params) {
1571
- const port = readPort();
1572
- if (!port) {
1573
- throw new Error("VibeGrid app is not running. Start VibeGrid to use session management tools.");
1700
+ const result = readPort();
1701
+ if (!result.port) {
1702
+ throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1574
1703
  }
1575
1704
  return new Promise((resolve, reject) => {
1576
- const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1705
+ const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
1577
1706
  const id = ++rpcId;
1578
1707
  const timer = setTimeout(() => {
1579
1708
  ws.close();
@@ -1603,12 +1732,12 @@ async function rpcCall(method, params) {
1603
1732
  });
1604
1733
  }
1605
1734
  async function rpcNotify(method, params) {
1606
- const port = readPort();
1607
- if (!port) {
1608
- throw new Error("VibeGrid app is not running. Start VibeGrid to use session management tools.");
1735
+ const result = readPort();
1736
+ if (!result.port) {
1737
+ throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1609
1738
  }
1610
1739
  return new Promise((resolve, reject) => {
1611
- const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1740
+ const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
1612
1741
  ws.on("open", () => {
1613
1742
  ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
1614
1743
  ws.close();
@@ -1844,12 +1973,12 @@ function registerSessionTools(server) {
1844
1973
  "Send input to a running terminal session. Requires the VibeGrid app to be running.",
1845
1974
  {
1846
1975
  id: V.id.describe("Session ID"),
1847
- data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)")
1976
+ data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)"),
1977
+ raw: z4.boolean().optional().describe("Send data as-is without appending carriage return (for raw terminal control)")
1848
1978
  },
1849
1979
  async (args) => {
1850
1980
  try {
1851
- const trimmed = args.data.replace(/[\r\n]+$/, "");
1852
- const data = trimmed + "\r";
1981
+ const data = args.raw ? args.data : args.data.replace(/[\r\n]+$/, "") + "\r";
1853
1982
  await rpcNotify("terminal:write", { id: args.id, data });
1854
1983
  return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
1855
1984
  } catch (err) {
@@ -1865,6 +1994,73 @@ function registerSessionTools(server) {
1865
1994
  }
1866
1995
  }
1867
1996
  );
1997
+ const KEY_MAP = {
1998
+ enter: "\r",
1999
+ escape: "\x1B",
2000
+ esc: "\x1B",
2001
+ tab: " ",
2002
+ "shift+tab": "\x1B[Z",
2003
+ up: "\x1B[A",
2004
+ down: "\x1B[B",
2005
+ left: "\x1B[D",
2006
+ right: "\x1B[C",
2007
+ backspace: "\x7F",
2008
+ delete: "\x1B[3~",
2009
+ home: "\x1B[H",
2010
+ end: "\x1B[F",
2011
+ "ctrl+c": "",
2012
+ "ctrl+d": "",
2013
+ "ctrl+x": "",
2014
+ "ctrl+z": ""
2015
+ };
2016
+ server.tool(
2017
+ "send_key",
2018
+ "Send a single keystroke or key combo to a terminal session without appending Enter. Use for TUI interactions like selecting menu options (1, 2, y, n), pressing Escape, Ctrl+C, arrow keys, etc.",
2019
+ {
2020
+ id: V.id.describe("Session ID"),
2021
+ key: z4.string().min(1).max(20).describe(
2022
+ "Key to send: single char (1, y, n), named key (enter, escape, tab, up, down, left, right, backspace, delete, home, end), or combo (ctrl+c, ctrl+d, ctrl+x, ctrl+z, shift+tab)"
2023
+ )
2024
+ },
2025
+ async (args) => {
2026
+ const key = args.key.toLowerCase().trim();
2027
+ let data = KEY_MAP[key];
2028
+ if (!data) {
2029
+ const ctrlMatch = key.match(/^ctrl\+([a-z])$/);
2030
+ if (ctrlMatch) {
2031
+ data = String.fromCharCode(ctrlMatch[1].toUpperCase().charCodeAt(0) - 64);
2032
+ } else if (args.key.length === 1) {
2033
+ data = args.key;
2034
+ } else {
2035
+ return {
2036
+ content: [
2037
+ {
2038
+ type: "text",
2039
+ text: `Unknown key: "${args.key}". Supported: single chars (1, y, n), named keys (${Object.keys(KEY_MAP).join(", ")}), or ctrl+<letter>.`
2040
+ }
2041
+ ],
2042
+ isError: true
2043
+ };
2044
+ }
2045
+ }
2046
+ try {
2047
+ await rpcNotify("terminal:write", { id: args.id, data });
2048
+ return {
2049
+ content: [{ type: "text", text: `Sent key "${args.key}" to session: ${args.id}` }]
2050
+ };
2051
+ } catch (err) {
2052
+ return {
2053
+ content: [
2054
+ {
2055
+ type: "text",
2056
+ text: `Error sending key to terminal: ${err instanceof Error ? err.message : err}`
2057
+ }
2058
+ ],
2059
+ isError: true
2060
+ };
2061
+ }
2062
+ }
2063
+ );
1868
2064
  server.tool(
1869
2065
  "list_session_events",
1870
2066
  "List session lifecycle events (created, exited, task_linked, renamed, archived, unarchived). Use for post-mortem analysis and multi-agent coordination.",
@@ -2025,7 +2221,7 @@ function registerWorkflowTools(server) {
2025
2221
  const workflow = {
2026
2222
  id: crypto2.randomUUID(),
2027
2223
  name: args.name,
2028
- icon: args.icon ?? "zap",
2224
+ icon: args.icon ?? "Zap",
2029
2225
  iconColor: args.icon_color ?? "#6366f1",
2030
2226
  nodes,
2031
2227
  edges,
@@ -2280,7 +2476,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
2280
2476
  console.error = (...args) => _origError("[mcp:error]", ...args);
2281
2477
  async function main() {
2282
2478
  configManager.init();
2283
- const version = true ? "0.4.0-beta.5" : createRequire(import.meta.url)("../package.json").version;
2479
+ const version = true ? "0.4.0-beta.7" : createRequire(import.meta.url)("../package.json").version;
2284
2480
  const server = createMcpServer(version);
2285
2481
  const transport = new StdioServerTransport();
2286
2482
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibegrid/mcp",
3
- "version": "0.4.0-beta.5",
3
+ "version": "0.4.0-beta.7",
4
4
  "description": "VibeGrid MCP server — task management, git, and workflow tools for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",