#!/usr/bin/env node /** * agor-launcher MCP — manages Electrobun app lifecycle. * Tools: start, stop, restart, clean, rebuild, status, kill-stale, build-native */ import { execSync, spawnSync, 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}`; } } /** Non-blocking start: spawn everything detached, return immediately */ function startApp(clean = false) { const steps = []; // Step 1: Kill old Electrobun/WebKit (NOT node — that's us) spawnSync("bash", ["-c", 'pkill -f "AgentOrch" 2>/dev/null; pkill -f "WebKitWebProcess.*9760" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true' ], { timeout: 3000, stdio: "ignore" }); steps.push("[1/4] Stopped old instances"); // Step 1.5: Clean if requested if (clean) { spawnSync("rm", ["-rf", `${EBUN}/build/`, `${EBUN}/node_modules/.electrobun-cache/`], { timeout: 3000 }); steps.push("[1.5] Cleaned build cache"); } // Step 2: Start PTY daemon (if not running) try { const r = spawnSync("pgrep", ["-fc", "agor-ptyd"], { encoding: "utf-8", timeout: 2000 }); const count = (r.stdout || "0").trim(); if (count === "0" || count === "") { const p = spawn(PTYD, [], { detached: true, stdio: "ignore" }); p.unref(); steps.push("[2/4] PTY daemon started"); } else { steps.push(`[2/4] PTY daemon already running (${count})`); } } catch (e) { steps.push(`[2/4] PTY error: ${e.message}`); } // Step 3: Start Vite (if port 9760 not in use) try { const r = spawnSync("fuser", ["9760/tcp"], { encoding: "utf-8", timeout: 2000 }); if (r.status !== 0) { const v = spawn("npx", ["vite", "dev", "--port", "9760", "--host", "localhost"], { cwd: EBUN, detached: true, stdio: "ignore", }); v.unref(); steps.push("[3/4] Vite started on :9760"); } else { steps.push("[3/4] Vite already on :9760"); } } catch (e) { steps.push(`[3/4] Vite error: ${e.message}`); } // Step 4: Launch Electrobun (detached) after a brief pause via a wrapper script const wrapper = spawn("bash", ["-c", `sleep 4; cd "${EBUN}" && npx electrobun dev`], { detached: true, stdio: "ignore", }); wrapper.unref(); steps.push(`[4/4] Electrobun launching in 4s (PID ${wrapper.pid})`); 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: () => { spawnSync("bash", ["-c", 'pkill -f "AgentOrch" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; pkill -f "WebKitWebProcess" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true' ], { timeout: 5000, stdio: "ignore" }); return "Stopped all instances."; }, }, "agor-restart": { description: "Restart Electrobun app. Pass clean=true for clean restart.", schema: { type: "object", properties: { clean: { type: "boolean", default: false } } }, handler: ({ clean }) => { spawnSync("bash", ["-c", 'pkill -f "AgentOrch" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; pkill -f "WebKitWebProcess" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true' ], { timeout: 5000, stdio: "ignore" }); 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: () => { // Stop app (targeted kill — don't kill this Node/MCP process) spawnSync("bash", ["-c", 'pkill -f "AgentOrch" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; pkill -f "WebKitWebProcess" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true' ], { timeout: 5000, stdio: "ignore" }); // Clean spawnSync("rm", ["-rf", `${EBUN}/build/`, `${EBUN}/node_modules/.electrobun-cache/`], { timeout: 5000 }); // npm install const npm = spawnSync("npm", ["install", "--legacy-peer-deps"], { cwd: EBUN, timeout: 60000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); // vite build const vite = spawnSync("npx", ["vite", "build"], { cwd: EBUN, timeout: 60000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); // native build const native = spawnSync("bash", ["-c", `cd ${ROOT}/agor-pty/native && gcc -shared -fPIC -o libagor-resize.so agor_resize.c $(pkg-config --cflags --libs gtk+-3.0) 2>&1`], { timeout: 30000, encoding: "utf-8" }); const cargo = spawnSync("cargo", ["build", "--release"], { cwd: `${ROOT}/agor-pty`, timeout: 120000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); return [ "Rebuild complete:", `npm: ${npm.status === 0 ? 'ok' : 'FAIL'}`, `vite: ${vite.status === 0 ? 'ok' : 'FAIL'}`, `native: ${native.status === 0 ? 'ok' : 'FAIL'}`, `cargo: ${cargo.status === 0 ? 'ok' : 'FAIL'}`, ].join("\n"); }, }, "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: () => { spawnSync("pkill", ["-9", "-f", "agor-ptyd"], { timeout: 3000, stdio: "ignore" }); const r = spawnSync("pgrep", ["-fc", "agor-ptyd"], { encoding: "utf-8", timeout: 2000 }); return `Killed stale ptyd. Remaining: ${(r.stdout || "0").trim()}`; }, }, "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.1.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: typeof result === "string" ? result : JSON.stringify(result) }], }}); } });