@vibegrid/mcp 0.4.0-beta.5 → 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.
- package/dist/index.js +211 -21
- 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
|
-
|
|
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,32 +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
|
-
if (!raw) return null;
|
|
1669
|
+
if (!raw) return { port: null, reason: "invalid" };
|
|
1559
1670
|
if (raw.startsWith("{")) {
|
|
1560
1671
|
const parsed = JSON.parse(raw);
|
|
1561
|
-
const
|
|
1562
|
-
|
|
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 };
|
|
1563
1686
|
}
|
|
1564
|
-
const
|
|
1565
|
-
return Number.isFinite(
|
|
1687
|
+
const p = parseInt(raw, 10);
|
|
1688
|
+
return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
|
|
1566
1689
|
} catch {
|
|
1567
|
-
return
|
|
1690
|
+
return discoverAndHeal();
|
|
1568
1691
|
}
|
|
1569
1692
|
}
|
|
1570
1693
|
async function rpcCall(method, params) {
|
|
1571
|
-
const
|
|
1572
|
-
if (!port) {
|
|
1573
|
-
throw new Error(
|
|
1694
|
+
const result = readPort();
|
|
1695
|
+
if (!result.port) {
|
|
1696
|
+
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
1574
1697
|
}
|
|
1575
1698
|
return new Promise((resolve, reject) => {
|
|
1576
|
-
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1699
|
+
const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
|
|
1577
1700
|
const id = ++rpcId;
|
|
1578
1701
|
const timer = setTimeout(() => {
|
|
1579
1702
|
ws.close();
|
|
@@ -1603,12 +1726,12 @@ async function rpcCall(method, params) {
|
|
|
1603
1726
|
});
|
|
1604
1727
|
}
|
|
1605
1728
|
async function rpcNotify(method, params) {
|
|
1606
|
-
const
|
|
1607
|
-
if (!port) {
|
|
1608
|
-
throw new Error(
|
|
1729
|
+
const result = readPort();
|
|
1730
|
+
if (!result.port) {
|
|
1731
|
+
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
1609
1732
|
}
|
|
1610
1733
|
return new Promise((resolve, reject) => {
|
|
1611
|
-
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1734
|
+
const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
|
|
1612
1735
|
ws.on("open", () => {
|
|
1613
1736
|
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
|
|
1614
1737
|
ws.close();
|
|
@@ -1844,12 +1967,12 @@ function registerSessionTools(server) {
|
|
|
1844
1967
|
"Send input to a running terminal session. Requires the VibeGrid app to be running.",
|
|
1845
1968
|
{
|
|
1846
1969
|
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)")
|
|
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)")
|
|
1848
1972
|
},
|
|
1849
1973
|
async (args) => {
|
|
1850
1974
|
try {
|
|
1851
|
-
const
|
|
1852
|
-
const data = trimmed + "\r";
|
|
1975
|
+
const data = args.raw ? args.data : args.data.replace(/[\r\n]+$/, "") + "\r";
|
|
1853
1976
|
await rpcNotify("terminal:write", { id: args.id, data });
|
|
1854
1977
|
return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
|
|
1855
1978
|
} catch (err) {
|
|
@@ -1865,6 +1988,73 @@ function registerSessionTools(server) {
|
|
|
1865
1988
|
}
|
|
1866
1989
|
}
|
|
1867
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
|
+
);
|
|
1868
2058
|
server.tool(
|
|
1869
2059
|
"list_session_events",
|
|
1870
2060
|
"List session lifecycle events (created, exited, task_linked, renamed, archived, unarchived). Use for post-mortem analysis and multi-agent coordination.",
|
|
@@ -2025,7 +2215,7 @@ function registerWorkflowTools(server) {
|
|
|
2025
2215
|
const workflow = {
|
|
2026
2216
|
id: crypto2.randomUUID(),
|
|
2027
2217
|
name: args.name,
|
|
2028
|
-
icon: args.icon ?? "
|
|
2218
|
+
icon: args.icon ?? "Zap",
|
|
2029
2219
|
iconColor: args.icon_color ?? "#6366f1",
|
|
2030
2220
|
nodes,
|
|
2031
2221
|
edges,
|
|
@@ -2280,7 +2470,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
|
|
|
2280
2470
|
console.error = (...args) => _origError("[mcp:error]", ...args);
|
|
2281
2471
|
async function main() {
|
|
2282
2472
|
configManager.init();
|
|
2283
|
-
const version = true ? "0.4.0-beta.
|
|
2473
|
+
const version = true ? "0.4.0-beta.6" : createRequire(import.meta.url)("../package.json").version;
|
|
2284
2474
|
const server = createMcpServer(version);
|
|
2285
2475
|
const transport = new StdioServerTransport();
|
|
2286
2476
|
await server.connect(transport);
|