Files

223 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Beanstalk Gateway Client
*
* Connects local Hanzo Bot to beans.talk relay server.
* Reads config from .beanstalk/gateway.json or environment variables.
*/
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Find config
function loadConfig() {
// Try environment variables first
if (process.env.GATEWAY_URL && process.env.GATEWAY_TOKEN) {
return {
url: process.env.GATEWAY_URL,
token: process.env.GATEWAY_TOKEN,
botUrl: process.env.BOT_URL || 'http://localhost:18789',
};
}
// Try workspace config
const workspacePaths = [
process.cwd(),
process.env.BOT_WORKSPACE,
path.join(os.homedir(), '.bot'),
path.join(os.homedir(), 'clawd'),
].filter(Boolean);
for (const base of workspacePaths) {
const configPath = path.join(base, '.beanstalk', 'gateway.json');
if (fs.existsSync(configPath)) {
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
console.log(`[Gateway] Loaded config from ${configPath}`);
return {
url: config.url,
token: config.token,
botUrl: config.botUrl || 'http://localhost:18789',
};
} catch (err) {
console.error(`[Gateway] Failed to parse ${configPath}:`, err.message);
}
}
}
return null;
}
const config = loadConfig();
if (!config) {
console.error('[Gateway] No configuration found.');
console.error('');
console.error('Set environment variables:');
console.error(' GATEWAY_URL=wss://beanstalk.fly.dev/ws/gw_xxx');
console.error(' GATEWAY_TOKEN=gt_xxx');
console.error('');
console.error('Or create .beanstalk/gateway.json in your workspace:');
console.error(' {"url": "wss://...", "token": "gt_..."}');
process.exit(1);
}
const STATUS_INTERVAL = 10000;
const RECONNECT_DELAY = 5000;
let ws = null;
let reconnectTimer = null;
let statusTimer = null;
let authenticated = false;
/**
* Fetch status from local Hanzo Bot
*/
async function getHanzo BotStatus() {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${config.botUrl}/status`, {
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return {
timestamp: new Date().toISOString(),
agents: [{
id: 'main',
name: data.agent?.name || 'Hanzo Bot',
status: 'running',
model: data.agent?.model,
sessions: data.sessions?.active || 0,
}],
channels: (data.channels || []).map(ch => ({
id: ch.id || ch.type,
name: ch.name || ch.type,
type: ch.type,
status: ch.connected ? 'connected' : 'disconnected',
messagesProcessed: ch.messagesProcessed || 0,
})),
system: { uptime: data.uptime },
};
} catch {
return {
timestamp: new Date().toISOString(),
agents: [{ id: 'main', name: 'Hanzo Bot', status: 'stopped' }],
channels: [],
};
}
}
/**
* Execute command on local Hanzo Bot
*/
async function executeCommand(commandId, action, payload) {
console.log(`[Gateway] Executing: ${action}`, payload || '');
try {
const endpoints = {
restart: '/restart',
stop: '/stop',
start: '/start',
status: '/status',
};
const endpoint = endpoints[action] || '/command';
const method = action === 'status' ? 'GET' : 'POST';
const body = endpoints[action] ? null : JSON.stringify({ action, ...payload });
const response = await fetch(`${config.botUrl}${endpoint}`, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body,
});
const result = await response.json().catch(() => ({ ok: response.ok }));
return { type: 'command_result', commandId, success: response.ok, result };
} catch (err) {
return { type: 'command_result', commandId, success: false, result: { error: err.message } };
}
}
/**
* Connect to relay
*/
function connect() {
if (ws) ws.terminate();
console.log(`[Gateway] Connecting to relay...`);
ws = new WebSocket(config.url);
authenticated = false;
ws.on('open', () => {
console.log('[Gateway] Connected, authenticating...');
ws.send(JSON.stringify({ type: 'auth', token: config.token }));
});
ws.on('message', async (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.type === 'auth_ok') {
console.log('[Gateway] ✓ Authenticated');
authenticated = true;
startStatusUpdates();
} else if (msg.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }));
} else if (msg.type === 'command') {
const result = await executeCommand(msg.commandId, msg.action, msg.payload);
ws.send(JSON.stringify(result));
} else if (msg.type === 'error') {
console.error('[Gateway] Error:', msg.message);
}
} catch (err) {
console.error('[Gateway] Parse error:', err.message);
}
});
ws.on('close', (code, reason) => {
console.log(`[Gateway] Disconnected (${code})`);
authenticated = false;
stopStatusUpdates();
scheduleReconnect();
});
ws.on('error', (err) => console.error('[Gateway] Error:', err.message));
}
function scheduleReconnect() {
if (reconnectTimer) return;
console.log(`[Gateway] Reconnecting in ${RECONNECT_DELAY/1000}s...`);
reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, RECONNECT_DELAY);
}
function startStatusUpdates() {
stopStatusUpdates();
sendStatus();
statusTimer = setInterval(sendStatus, STATUS_INTERVAL);
}
function stopStatusUpdates() {
if (statusTimer) { clearInterval(statusTimer); statusTimer = null; }
}
async function sendStatus() {
if (!ws || ws.readyState !== WebSocket.OPEN || !authenticated) return;
ws.send(JSON.stringify({ type: 'status', payload: await getHanzo BotStatus() }));
}
process.on('SIGINT', () => { console.log('\n[Gateway] Shutting down...'); stopStatusUpdates(); if (ws) ws.close(); process.exit(0); });
process.on('SIGTERM', () => { stopStatusUpdates(); if (ws) ws.close(); process.exit(0); });
console.log('[Gateway] Beanstalk Gateway Client v1.0.0');
console.log(`[Gateway] Hanzo Bot: ${config.botUrl}`);
connect();