apiVersion: v1 data: hub.js: | #!/usr/bin/env node // agent-hub: lives inside the pi container (not a sidecar) so it can spawn // `pi -p --mode json` directly. One persistent in-cluster service -- // POST /run to trigger an agent, GET /console (WebSocket) to watch every // concurrent run live as it happens, relaying pi's own session protocol // verbatim (same event shape Claude Code sessions use). const http = require("node:http"); const crypto = require("node:crypto"); const { spawn } = require("node:child_process"); const readline = require("node:readline"); const { WebSocketServer } = require("ws"); const PORT = process.env.HUB_PORT || 9090; const sessions = new Map(); // id -> {id, agent, status, events, startedAt, endedAt} const viewers = new Set(); // WebSocket connections watching /console function broadcast(type, session) { const msg = JSON.stringify({ type, session }); for (const ws of viewers) { if (ws.readyState === ws.OPEN) ws.send(msg); } } function startSession(agent) { const id = crypto.randomUUID(); const session = { id, agent, status: "running", events: [], startedAt: new Date().toISOString() }; sessions.set(id, session); broadcast("start", session); return session; } function addEvent(session, rawLine) { session.events.push(JSON.parse(rawLine)); broadcast("event", session); } function endSession(session, status) { session.status = status; session.endedAt = new Date().toISOString(); broadcast("end", session); } // Spawns `pi -p --mode json ` and relays it live. Same shape as the // old agent-run.js wrapper, just in-process instead of a separate exec. function runAgent(agent, prompt, extraArgs = []) { const session = startSession(agent); const child = spawn("pi", ["-p", "--mode", "json", ...extraArgs, prompt], { stdio: ["ignore", "pipe", "pipe"], }); const rl = readline.createInterface({ input: child.stdout }); rl.on("line", (line) => { if (!line.trim()) return; try { addEvent(session, line); } catch { // non-JSON stdout noise, ignore } }); child.stderr.on("data", (chunk) => process.stderr.write(chunk)); child.on("close", (code) => endSession(session, code === 0 ? "done" : "error")); return session; } const server = http.createServer((req, res) => { const url = new URL(req.url, "http://localhost"); if (url.pathname === "/healthz") { res.writeHead(200).end(); return; } if (url.pathname === "/sessions" && req.method === "GET") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify([...sessions.values()])); return; } if (url.pathname === "/run" && req.method === "POST") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { try { const { agent, prompt, provider, model } = JSON.parse(body); if (!agent || !prompt) throw new Error("agent and prompt are required"); const extraArgs = []; if (provider) extraArgs.push("--provider", provider); if (model) extraArgs.push("--model", model); const session = runAgent(agent, prompt, extraArgs); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ id: session.id })); } catch (err) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: err.message })); } }); return; } res.writeHead(404).end(); }); const wss = new WebSocketServer({ server, path: "/console" }); wss.on("connection", (ws) => { for (const session of sessions.values()) { ws.send(JSON.stringify({ type: "snapshot", session })); } viewers.add(ws); ws.on("close", () => viewers.delete(ws)); }); server.listen(PORT, () => console.log(`agent-hub listening on :${PORT}`)); kind: ConfigMap metadata: name: hub-src namespace: agent-pod