|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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> {
|
|
|
| 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}`);
|
|
|
|
|
| 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(),
|
| });
|
|
|
|
|
|
|
|
|
| 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);
|
|
|
|
|
| const newStatus = await redetectCLI();
|
| latestCliStatus = newStatus;
|
|
|
|
|
| io.emit('settings:changed', { settings: updated, cli: newStatus });
|
|
|
| if (typeof callback === 'function') {
|
| callback({ settings: updated, cli: newStatus });
|
| }
|
| });
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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,
|
| });
|
| }
|
| });
|
|
|
|
|
|
|
|
|
| socket.on('prompt:send', async (data: { message: string; id: string }) => {
|
| const { message, id } = data;
|
| console.log(` [Bridge] Prompt: ${message}`);
|
|
|
|
|
| io.emit('prompt:received', {
|
| message, id, from: 'human',
|
| timestamp: new Date().toISOString(),
|
| });
|
|
|
|
|
| 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) {
|
|
|
| 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 {
|
|
|
| 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(),
|
| });
|
| }
|
| });
|
|
|
|
|
|
|
|
|
| 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}`);
|
|
|
| });
|
| });
|
| }
|
|
|