| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | 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}`);
|
| |
|
| | });
|
| | });
|
| | }
|
| |
|