#!/usr/bin/env node /** * Multi-Agent Gateway Bridge * * Sends commands between Clawdbot gateway instances via the HTTP API. * Each agent has its own gateway port and auth token. * * Usage: * node agent-bridge.js [args...] * * Targets: * rivet - Port 18789, token: rivet-api-2026 * builder - Port 18790, token: builder-api-2026 * susan - Port 18792, token: susan-api-2026 * harper - Port 18796, token: harper-api-2026 * sentinel - Port 18800, token: sentinel-api-2026 * radar - Port 18804, token: radar-api-2026 * herald - Port 18808, token: herald-api-2026 * cog - Port 18812, token: cog-api-2026 * ccvps - Direct CloudCode trigger bridge (local trigger script) * * Actions: * status - Get gateway status (or last ccvps run status) * wake - Send a wake event with text (via local inbox.js) * sessions - List active sessions * send - Send message to a specific session * invoke [json-args] - Invoke a tool directly * run - (ccvps only) trigger CloudCode with inline task * inbox - (ccvps only) trigger CloudCode from RIVET-TO-CCVPS.md * logs [lines] - (ccvps only) tail latest ccvps log * * Examples: * node agent-bridge.js rivet status * node agent-bridge.js rivet wake "Check BUILDER-INBOX.md for new task" * node agent-bridge.js builder wake "New deployment ready" * node agent-bridge.js susan status * node agent-bridge.js rivet invoke sessions_list '{}' */ const http = require('http'); const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); // Agent registry — add new agents here const AGENTS = { rivet: { port: 18789, token: 'rivet-api-2026', name: 'Rivet', kind: 'gateway' }, builder: { port: 18790, token: 'builder-api-2026', name: 'Builder', kind: 'gateway' }, susan: { port: 18792, token: 'susan-api-2026', name: 'Susan', kind: 'gateway' }, harper: { port: 18796, token: 'harper-api-2026', name: 'Harper', kind: 'gateway' }, sentinel: { port: 18800, token: 'sentinel-api-2026', name: 'Sentinel', kind: 'gateway' }, radar: { port: 18804, token: 'radar-api-2026', name: 'Radar', kind: 'gateway' }, herald: { port: 18808, token: 'herald-api-2026', name: 'Herald', kind: 'gateway' }, cog: { port: 18812, token: 'cog-api-2026', name: 'Cog', kind: 'gateway' }, ccvps: { name: 'CCVPS', kind: 'ccvps', triggerScript: '/home/ccuser/rateright-growth/rivet/scripts/trigger-ccvps.sh', logDir: '/home/ccuser/rateright-growth/rivet/logs/ccvps', inboxFile: '/home/ccuser/rateright-growth/rivet/RIVET-TO-CCVPS.md', }, }; // Parse args const target = process.argv[2]; const action = process.argv[3]; const arg1 = process.argv[4] || ''; const arg2 = process.argv[5] || ''; if (!target || !action) { console.error('Usage: agent-bridge.js [args...]'); console.error('Targets:', Object.keys(AGENTS).join(', ')); console.error('Actions: status, wake, sessions, send, invoke'); process.exit(1); } const agent = AGENTS[target]; if (!agent) { console.error(`Unknown target "${target}". Available: ${Object.keys(AGENTS).join(', ')}`); process.exit(1); } function httpPost(port, path, token, body) { return new Promise((resolve, reject) => { const data = JSON.stringify(body); const req = http.request({ hostname: '127.0.0.1', port, path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'Content-Length': Buffer.byteLength(data), }, timeout: 10000, }, (res) => { let buf = ''; res.on('data', chunk => buf += chunk); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(buf) }); } catch { resolve({ status: res.statusCode, body: buf }); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.write(data); req.end(); }); } function runCCVPS(agentCfg, mode, payload = '') { if (!agentCfg?.triggerScript) { throw new Error('ccvps trigger script is not configured'); } const cmd = [agentCfg.triggerScript]; if (mode === 'inbox') cmd.push('--inbox'); else if (mode === 'file') cmd.push('--file', payload); else if (mode === 'run') cmd.push(payload); else throw new Error(`unsupported ccvps mode: ${mode}`); const res = spawnSync('bash', cmd, { encoding: 'utf8' }); // Always inspect latest log for clear failure/success reason let latestLog = null; let latestTail = ''; try { const logInfo = tailLatestCCVPSLog(agentCfg, 80); latestLog = logInfo.logFile; latestTail = logInfo.content || ''; } catch { // ignore, fallback to stdout/stderr } if (res.status !== 0) { const err = (latestTail || res.stderr || res.stdout || 'unknown error').trim(); throw new Error(`ccvps trigger failed: ${err}`); } const output = (res.stdout || '').trim(); return { ok: true, mode, output, logFile: latestLog, logTail: latestTail }; } function tailLatestCCVPSLog(agentCfg, lines = 80) { const dir = agentCfg?.logDir; if (!dir || !fs.existsSync(dir)) { throw new Error(`ccvps log dir missing: ${dir}`); } const files = fs.readdirSync(dir) .filter((f) => f.startsWith('task_') && f.endsWith('.log')) .map((f) => ({ f, p: path.join(dir, f), m: fs.statSync(path.join(dir, f)).mtimeMs })) .sort((a, b) => b.m - a.m); if (!files.length) { return { ok: true, logFile: null, content: '(no ccvps logs yet)' }; } const latest = files[0].p; const content = spawnSync('tail', ['-n', String(lines), latest], { encoding: 'utf8' }); return { ok: true, logFile: latest, content: (content.stdout || '').trim(), }; } function sendWakeViaInbox(targetAgent, text) { // Use shared inbox system for wake semantics (stable across OpenClaw versions) const subject = `Wake: ${targetAgent}`; const body = text; const cmd = [ '/home/ccuser/shared/scripts/inbox.js', 'send', '--from', 'builder', '--to', targetAgent, '--subject', subject, '--body', body, ]; const res = spawnSync('node', cmd, { encoding: 'utf8' }); if (res.status !== 0) { const err = (res.stderr || res.stdout || 'unknown error').trim(); throw new Error(`wake via inbox failed: ${err}`); } return { ok: true, method: 'inbox', target: targetAgent, subject, body, output: (res.stdout || '').trim(), }; } async function run() { try { let result; // CCVPS direct bridge mode (no gateway API) if (agent.kind === 'ccvps') { switch (action) { case 'run': { if (!arg1) { console.error('Usage: agent-bridge.js ccvps run '); process.exit(1); } const out = runCCVPS(agent, 'run', [arg1, arg2].filter(Boolean).join(' ').trim()); console.log(JSON.stringify(out, null, 2)); return; } case 'inbox': { const out = runCCVPS(agent, 'inbox'); console.log(JSON.stringify(out, null, 2)); return; } case 'logs': { const lines = arg1 ? Number(arg1) : 80; const out = tailLatestCCVPSLog(agent, Number.isFinite(lines) ? lines : 80); console.log(JSON.stringify(out, null, 2)); return; } case 'status': { const out = tailLatestCCVPSLog(agent, 20); console.log(JSON.stringify({ ok: true, target: 'ccvps', ...out }, null, 2)); return; } default: console.error('CCVPS actions: run, inbox, logs, status'); process.exit(1); } } switch (action) { case 'status': // Use tools/invoke to call session_status result = await httpPost(agent.port, '/tools/invoke', agent.token, { tool: 'session_status', args: {}, }); break; case 'wake': if (!arg1) { console.error('Usage: agent-bridge.js wake '); process.exit(1); } // Cron tool is not universally available; use inbox-based wake instead. const wakeResult = sendWakeViaInbox(target, arg1); console.log(JSON.stringify(wakeResult, null, 2)); return; case 'sessions': result = await httpPost(agent.port, '/tools/invoke', agent.token, { tool: 'sessions_list', args: { limit: 10 }, }); break; case 'send': if (!arg1 || !arg2) { console.error('Usage: agent-bridge.js send '); process.exit(1); } result = await httpPost(agent.port, '/tools/invoke', agent.token, { tool: 'sessions_send', args: { sessionKey: arg1, message: arg2 }, }); break; case 'invoke': if (!arg1) { console.error('Usage: agent-bridge.js invoke [json-args]'); process.exit(1); } const args = arg2 ? JSON.parse(arg2) : {}; result = await httpPost(agent.port, '/tools/invoke', agent.token, { tool: arg1, args, }); break; default: console.error(`Unknown action "${action}". Use: status, wake, sessions, send, invoke`); process.exit(1); } if (result.status === 200 && result.body?.ok) { console.log(JSON.stringify(result.body.result || result.body, null, 2)); } else { console.error(`[${agent.name}:${agent.port}] ${action} FAILED (HTTP ${result.status})`); console.error(JSON.stringify(result.body, null, 2)); process.exit(1); } } catch (err) { console.error(`[${agent.name}:${agent.port}] Connection failed: ${err.message}`); process.exit(1); } } run();