agent-orchestrator/.claude/mcp-servers/agor-launcher/index.mjs

166 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* agor-launcher MCP — manages Electrobun app lifecycle.
* Tools: start, stop, restart, clean, rebuild, status, kill-stale, build-native, logs
*/
import { execSync, spawn } from "child_process";
import { createInterface } from "readline";
const SCRIPT = "/home/hibryda/code/ai/agent-orchestrator/scripts/launch.sh";
const ROOT = "/home/hibryda/code/ai/agent-orchestrator";
const EBUN = `${ROOT}/ui-electrobun`;
const PTYD = `${ROOT}/agor-pty/target/release/agor-ptyd`;
function run(cmd, timeout = 30000) {
try {
return execSync(`bash ${SCRIPT} ${cmd}`, {
timeout,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
} catch (e) {
return `Error: ${e.stderr || e.message}`;
}
}
/** Start the app: stop old → start PTY → start Vite → launch Electrobun (backgrounded) */
function startApp(clean = false) {
const steps = [];
// Step 1: Stop old instances
try {
execSync('pkill -f "electrobun|WebKit|AgentOrch|launcher" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true', {
timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
});
steps.push("[1/4] Stopped old instances");
} catch { steps.push("[1/4] Stop (nothing running)"); }
// Step 1.5: Clean if requested
if (clean) {
try {
execSync(`rm -rf ${EBUN}/build/ ${EBUN}/node_modules/.electrobun-cache/`, { timeout: 5000 });
steps.push("[1.5] Cleaned build cache");
} catch { steps.push("[1.5] Clean skipped"); }
}
// Step 2: Start PTY daemon (if not running)
try {
const ptydCount = execSync("pgrep -fc agor-ptyd 2>/dev/null || echo 0", { encoding: "utf-8" }).trim();
if (ptydCount === "0") {
const ptyd = spawn(PTYD, [], { detached: true, stdio: "ignore" });
ptyd.unref();
steps.push("[2/4] PTY daemon started");
} else {
steps.push(`[2/4] PTY daemon already running (${ptydCount})`);
}
} catch (e) { steps.push(`[2/4] PTY daemon error: ${e.message}`); }
// Step 3: Start Vite (if port 9760 not in use)
try {
const portCheck = execSync("fuser 9760/tcp 2>/dev/null || echo free", { encoding: "utf-8" }).trim();
if (portCheck === "free") {
const vite = spawn("npx", ["vite", "dev", "--port", "9760", "--host", "localhost"], {
cwd: EBUN, detached: true, stdio: "ignore",
});
vite.unref();
steps.push("[3/4] Vite started on :9760");
} else {
steps.push("[3/4] Vite already running on :9760");
}
} catch (e) { steps.push(`[3/4] Vite error: ${e.message}`); }
// Wait for Vite to be ready
try { execSync("sleep 3", { timeout: 5000 }); } catch {}
// Step 4: Launch Electrobun (detached, output to log)
try {
const ebun = spawn("npx", ["electrobun", "dev"], {
cwd: EBUN,
detached: true,
stdio: ["ignore", "ignore", "ignore"],
});
ebun.unref();
steps.push(`[4/4] Electrobun launched (PID ${ebun.pid})`);
} catch (e) { steps.push(`[4/4] Electrobun error: ${e.message}`); }
return steps.join("\n");
}
const TOOLS = {
"agor-start": {
description: "Start Electrobun app (kills old instances first). Pass clean=true to remove build cache.",
schema: { type: "object", properties: { clean: { type: "boolean", default: false } } },
handler: ({ clean }) => startApp(clean),
},
"agor-stop": {
description: "Stop all running Electrobun/PTY/Vite instances.",
schema: { type: "object", properties: {} },
handler: () => run("stop"),
},
"agor-restart": {
description: "Restart Electrobun app. Pass clean=true for clean restart.",
schema: { type: "object", properties: { clean: { type: "boolean", default: false } } },
handler: ({ clean }) => { run("stop"); return startApp(clean); },
},
"agor-clean": {
description: "Remove build artifacts, caches, and temp files.",
schema: { type: "object", properties: {} },
handler: () => run("clean"),
},
"agor-rebuild": {
description: "Full rebuild: clean + npm install + vite build + native C library + PTY daemon.",
schema: { type: "object", properties: {} },
handler: () => run("rebuild", 120000),
},
"agor-status": {
description: "Show running processes, ports, and window status.",
schema: { type: "object", properties: {} },
handler: () => run("status"),
},
"agor-kill-stale": {
description: "Kill ALL stale agor-ptyd instances that accumulated over sessions.",
schema: { type: "object", properties: {} },
handler: () => run("kill-stale"),
},
"agor-build-native": {
description: "Rebuild libagor-resize.so (GTK resize) and agor-ptyd (PTY daemon).",
schema: { type: "object", properties: {} },
handler: () => run("build-native", 120000),
},
};
// Minimal MCP stdio server
const rl = createInterface({ input: process.stdin });
function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n"); }
rl.on("line", (line) => {
let msg;
try { msg = JSON.parse(line); } catch { return; }
if (msg.method === "initialize") {
send({ jsonrpc: "2.0", id: msg.id, result: {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "agor-launcher", version: "1.0.0" },
}});
} else if (msg.method === "notifications/initialized") {
// no-op
} else if (msg.method === "tools/list") {
send({ jsonrpc: "2.0", id: msg.id, result: {
tools: Object.entries(TOOLS).map(([name, t]) => ({
name, description: t.description, inputSchema: t.schema,
})),
}});
} else if (msg.method === "tools/call") {
const tool = TOOLS[msg.params.name];
if (!tool) {
send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Unknown tool: ${msg.params.name}` }});
return;
}
const result = tool.handler(msg.params.arguments || {});
send({ jsonrpc: "2.0", id: msg.id, result: {
content: [{ type: "text", text: result }],
}});
}
});