agent-bridge / server /bridge.ts
algorembrant's picture
Upload 18 files
b9a3ef2 verified
// ---------------------------------------------------------------------------
// Bridge -- Real persistent Antigravity CLI session management
// ---------------------------------------------------------------------------
// Spawns the Antigravity CLI as a persistent interactive subprocess.
// Relays stdin/stdout/stderr over WebSocket so the webapp mirrors the CLI.
// ---------------------------------------------------------------------------
import { Server, Socket } from 'socket.io';
import { spawn, ChildProcess } from 'child_process';
import { ToolRegistry, ToolResult } from './toolRegistry';
import { CLIDetectionResult, detectCLI } from './cliDetector';
import fs from 'fs';
import path from 'path';
const SETTINGS_FILE = path.join(__dirname, '..', 'settings.json');
interface Settings {
cliPath: string;
autoConnect: boolean;
}
interface CLISession {
process: ChildProcess;
logs: string[];
started: number;
}
// Persistent state
let currentSession: CLISession | null = null;
let settings: Settings = loadSettings();
let latestCliStatus: CLIDetectionResult | null = null;
function loadSettings(): Settings {
try {
if (fs.existsSync(SETTINGS_FILE)) {
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8'));
}
} catch { }
return { cliPath: '', autoConnect: false };
}
function saveSettings(s: Settings): void {
settings = s;
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2), 'utf-8');
}
export function getSettings(): Settings {
return settings;
}
export function updateSettings(partial: Partial<Settings>): Settings {
settings = { ...settings, ...partial };
saveSettings(settings);
return settings;
}
export async function redetectCLI(): Promise<CLIDetectionResult> {
// Override detection with user-configured path
if (settings.cliPath) {
process.env.ANTIGRAVITY_CLI_PATH = settings.cliPath;
}
latestCliStatus = await detectCLI();
return latestCliStatus;
}
export function setupBridge(
io: Server,
toolRegistry: ToolRegistry,
cliStatus: CLIDetectionResult
): void {
latestCliStatus = cliStatus;
io.on('connection', (socket: Socket) => {
console.log(` [Bridge] Client connected: ${socket.id}`);
// Send initial state
socket.emit('bridge:init', {
tools: toolRegistry.listTools(),
cli: {
detected: latestCliStatus!.detected,
version: latestCliStatus!.version,
method: latestCliStatus!.method,
path: latestCliStatus!.path,
},
settings: settings,
sessionActive: currentSession !== null,
serverTime: new Date().toISOString(),
});
// ------------------------------------------------------------------
// Settings management (from UI)
// ------------------------------------------------------------------
socket.on('settings:get', (callback: Function) => {
if (typeof callback === 'function') {
callback({ settings, cli: latestCliStatus });
}
});
socket.on('settings:update', async (data: Partial<Settings>, callback: Function) => {
const updated = updateSettings(data);
console.log(` [Bridge] Settings updated:`, updated);
// Re-detect with new path
const newStatus = await redetectCLI();
latestCliStatus = newStatus;
// Notify all clients
io.emit('settings:changed', { settings: updated, cli: newStatus });
if (typeof callback === 'function') {
callback({ settings: updated, cli: newStatus });
}
});
// ------------------------------------------------------------------
// CLI Session management
// ------------------------------------------------------------------
socket.on('cli:start', async (data: { cliPath?: string }) => {
const cliPath = data?.cliPath || settings.cliPath || latestCliStatus?.path;
if (!cliPath) {
socket.emit('cli:error', {
message: 'No CLI path configured. Go to Settings and set the path to your Antigravity CLI executable.',
});
return;
}
// Kill existing session
if (currentSession) {
try { currentSession.process.kill('SIGTERM'); } catch { }
currentSession = null;
}
console.log(` [Bridge] Starting CLI session: ${cliPath}`);
io.emit('cli:starting', { path: cliPath });
try {
const child = spawn(cliPath, [], {
shell: true,
cwd: process.cwd(),
env: { ...process.env, TERM: 'dumb', NO_COLOR: '1' },
stdio: ['pipe', 'pipe', 'pipe'],
});
currentSession = {
process: child,
logs: [],
started: Date.now(),
};
child.stdout?.on('data', (chunk: Buffer) => {
const text = chunk.toString();
currentSession?.logs.push(text);
io.emit('cli:stdout', { data: text, timestamp: Date.now() });
});
child.stderr?.on('data', (chunk: Buffer) => {
const text = chunk.toString();
currentSession?.logs.push(`[stderr] ${text}`);
io.emit('cli:stderr', { data: text, timestamp: Date.now() });
});
child.on('close', (code: number | null) => {
console.log(` [Bridge] CLI session ended: exit code ${code}`);
io.emit('cli:ended', { exitCode: code });
currentSession = null;
});
child.on('error', (err: Error) => {
console.error(` [Bridge] CLI error: ${err.message}`);
io.emit('cli:error', { message: `Failed to start CLI: ${err.message}` });
currentSession = null;
});
io.emit('cli:started', { path: cliPath, timestamp: Date.now() });
} catch (err: any) {
io.emit('cli:error', { message: `Failed to spawn CLI: ${err.message}` });
}
});
socket.on('cli:stop', () => {
if (currentSession) {
try { currentSession.process.kill('SIGTERM'); } catch { }
currentSession = null;
io.emit('cli:ended', { exitCode: null, reason: 'user-stopped' });
}
});
socket.on('cli:status', (callback: Function) => {
if (typeof callback === 'function') {
callback({
active: currentSession !== null,
uptime: currentSession ? Date.now() - currentSession.started : 0,
logLength: currentSession?.logs.length || 0,
});
}
});
// ------------------------------------------------------------------
// Prompt handling -- relay to CLI or match tools
// ------------------------------------------------------------------
socket.on('prompt:send', async (data: { message: string; id: string }) => {
const { message, id } = data;
console.log(` [Bridge] Prompt: ${message}`);
// Broadcast to all clients
io.emit('prompt:received', {
message, id, from: 'human',
timestamp: new Date().toISOString(),
});
// Check for tool match first
const match = toolRegistry.matchTool(message);
if (match) {
const { tool, params } = match;
console.log(` [Bridge] Tool matched: ${tool.name}`);
io.emit('tool:started', { id, toolName: tool.name, params, timestamp: new Date().toISOString() });
try {
const result: ToolResult = await tool.execute(params, (progress: string) => {
io.emit('tool:progress', { id, toolName: tool.name, progress, timestamp: new Date().toISOString() });
});
io.emit('tool:completed', { id, toolName: tool.name, result, timestamp: new Date().toISOString() });
} catch (err: any) {
io.emit('tool:error', { id, toolName: tool.name, error: err.message, timestamp: new Date().toISOString() });
}
} else if (currentSession?.process?.stdin) {
// Send prompt to the running CLI process
try {
currentSession.process.stdin.write(message + '\n');
console.log(` [Bridge] Sent to CLI stdin: ${message}`);
} catch (err: any) {
io.emit('cli:error', { message: `Failed to write to CLI: ${err.message}` });
}
} else {
// No tool match, no CLI session
io.emit('agent:message', {
id, from: 'system',
message: currentSession
? 'CLI session is active but stdin is unavailable.'
: 'No CLI session is running. Click "Connect" in Settings to start the Antigravity CLI, or configure the path first.',
timestamp: new Date().toISOString(),
});
}
});
// ------------------------------------------------------------------
// MCP tool discovery & invocation
// ------------------------------------------------------------------
socket.on('tool:list', (callback: Function) => {
if (typeof callback === 'function') {
callback({ status: 'success', tools: toolRegistry.listTools() });
}
});
socket.on('tool:invoke', async (data: { toolName: string; params: any }, callback: Function) => {
const tool = toolRegistry.getTool(data.toolName);
if (!tool) {
if (typeof callback === 'function') callback({ status: 'error', message: `Tool "${data.toolName}" not found.` });
return;
}
try {
const result = await tool.execute(data.params, (progress: string) => {
io.emit('tool:progress', { toolName: data.toolName, progress, timestamp: new Date().toISOString() });
});
if (typeof callback === 'function') callback({ status: 'success', result });
} catch (err: any) {
if (typeof callback === 'function') callback({ status: 'error', message: err.message });
}
});
socket.on('agent:response', (data: any) => {
io.emit('agent:message', { ...data, from: 'agent', timestamp: new Date().toISOString() });
});
socket.on('disconnect', () => {
console.log(` [Bridge] Client disconnected: ${socket.id}`);
// Don't kill CLI session on disconnect -- it persists
});
});
}