@vibegrid/mcp 0.4.0-beta.4 → 0.4.0-beta.6

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 +214 -18
  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)) {
@@ -541,6 +560,7 @@ function loadAgentCommands(d) {
541
560
  result[r.agent_type] = {
542
561
  command: r.command,
543
562
  args: JSON.parse(r.args),
563
+ ...r.headless_args != null && { headlessArgs: JSON.parse(r.headless_args) },
544
564
  ...r.fallback_command != null && { fallbackCommand: r.fallback_command },
545
565
  ...r.fallback_args != null && { fallbackArgs: JSON.parse(r.fallback_args) }
546
566
  };
@@ -617,7 +637,7 @@ function saveConfig(config) {
617
637
  }
618
638
  d.prepare("DELETE FROM agent_commands").run();
619
639
  const insertAgent = d.prepare(
620
- "INSERT INTO agent_commands (agent_type, command, args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?)"
640
+ "INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?, ?)"
621
641
  );
622
642
  if (config.agentCommands) {
623
643
  for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
@@ -626,6 +646,7 @@ function saveConfig(config) {
626
646
  agentType,
627
647
  cmd.command,
628
648
  JSON.stringify(cmd.args),
649
+ cmd.headlessArgs ? JSON.stringify(cmd.headlessArgs) : null,
629
650
  cmd.fallbackCommand ?? null,
630
651
  cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
631
652
  );
@@ -1072,6 +1093,7 @@ var ConfigManager = class {
1072
1093
  changeCallbacks = [];
1073
1094
  dbWatcher = null;
1074
1095
  debounceTimer = null;
1096
+ cachedConfig = null;
1075
1097
  init() {
1076
1098
  initDatabase();
1077
1099
  }
@@ -1080,8 +1102,11 @@ var ConfigManager = class {
1080
1102
  closeDatabase();
1081
1103
  }
1082
1104
  loadConfig() {
1105
+ if (this.cachedConfig) return this.cachedConfig;
1083
1106
  try {
1084
- return loadConfig();
1107
+ const config = loadConfig();
1108
+ this.cachedConfig = config;
1109
+ return config;
1085
1110
  } catch (err) {
1086
1111
  logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
1087
1112
  return {
@@ -1101,6 +1126,7 @@ var ConfigManager = class {
1101
1126
  saveConfig(config) {
1102
1127
  try {
1103
1128
  saveConfig(config);
1129
+ this.cachedConfig = null;
1104
1130
  } catch (err) {
1105
1131
  logger_default.error("[config-manager] saveConfig failed:", err);
1106
1132
  throw err;
@@ -1112,6 +1138,7 @@ var ConfigManager = class {
1112
1138
  }
1113
1139
  /** Notify all registered callbacks (call after main-process config mutations) */
1114
1140
  notifyChanged() {
1141
+ this.cachedConfig = null;
1115
1142
  const config = this.loadConfig();
1116
1143
  for (const cb of this.changeCallbacks) {
1117
1144
  cb(config);
@@ -1548,26 +1575,128 @@ import { z as z4 } from "zod";
1548
1575
  import fs3 from "fs";
1549
1576
  import path4 from "path";
1550
1577
  import os3 from "os";
1578
+ import { execFileSync } from "child_process";
1551
1579
  import { WebSocket } from "ws";
1552
1580
  var PORT_FILE = path4.join(os3.homedir(), ".vibegrid", "ws-port");
1553
1581
  var TIMEOUT_MS = 1e4;
1582
+ var IS_WIN = process.platform === "win32";
1583
+ var PORT_FILE_MISSING_MSG = IS_WIN ? `VibeGrid port file not found (~/.vibegrid/ws-port).
1584
+ The app may be running but the port file was deleted (e.g. by another instance shutting down).
1585
+ To fix, find the VibeGrid process and its listening port:
1586
+ powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process VibeGrid).Id | Select LocalPort"
1587
+ Then write the WS port to the file:
1588
+ echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vibegrid\\ws-port
1589
+ Or restart VibeGrid to regenerate it.` : `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, run: lsof -iTCP -sTCP:LISTEN -P | grep VibeGrid
1592
+ Then write the WS port (the one on *:<port>) to the file:
1593
+ echo '{"port":<PORT>,"pid":<PID>}' > ~/.vibegrid/ws-port
1594
+ Or restart VibeGrid to regenerate it.`;
1595
+ var PORT_FILE_INVALID_MSG = `VibeGrid port file exists but contains invalid data (~/.vibegrid/ws-port).
1596
+ Delete it and restart VibeGrid, or overwrite it with the correct port:
1597
+ ${IS_WIN ? "del %USERPROFILE%\\.vibegrid\\ws-port" : "rm ~/.vibegrid/ws-port"}`;
1554
1598
  var rpcId = 0;
1599
+ var cachedPort = null;
1600
+ var cacheTimestamp = 0;
1601
+ var CACHE_TTL_MS = 5e3;
1602
+ var EXEC_OPTS = {
1603
+ encoding: "utf-8",
1604
+ timeout: 5e3,
1605
+ stdio: ["pipe", "pipe", "pipe"]
1606
+ };
1607
+ function discoverPort() {
1608
+ try {
1609
+ if (IS_WIN) {
1610
+ const taskOut = execFileSync(
1611
+ "tasklist",
1612
+ ["/FI", "IMAGENAME eq VibeGrid.exe", "/FO", "CSV", "/NH"],
1613
+ EXEC_OPTS
1614
+ );
1615
+ const pidMatch = taskOut.match(/"VibeGrid\.exe","(\d+)"/);
1616
+ if (!pidMatch) return null;
1617
+ const pid = pidMatch[1];
1618
+ const lines = execFileSync("netstat", ["-ano"], EXEC_OPTS).split("\n");
1619
+ let fallback = null;
1620
+ for (const line of lines) {
1621
+ if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
1622
+ const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
1623
+ if (!m) continue;
1624
+ if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
1625
+ fallback ??= parseInt(m[1], 10);
1626
+ }
1627
+ return fallback;
1628
+ } else {
1629
+ const lines = execFileSync("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
1630
+ "\n"
1631
+ );
1632
+ let fallback = null;
1633
+ for (const line of lines) {
1634
+ if (!line.includes("VibeGrid")) continue;
1635
+ if (line.includes("*:")) {
1636
+ const m = line.match(/\*:(\d+)/);
1637
+ if (m) return parseInt(m[1], 10);
1638
+ }
1639
+ if (!fallback) {
1640
+ const m = line.match(/:(\d+)\s/);
1641
+ if (m) fallback = parseInt(m[1], 10);
1642
+ }
1643
+ }
1644
+ return fallback;
1645
+ }
1646
+ } catch {
1647
+ }
1648
+ return null;
1649
+ }
1650
+ function discoverAndHeal() {
1651
+ const now = Date.now();
1652
+ if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
1653
+ const discovered = discoverPort();
1654
+ cachedPort = discovered;
1655
+ cacheTimestamp = now;
1656
+ if (discovered) {
1657
+ try {
1658
+ fs3.mkdirSync(path4.dirname(PORT_FILE), { recursive: true });
1659
+ fs3.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
1660
+ } catch {
1661
+ }
1662
+ return { port: discovered };
1663
+ }
1664
+ return { port: null, reason: "missing" };
1665
+ }
1555
1666
  function readPort() {
1556
1667
  try {
1557
1668
  const raw = fs3.readFileSync(PORT_FILE, "utf-8").trim();
1558
- const port = parseInt(raw, 10);
1559
- return Number.isFinite(port) && port > 0 ? port : null;
1669
+ if (!raw) return { port: null, reason: "invalid" };
1670
+ if (raw.startsWith("{")) {
1671
+ const parsed = JSON.parse(raw);
1672
+ const p2 = parsed?.port;
1673
+ const pid = parsed?.pid;
1674
+ if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
1675
+ return { port: null, reason: "invalid" };
1676
+ }
1677
+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
1678
+ try {
1679
+ process.kill(pid, 0);
1680
+ } catch (err) {
1681
+ if (err.code === "EPERM") return { port: p2 };
1682
+ return discoverAndHeal();
1683
+ }
1684
+ }
1685
+ return { port: p2 };
1686
+ }
1687
+ const p = parseInt(raw, 10);
1688
+ return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
1560
1689
  } catch {
1561
- return null;
1690
+ return discoverAndHeal();
1562
1691
  }
1563
1692
  }
1564
1693
  async function rpcCall(method, params) {
1565
- const port = readPort();
1566
- if (!port) {
1567
- throw new Error("VibeGrid app is not running. Start VibeGrid to use session management tools.");
1694
+ const result = readPort();
1695
+ if (!result.port) {
1696
+ throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1568
1697
  }
1569
1698
  return new Promise((resolve, reject) => {
1570
- const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1699
+ const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
1571
1700
  const id = ++rpcId;
1572
1701
  const timer = setTimeout(() => {
1573
1702
  ws.close();
@@ -1597,12 +1726,12 @@ async function rpcCall(method, params) {
1597
1726
  });
1598
1727
  }
1599
1728
  async function rpcNotify(method, params) {
1600
- const port = readPort();
1601
- if (!port) {
1602
- throw new Error("VibeGrid app is not running. Start VibeGrid to use session management tools.");
1729
+ const result = readPort();
1730
+ if (!result.port) {
1731
+ throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
1603
1732
  }
1604
1733
  return new Promise((resolve, reject) => {
1605
- const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1734
+ const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
1606
1735
  ws.on("open", () => {
1607
1736
  ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
1608
1737
  ws.close();
@@ -1838,12 +1967,12 @@ function registerSessionTools(server) {
1838
1967
  "Send input to a running terminal session. Requires the VibeGrid app to be running.",
1839
1968
  {
1840
1969
  id: V.id.describe("Session ID"),
1841
- data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)")
1970
+ data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)"),
1971
+ raw: z4.boolean().optional().describe("Send data as-is without appending carriage return (for raw terminal control)")
1842
1972
  },
1843
1973
  async (args) => {
1844
1974
  try {
1845
- const trimmed = args.data.replace(/[\r\n]+$/, "");
1846
- const data = trimmed + "\r";
1975
+ const data = args.raw ? args.data : args.data.replace(/[\r\n]+$/, "") + "\r";
1847
1976
  await rpcNotify("terminal:write", { id: args.id, data });
1848
1977
  return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
1849
1978
  } catch (err) {
@@ -1859,6 +1988,73 @@ function registerSessionTools(server) {
1859
1988
  }
1860
1989
  }
1861
1990
  );
1991
+ const KEY_MAP = {
1992
+ enter: "\r",
1993
+ escape: "\x1B",
1994
+ esc: "\x1B",
1995
+ tab: " ",
1996
+ "shift+tab": "\x1B[Z",
1997
+ up: "\x1B[A",
1998
+ down: "\x1B[B",
1999
+ left: "\x1B[D",
2000
+ right: "\x1B[C",
2001
+ backspace: "\x7F",
2002
+ delete: "\x1B[3~",
2003
+ home: "\x1B[H",
2004
+ end: "\x1B[F",
2005
+ "ctrl+c": "",
2006
+ "ctrl+d": "",
2007
+ "ctrl+x": "",
2008
+ "ctrl+z": ""
2009
+ };
2010
+ server.tool(
2011
+ "send_key",
2012
+ "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.",
2013
+ {
2014
+ id: V.id.describe("Session ID"),
2015
+ key: z4.string().min(1).max(20).describe(
2016
+ "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)"
2017
+ )
2018
+ },
2019
+ async (args) => {
2020
+ const key = args.key.toLowerCase().trim();
2021
+ let data = KEY_MAP[key];
2022
+ if (!data) {
2023
+ const ctrlMatch = key.match(/^ctrl\+([a-z])$/);
2024
+ if (ctrlMatch) {
2025
+ data = String.fromCharCode(ctrlMatch[1].toUpperCase().charCodeAt(0) - 64);
2026
+ } else if (args.key.length === 1) {
2027
+ data = args.key;
2028
+ } else {
2029
+ return {
2030
+ content: [
2031
+ {
2032
+ type: "text",
2033
+ text: `Unknown key: "${args.key}". Supported: single chars (1, y, n), named keys (${Object.keys(KEY_MAP).join(", ")}), or ctrl+<letter>.`
2034
+ }
2035
+ ],
2036
+ isError: true
2037
+ };
2038
+ }
2039
+ }
2040
+ try {
2041
+ await rpcNotify("terminal:write", { id: args.id, data });
2042
+ return {
2043
+ content: [{ type: "text", text: `Sent key "${args.key}" to session: ${args.id}` }]
2044
+ };
2045
+ } catch (err) {
2046
+ return {
2047
+ content: [
2048
+ {
2049
+ type: "text",
2050
+ text: `Error sending key to terminal: ${err instanceof Error ? err.message : err}`
2051
+ }
2052
+ ],
2053
+ isError: true
2054
+ };
2055
+ }
2056
+ }
2057
+ );
1862
2058
  server.tool(
1863
2059
  "list_session_events",
1864
2060
  "List session lifecycle events (created, exited, task_linked, renamed, archived, unarchived). Use for post-mortem analysis and multi-agent coordination.",
@@ -2019,7 +2215,7 @@ function registerWorkflowTools(server) {
2019
2215
  const workflow = {
2020
2216
  id: crypto2.randomUUID(),
2021
2217
  name: args.name,
2022
- icon: args.icon ?? "zap",
2218
+ icon: args.icon ?? "Zap",
2023
2219
  iconColor: args.icon_color ?? "#6366f1",
2024
2220
  nodes,
2025
2221
  edges,
@@ -2274,7 +2470,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
2274
2470
  console.error = (...args) => _origError("[mcp:error]", ...args);
2275
2471
  async function main() {
2276
2472
  configManager.init();
2277
- const version = true ? "0.4.0-beta.4" : createRequire(import.meta.url)("../package.json").version;
2473
+ const version = true ? "0.4.0-beta.6" : createRequire(import.meta.url)("../package.json").version;
2278
2474
  const server = createMcpServer(version);
2279
2475
  const transport = new StdioServerTransport();
2280
2476
  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.4",
3
+ "version": "0.4.0-beta.6",
4
4
  "description": "VibeGrid MCP server — task management, git, and workflow tools for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",