@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.
- package/dist/index.js +217 -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)) {
|
|
@@ -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
|
-
|
|
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
|
|
1562
|
-
|
|
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
|
|
1565
|
-
return Number.isFinite(
|
|
1693
|
+
const p = parseInt(raw, 10);
|
|
1694
|
+
return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
|
|
1566
1695
|
} catch {
|
|
1567
|
-
return
|
|
1696
|
+
return discoverAndHeal();
|
|
1568
1697
|
}
|
|
1569
1698
|
}
|
|
1570
1699
|
async function rpcCall(method, params) {
|
|
1571
|
-
const
|
|
1572
|
-
if (!port) {
|
|
1573
|
-
throw new Error(
|
|
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
|
|
1607
|
-
if (!port) {
|
|
1608
|
-
throw new Error(
|
|
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
|
|
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 ?? "
|
|
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.
|
|
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);
|