@vibegrid/mcp 0.4.0-beta.0 → 0.4.0-beta.10
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 +455 -19
- package/package.json +2 -2
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
|
);
|
|
@@ -237,7 +238,9 @@ function createSchema() {
|
|
|
237
238
|
remote_host_label TEXT,
|
|
238
239
|
hook_session_id TEXT,
|
|
239
240
|
status_source TEXT,
|
|
240
|
-
saved_at INTEGER
|
|
241
|
+
saved_at INTEGER,
|
|
242
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
243
|
+
worktree_name TEXT
|
|
241
244
|
);
|
|
242
245
|
|
|
243
246
|
CREATE TABLE IF NOT EXISTS schedule_log (
|
|
@@ -300,8 +303,37 @@ function createSchema() {
|
|
|
300
303
|
CREATE INDEX IF NOT EXISTS idx_workflow_runs_task ON workflow_runs(trigger_task_id);
|
|
301
304
|
CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_run ON workflow_run_nodes(run_id);
|
|
302
305
|
CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_task ON workflow_run_nodes(task_id);
|
|
306
|
+
|
|
307
|
+
CREATE TABLE IF NOT EXISTS session_logs (
|
|
308
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
309
|
+
task_id TEXT NOT NULL,
|
|
310
|
+
session_id TEXT NOT NULL,
|
|
311
|
+
agent_type TEXT,
|
|
312
|
+
branch TEXT,
|
|
313
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
314
|
+
started_at TEXT NOT NULL,
|
|
315
|
+
completed_at TEXT,
|
|
316
|
+
exit_code INTEGER,
|
|
317
|
+
logs TEXT,
|
|
318
|
+
project_name TEXT
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
CREATE INDEX IF NOT EXISTS idx_session_logs_task ON session_logs(task_id);
|
|
322
|
+
CREATE INDEX IF NOT EXISTS idx_session_logs_session ON session_logs(session_id);
|
|
323
|
+
|
|
324
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
325
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
326
|
+
session_id TEXT NOT NULL,
|
|
327
|
+
event_type TEXT NOT NULL,
|
|
328
|
+
timestamp TEXT NOT NULL,
|
|
329
|
+
metadata TEXT
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp DESC);
|
|
333
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_type ON session_events(event_type, timestamp DESC);
|
|
303
334
|
`);
|
|
304
335
|
migrateSchema(d);
|
|
336
|
+
verifySchema(d);
|
|
305
337
|
}
|
|
306
338
|
function migrateSchema(d) {
|
|
307
339
|
const row = d.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
|
|
@@ -359,6 +391,106 @@ function migrateSchema(d) {
|
|
|
359
391
|
})();
|
|
360
392
|
logger_default.info("[database] migrated schema to version 2 (ssh credential vault)");
|
|
361
393
|
}
|
|
394
|
+
if (version < 3) {
|
|
395
|
+
d.transaction(() => {
|
|
396
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
397
|
+
if (!sessionCols.some((c) => c.name === "sort_order")) {
|
|
398
|
+
d.exec("ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0");
|
|
399
|
+
}
|
|
400
|
+
d.prepare(
|
|
401
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '3')"
|
|
402
|
+
).run();
|
|
403
|
+
})();
|
|
404
|
+
logger_default.info("[database] migrated schema to version 3 (session sort order)");
|
|
405
|
+
}
|
|
406
|
+
if (version < 4) {
|
|
407
|
+
d.transaction(() => {
|
|
408
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
409
|
+
if (!sessionCols.some((c) => c.name === "worktree_name")) {
|
|
410
|
+
d.exec("ALTER TABLE sessions ADD COLUMN worktree_name TEXT");
|
|
411
|
+
}
|
|
412
|
+
d.prepare(
|
|
413
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '4')"
|
|
414
|
+
).run();
|
|
415
|
+
})();
|
|
416
|
+
logger_default.info("[database] migrated schema to version 4 (worktree name)");
|
|
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
|
+
}
|
|
430
|
+
if (version < 6) {
|
|
431
|
+
d.transaction(() => {
|
|
432
|
+
const sessionCols = d.prepare("PRAGMA table_info(sessions)").all();
|
|
433
|
+
if (!sessionCols.some((c) => c.name === "claude_session_id")) {
|
|
434
|
+
d.exec("ALTER TABLE sessions ADD COLUMN claude_session_id TEXT");
|
|
435
|
+
}
|
|
436
|
+
d.prepare(
|
|
437
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '6')"
|
|
438
|
+
).run();
|
|
439
|
+
})();
|
|
440
|
+
logger_default.info("[database] migrated schema to version 6 (claude session id)");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function verifySchema(d) {
|
|
444
|
+
const expectedByTable = {
|
|
445
|
+
projects: [
|
|
446
|
+
{
|
|
447
|
+
column: "workspace_id",
|
|
448
|
+
ddl: "ALTER TABLE projects ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'"
|
|
449
|
+
}
|
|
450
|
+
],
|
|
451
|
+
workflows: [
|
|
452
|
+
{
|
|
453
|
+
column: "workspace_id",
|
|
454
|
+
ddl: "ALTER TABLE workflows ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'"
|
|
455
|
+
}
|
|
456
|
+
],
|
|
457
|
+
remote_hosts: [
|
|
458
|
+
{ column: "auth_method", ddl: "ALTER TABLE remote_hosts ADD COLUMN auth_method TEXT" },
|
|
459
|
+
{ column: "credential_id", ddl: "ALTER TABLE remote_hosts ADD COLUMN credential_id TEXT" },
|
|
460
|
+
{
|
|
461
|
+
column: "encrypted_password",
|
|
462
|
+
ddl: "ALTER TABLE remote_hosts ADD COLUMN encrypted_password TEXT"
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
sessions: [
|
|
466
|
+
{
|
|
467
|
+
column: "sort_order",
|
|
468
|
+
ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
|
|
469
|
+
},
|
|
470
|
+
{ column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
|
|
471
|
+
{ column: "claude_session_id", ddl: "ALTER TABLE sessions ADD COLUMN claude_session_id TEXT" }
|
|
472
|
+
],
|
|
473
|
+
agent_commands: [
|
|
474
|
+
{
|
|
475
|
+
column: "headless_args",
|
|
476
|
+
ddl: "ALTER TABLE agent_commands ADD COLUMN headless_args TEXT"
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
};
|
|
480
|
+
for (const [table, columns] of Object.entries(expectedByTable)) {
|
|
481
|
+
const existing = new Set(
|
|
482
|
+
d.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name)
|
|
483
|
+
);
|
|
484
|
+
for (const { column, ddl } of columns) {
|
|
485
|
+
if (existing.has(column)) continue;
|
|
486
|
+
try {
|
|
487
|
+
d.exec(ddl);
|
|
488
|
+
logger_default.warn(`[database] self-heal: added missing column ${table}.${column}`);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
logger_default.error(`[database] self-heal: failed to add ${table}.${column}:`, err);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
362
494
|
}
|
|
363
495
|
function loadConfig() {
|
|
364
496
|
const d = getDb();
|
|
@@ -423,6 +555,12 @@ function loadDefaults(d) {
|
|
|
423
555
|
},
|
|
424
556
|
...map.networkAccessEnabled !== void 0 && {
|
|
425
557
|
networkAccessEnabled: map.networkAccessEnabled
|
|
558
|
+
},
|
|
559
|
+
...map.showHeadlessAgents !== void 0 && {
|
|
560
|
+
showHeadlessAgents: map.showHeadlessAgents
|
|
561
|
+
},
|
|
562
|
+
...map.headlessRetentionMinutes !== void 0 && {
|
|
563
|
+
headlessRetentionMinutes: map.headlessRetentionMinutes
|
|
426
564
|
}
|
|
427
565
|
};
|
|
428
566
|
}
|
|
@@ -441,6 +579,7 @@ function loadAgentCommands(d) {
|
|
|
441
579
|
result[r.agent_type] = {
|
|
442
580
|
command: r.command,
|
|
443
581
|
args: JSON.parse(r.args),
|
|
582
|
+
...r.headless_args != null && { headlessArgs: JSON.parse(r.headless_args) },
|
|
444
583
|
...r.fallback_command != null && { fallbackCommand: r.fallback_command },
|
|
445
584
|
...r.fallback_args != null && { fallbackArgs: JSON.parse(r.fallback_args) }
|
|
446
585
|
};
|
|
@@ -517,7 +656,7 @@ function saveConfig(config) {
|
|
|
517
656
|
}
|
|
518
657
|
d.prepare("DELETE FROM agent_commands").run();
|
|
519
658
|
const insertAgent = d.prepare(
|
|
520
|
-
"INSERT INTO agent_commands (agent_type, command, args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?)"
|
|
659
|
+
"INSERT INTO agent_commands (agent_type, command, args, headless_args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?, ?)"
|
|
521
660
|
);
|
|
522
661
|
if (config.agentCommands) {
|
|
523
662
|
for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
|
|
@@ -526,6 +665,7 @@ function saveConfig(config) {
|
|
|
526
665
|
agentType,
|
|
527
666
|
cmd.command,
|
|
528
667
|
JSON.stringify(cmd.args),
|
|
668
|
+
cmd.headlessArgs ? JSON.stringify(cmd.headlessArgs) : null,
|
|
529
669
|
cmd.fallbackCommand ?? null,
|
|
530
670
|
cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
|
|
531
671
|
);
|
|
@@ -972,6 +1112,7 @@ var ConfigManager = class {
|
|
|
972
1112
|
changeCallbacks = [];
|
|
973
1113
|
dbWatcher = null;
|
|
974
1114
|
debounceTimer = null;
|
|
1115
|
+
cachedConfig = null;
|
|
975
1116
|
init() {
|
|
976
1117
|
initDatabase();
|
|
977
1118
|
}
|
|
@@ -980,8 +1121,11 @@ var ConfigManager = class {
|
|
|
980
1121
|
closeDatabase();
|
|
981
1122
|
}
|
|
982
1123
|
loadConfig() {
|
|
1124
|
+
if (this.cachedConfig) return this.cachedConfig;
|
|
983
1125
|
try {
|
|
984
|
-
|
|
1126
|
+
const config = loadConfig();
|
|
1127
|
+
this.cachedConfig = config;
|
|
1128
|
+
return config;
|
|
985
1129
|
} catch (err) {
|
|
986
1130
|
logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
|
|
987
1131
|
return {
|
|
@@ -1001,6 +1145,7 @@ var ConfigManager = class {
|
|
|
1001
1145
|
saveConfig(config) {
|
|
1002
1146
|
try {
|
|
1003
1147
|
saveConfig(config);
|
|
1148
|
+
this.cachedConfig = null;
|
|
1004
1149
|
} catch (err) {
|
|
1005
1150
|
logger_default.error("[config-manager] saveConfig failed:", err);
|
|
1006
1151
|
throw err;
|
|
@@ -1012,6 +1157,7 @@ var ConfigManager = class {
|
|
|
1012
1157
|
}
|
|
1013
1158
|
/** Notify all registered callbacks (call after main-process config mutations) */
|
|
1014
1159
|
notifyChanged() {
|
|
1160
|
+
this.cachedConfig = null;
|
|
1015
1161
|
const config = this.loadConfig();
|
|
1016
1162
|
for (const cb of this.changeCallbacks) {
|
|
1017
1163
|
cb(config);
|
|
@@ -1448,26 +1594,128 @@ import { z as z4 } from "zod";
|
|
|
1448
1594
|
import fs3 from "fs";
|
|
1449
1595
|
import path4 from "path";
|
|
1450
1596
|
import os3 from "os";
|
|
1597
|
+
import { execFileSync } from "child_process";
|
|
1451
1598
|
import { WebSocket } from "ws";
|
|
1452
1599
|
var PORT_FILE = path4.join(os3.homedir(), ".vibegrid", "ws-port");
|
|
1453
1600
|
var TIMEOUT_MS = 1e4;
|
|
1601
|
+
var IS_WIN = process.platform === "win32";
|
|
1602
|
+
var PORT_FILE_MISSING_MSG = IS_WIN ? `VibeGrid port file not found (~/.vibegrid/ws-port).
|
|
1603
|
+
The app may be running but the port file was deleted (e.g. by another instance shutting down).
|
|
1604
|
+
To fix, find the VibeGrid process and its listening port:
|
|
1605
|
+
powershell -c "Get-NetTCPConnection -State Listen -OwningProcess (Get-Process VibeGrid).Id | Select LocalPort"
|
|
1606
|
+
Then write the WS port to the file:
|
|
1607
|
+
echo {"port":<PORT>,"pid":<PID>} > %USERPROFILE%\\.vibegrid\\ws-port
|
|
1608
|
+
Or restart VibeGrid to regenerate it.` : `VibeGrid port file not found (~/.vibegrid/ws-port).
|
|
1609
|
+
The app may be running but the port file was deleted (e.g. by another instance shutting down).
|
|
1610
|
+
To fix, run: lsof -iTCP -sTCP:LISTEN -P | grep VibeGrid
|
|
1611
|
+
Then write the WS port (the one on *:<port>) to the file:
|
|
1612
|
+
echo '{"port":<PORT>,"pid":<PID>}' > ~/.vibegrid/ws-port
|
|
1613
|
+
Or restart VibeGrid to regenerate it.`;
|
|
1614
|
+
var PORT_FILE_INVALID_MSG = `VibeGrid port file exists but contains invalid data (~/.vibegrid/ws-port).
|
|
1615
|
+
Delete it and restart VibeGrid, or overwrite it with the correct port:
|
|
1616
|
+
${IS_WIN ? "del %USERPROFILE%\\.vibegrid\\ws-port" : "rm ~/.vibegrid/ws-port"}`;
|
|
1454
1617
|
var rpcId = 0;
|
|
1618
|
+
var cachedPort = null;
|
|
1619
|
+
var cacheTimestamp = 0;
|
|
1620
|
+
var CACHE_TTL_MS = 5e3;
|
|
1621
|
+
var EXEC_OPTS = {
|
|
1622
|
+
encoding: "utf-8",
|
|
1623
|
+
timeout: 5e3,
|
|
1624
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1625
|
+
};
|
|
1626
|
+
function discoverPort() {
|
|
1627
|
+
try {
|
|
1628
|
+
if (IS_WIN) {
|
|
1629
|
+
const taskOut = execFileSync(
|
|
1630
|
+
"tasklist",
|
|
1631
|
+
["/FI", "IMAGENAME eq VibeGrid.exe", "/FO", "CSV", "/NH"],
|
|
1632
|
+
EXEC_OPTS
|
|
1633
|
+
);
|
|
1634
|
+
const pidMatch = taskOut.match(/"VibeGrid\.exe","(\d+)"/);
|
|
1635
|
+
if (!pidMatch) return null;
|
|
1636
|
+
const pid = pidMatch[1];
|
|
1637
|
+
const lines = execFileSync("netstat", ["-ano"], EXEC_OPTS).split("\n");
|
|
1638
|
+
let fallback = null;
|
|
1639
|
+
for (const line of lines) {
|
|
1640
|
+
if (!line.includes("LISTENING") || !line.trim().endsWith(pid)) continue;
|
|
1641
|
+
const m = line.match(/(?:0\.0\.0\.0|127\.0\.0\.1):(\d+)/);
|
|
1642
|
+
if (!m) continue;
|
|
1643
|
+
if (line.includes("0.0.0.0")) return parseInt(m[1], 10);
|
|
1644
|
+
fallback ??= parseInt(m[1], 10);
|
|
1645
|
+
}
|
|
1646
|
+
return fallback;
|
|
1647
|
+
} else {
|
|
1648
|
+
const lines = execFileSync("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], EXEC_OPTS).split(
|
|
1649
|
+
"\n"
|
|
1650
|
+
);
|
|
1651
|
+
let fallback = null;
|
|
1652
|
+
for (const line of lines) {
|
|
1653
|
+
if (!line.includes("VibeGrid")) continue;
|
|
1654
|
+
if (line.includes("*:")) {
|
|
1655
|
+
const m = line.match(/\*:(\d+)/);
|
|
1656
|
+
if (m) return parseInt(m[1], 10);
|
|
1657
|
+
}
|
|
1658
|
+
if (!fallback) {
|
|
1659
|
+
const m = line.match(/:(\d+)\s/);
|
|
1660
|
+
if (m) fallback = parseInt(m[1], 10);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return fallback;
|
|
1664
|
+
}
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
function discoverAndHeal() {
|
|
1670
|
+
const now = Date.now();
|
|
1671
|
+
if (cachedPort && now - cacheTimestamp < CACHE_TTL_MS) return { port: cachedPort };
|
|
1672
|
+
const discovered = discoverPort();
|
|
1673
|
+
cachedPort = discovered;
|
|
1674
|
+
cacheTimestamp = now;
|
|
1675
|
+
if (discovered) {
|
|
1676
|
+
try {
|
|
1677
|
+
fs3.mkdirSync(path4.dirname(PORT_FILE), { recursive: true });
|
|
1678
|
+
fs3.writeFileSync(PORT_FILE, JSON.stringify({ port: discovered }), "utf-8");
|
|
1679
|
+
} catch {
|
|
1680
|
+
}
|
|
1681
|
+
return { port: discovered };
|
|
1682
|
+
}
|
|
1683
|
+
return { port: null, reason: "missing" };
|
|
1684
|
+
}
|
|
1455
1685
|
function readPort() {
|
|
1456
1686
|
try {
|
|
1457
1687
|
const raw = fs3.readFileSync(PORT_FILE, "utf-8").trim();
|
|
1458
|
-
|
|
1459
|
-
|
|
1688
|
+
if (!raw) return { port: null, reason: "invalid" };
|
|
1689
|
+
if (raw.startsWith("{")) {
|
|
1690
|
+
const parsed = JSON.parse(raw);
|
|
1691
|
+
const p2 = parsed?.port;
|
|
1692
|
+
const pid = parsed?.pid;
|
|
1693
|
+
if (typeof p2 !== "number" || !Number.isFinite(p2) || p2 <= 0) {
|
|
1694
|
+
return { port: null, reason: "invalid" };
|
|
1695
|
+
}
|
|
1696
|
+
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
|
|
1697
|
+
try {
|
|
1698
|
+
process.kill(pid, 0);
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
if (err.code === "EPERM") return { port: p2 };
|
|
1701
|
+
return discoverAndHeal();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return { port: p2 };
|
|
1705
|
+
}
|
|
1706
|
+
const p = parseInt(raw, 10);
|
|
1707
|
+
return Number.isFinite(p) && p > 0 ? { port: p } : { port: null, reason: "invalid" };
|
|
1460
1708
|
} catch {
|
|
1461
|
-
return
|
|
1709
|
+
return discoverAndHeal();
|
|
1462
1710
|
}
|
|
1463
1711
|
}
|
|
1464
1712
|
async function rpcCall(method, params) {
|
|
1465
|
-
const
|
|
1466
|
-
if (!port) {
|
|
1467
|
-
throw new Error(
|
|
1713
|
+
const result = readPort();
|
|
1714
|
+
if (!result.port) {
|
|
1715
|
+
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
1468
1716
|
}
|
|
1469
1717
|
return new Promise((resolve, reject) => {
|
|
1470
|
-
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1718
|
+
const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
|
|
1471
1719
|
const id = ++rpcId;
|
|
1472
1720
|
const timer = setTimeout(() => {
|
|
1473
1721
|
ws.close();
|
|
@@ -1497,12 +1745,12 @@ async function rpcCall(method, params) {
|
|
|
1497
1745
|
});
|
|
1498
1746
|
}
|
|
1499
1747
|
async function rpcNotify(method, params) {
|
|
1500
|
-
const
|
|
1501
|
-
if (!port) {
|
|
1502
|
-
throw new Error(
|
|
1748
|
+
const result = readPort();
|
|
1749
|
+
if (!result.port) {
|
|
1750
|
+
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
1503
1751
|
}
|
|
1504
1752
|
return new Promise((resolve, reject) => {
|
|
1505
|
-
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1753
|
+
const ws = new WebSocket(`ws://127.0.0.1:${result.port}/ws`);
|
|
1506
1754
|
ws.on("open", () => {
|
|
1507
1755
|
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
|
|
1508
1756
|
ws.close();
|
|
@@ -1648,17 +1896,102 @@ function registerSessionTools(server) {
|
|
|
1648
1896
|
}
|
|
1649
1897
|
}
|
|
1650
1898
|
);
|
|
1899
|
+
server.tool(
|
|
1900
|
+
"rename_session",
|
|
1901
|
+
"Rename a terminal session. Changes the display name shown in the UI.",
|
|
1902
|
+
{
|
|
1903
|
+
id: V.id.describe("Session ID"),
|
|
1904
|
+
display_name: V.shortText.describe("New display name")
|
|
1905
|
+
},
|
|
1906
|
+
async (args) => {
|
|
1907
|
+
try {
|
|
1908
|
+
await rpcCall("terminal:rename", { id: args.id, displayName: args.display_name });
|
|
1909
|
+
return {
|
|
1910
|
+
content: [{ type: "text", text: `Renamed session ${args.id} to "${args.display_name}"` }]
|
|
1911
|
+
};
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
return {
|
|
1914
|
+
content: [
|
|
1915
|
+
{
|
|
1916
|
+
type: "text",
|
|
1917
|
+
text: `Error renaming session: ${err instanceof Error ? err.message : err}`
|
|
1918
|
+
}
|
|
1919
|
+
],
|
|
1920
|
+
isError: true
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
);
|
|
1925
|
+
server.tool(
|
|
1926
|
+
"reorder_sessions",
|
|
1927
|
+
"Reorder terminal sessions in the grid. Provide session IDs in the desired display order.",
|
|
1928
|
+
{
|
|
1929
|
+
session_ids: z4.array(V.id).min(1, "At least one session ID is required").describe("Session IDs in desired order")
|
|
1930
|
+
},
|
|
1931
|
+
async (args) => {
|
|
1932
|
+
try {
|
|
1933
|
+
await rpcCall("terminal:reorder", args.session_ids);
|
|
1934
|
+
return {
|
|
1935
|
+
content: [
|
|
1936
|
+
{
|
|
1937
|
+
type: "text",
|
|
1938
|
+
text: `Reordered ${args.session_ids.length} sessions`
|
|
1939
|
+
}
|
|
1940
|
+
]
|
|
1941
|
+
};
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
return {
|
|
1944
|
+
content: [
|
|
1945
|
+
{
|
|
1946
|
+
type: "text",
|
|
1947
|
+
text: `Error reordering sessions: ${err instanceof Error ? err.message : err}`
|
|
1948
|
+
}
|
|
1949
|
+
],
|
|
1950
|
+
isError: true
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
);
|
|
1955
|
+
server.tool(
|
|
1956
|
+
"read_session_output",
|
|
1957
|
+
"Read terminal output from a running session. Output is stored in a rolling 1000-line buffer with ANSI codes stripped.",
|
|
1958
|
+
{
|
|
1959
|
+
id: V.id.describe("Session ID"),
|
|
1960
|
+
lines: z4.number().int().min(1).max(1e3).optional().describe("Number of lines to read from the end (default: all)")
|
|
1961
|
+
},
|
|
1962
|
+
async (args) => {
|
|
1963
|
+
try {
|
|
1964
|
+
const output = await rpcCall("terminal:readOutput", {
|
|
1965
|
+
id: args.id,
|
|
1966
|
+
lines: args.lines
|
|
1967
|
+
});
|
|
1968
|
+
return {
|
|
1969
|
+
content: [{ type: "text", text: output.join("\n") }]
|
|
1970
|
+
};
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
return {
|
|
1973
|
+
content: [
|
|
1974
|
+
{
|
|
1975
|
+
type: "text",
|
|
1976
|
+
text: `Error reading session output: ${err instanceof Error ? err.message : err}`
|
|
1977
|
+
}
|
|
1978
|
+
],
|
|
1979
|
+
isError: true
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
);
|
|
1651
1984
|
server.tool(
|
|
1652
1985
|
"write_to_terminal",
|
|
1653
1986
|
"Send input to a running terminal session. Requires the VibeGrid app to be running.",
|
|
1654
1987
|
{
|
|
1655
1988
|
id: V.id.describe("Session ID"),
|
|
1656
|
-
data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)")
|
|
1989
|
+
data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)"),
|
|
1990
|
+
raw: z4.boolean().optional().describe("Send data as-is without appending carriage return (for raw terminal control)")
|
|
1657
1991
|
},
|
|
1658
1992
|
async (args) => {
|
|
1659
1993
|
try {
|
|
1660
|
-
const
|
|
1661
|
-
const data = trimmed + "\r";
|
|
1994
|
+
const data = args.raw ? args.data : args.data.replace(/[\r\n]+$/, "") + "\r";
|
|
1662
1995
|
await rpcNotify("terminal:write", { id: args.id, data });
|
|
1663
1996
|
return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
|
|
1664
1997
|
} catch (err) {
|
|
@@ -1674,6 +2007,109 @@ function registerSessionTools(server) {
|
|
|
1674
2007
|
}
|
|
1675
2008
|
}
|
|
1676
2009
|
);
|
|
2010
|
+
const KEY_MAP = {
|
|
2011
|
+
enter: "\r",
|
|
2012
|
+
escape: "\x1B",
|
|
2013
|
+
esc: "\x1B",
|
|
2014
|
+
tab: " ",
|
|
2015
|
+
"shift+tab": "\x1B[Z",
|
|
2016
|
+
up: "\x1B[A",
|
|
2017
|
+
down: "\x1B[B",
|
|
2018
|
+
left: "\x1B[D",
|
|
2019
|
+
right: "\x1B[C",
|
|
2020
|
+
backspace: "\x7F",
|
|
2021
|
+
delete: "\x1B[3~",
|
|
2022
|
+
home: "\x1B[H",
|
|
2023
|
+
end: "\x1B[F",
|
|
2024
|
+
"ctrl+c": "",
|
|
2025
|
+
"ctrl+d": "",
|
|
2026
|
+
"ctrl+x": "",
|
|
2027
|
+
"ctrl+z": ""
|
|
2028
|
+
};
|
|
2029
|
+
server.tool(
|
|
2030
|
+
"send_key",
|
|
2031
|
+
"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.",
|
|
2032
|
+
{
|
|
2033
|
+
id: V.id.describe("Session ID"),
|
|
2034
|
+
key: z4.string().min(1).max(20).describe(
|
|
2035
|
+
"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)"
|
|
2036
|
+
)
|
|
2037
|
+
},
|
|
2038
|
+
async (args) => {
|
|
2039
|
+
const key = args.key.toLowerCase().trim();
|
|
2040
|
+
let data = KEY_MAP[key];
|
|
2041
|
+
if (!data) {
|
|
2042
|
+
const ctrlMatch = key.match(/^ctrl\+([a-z])$/);
|
|
2043
|
+
if (ctrlMatch) {
|
|
2044
|
+
data = String.fromCharCode(ctrlMatch[1].toUpperCase().charCodeAt(0) - 64);
|
|
2045
|
+
} else if (args.key.length === 1) {
|
|
2046
|
+
data = args.key;
|
|
2047
|
+
} else {
|
|
2048
|
+
return {
|
|
2049
|
+
content: [
|
|
2050
|
+
{
|
|
2051
|
+
type: "text",
|
|
2052
|
+
text: `Unknown key: "${args.key}". Supported: single chars (1, y, n), named keys (${Object.keys(KEY_MAP).join(", ")}), or ctrl+<letter>.`
|
|
2053
|
+
}
|
|
2054
|
+
],
|
|
2055
|
+
isError: true
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
await rpcNotify("terminal:write", { id: args.id, data });
|
|
2061
|
+
return {
|
|
2062
|
+
content: [{ type: "text", text: `Sent key "${args.key}" to session: ${args.id}` }]
|
|
2063
|
+
};
|
|
2064
|
+
} catch (err) {
|
|
2065
|
+
return {
|
|
2066
|
+
content: [
|
|
2067
|
+
{
|
|
2068
|
+
type: "text",
|
|
2069
|
+
text: `Error sending key to terminal: ${err instanceof Error ? err.message : err}`
|
|
2070
|
+
}
|
|
2071
|
+
],
|
|
2072
|
+
isError: true
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
);
|
|
2077
|
+
server.tool(
|
|
2078
|
+
"list_session_events",
|
|
2079
|
+
"List session lifecycle events (created, exited, task_linked, renamed, archived, unarchived). Use for post-mortem analysis and multi-agent coordination.",
|
|
2080
|
+
{
|
|
2081
|
+
session_id: V.id.optional().describe("Filter by session ID"),
|
|
2082
|
+
event_type: z4.enum(["created", "exited", "task_linked", "renamed", "archived", "unarchived"]).optional().describe("Filter by event type"),
|
|
2083
|
+
limit: z4.number().int().min(1).max(200).optional().describe("Max events to return (default: 50)")
|
|
2084
|
+
},
|
|
2085
|
+
async (args) => {
|
|
2086
|
+
try {
|
|
2087
|
+
let events;
|
|
2088
|
+
if (args.session_id) {
|
|
2089
|
+
events = await rpcCall("sessionEvent:listBySession", {
|
|
2090
|
+
sessionId: args.session_id,
|
|
2091
|
+
limit: args.limit ?? 50
|
|
2092
|
+
});
|
|
2093
|
+
} else {
|
|
2094
|
+
events = await rpcCall("sessionEvent:list", {
|
|
2095
|
+
eventType: args.event_type,
|
|
2096
|
+
limit: args.limit ?? 50
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
return {
|
|
2102
|
+
content: [
|
|
2103
|
+
{
|
|
2104
|
+
type: "text",
|
|
2105
|
+
text: `Error listing session events: ${err instanceof Error ? err.message : err}`
|
|
2106
|
+
}
|
|
2107
|
+
],
|
|
2108
|
+
isError: true
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
);
|
|
1677
2113
|
}
|
|
1678
2114
|
|
|
1679
2115
|
// src/tools/workflows.ts
|
|
@@ -1798,7 +2234,7 @@ function registerWorkflowTools(server) {
|
|
|
1798
2234
|
const workflow = {
|
|
1799
2235
|
id: crypto2.randomUUID(),
|
|
1800
2236
|
name: args.name,
|
|
1801
|
-
icon: args.icon ?? "
|
|
2237
|
+
icon: args.icon ?? "Zap",
|
|
1802
2238
|
iconColor: args.icon_color ?? "#6366f1",
|
|
1803
2239
|
nodes,
|
|
1804
2240
|
edges,
|
|
@@ -2053,7 +2489,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
|
|
|
2053
2489
|
console.error = (...args) => _origError("[mcp:error]", ...args);
|
|
2054
2490
|
async function main() {
|
|
2055
2491
|
configManager.init();
|
|
2056
|
-
const version = true ? "0.4.0-beta.
|
|
2492
|
+
const version = true ? "0.4.0-beta.10" : createRequire(import.meta.url)("../package.json").version;
|
|
2057
2493
|
const server = createMcpServer(version);
|
|
2058
2494
|
const transport = new StdioServerTransport();
|
|
2059
2495
|
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.
|
|
3
|
+
"version": "0.4.0-beta.10",
|
|
4
4
|
"description": "VibeGrid MCP server — task management, git, and workflow tools for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
35
35
|
"libsql": "^0.5.22",
|
|
36
|
-
"pino": "^
|
|
36
|
+
"pino": "^10.3.1",
|
|
37
37
|
"ws": "^8.18.0",
|
|
38
38
|
"zod": "^3.23.0"
|
|
39
39
|
},
|