/** * Bridge Client — Antigravity Extension Bridge (localhost:9850)와 통신 * * Extension이 설치된 Antigravity에서 제공하는 REST API + WebSocket을 통해 * 대화 목록, 메시지 전송, 승인/거절 등을 수행합니다. */ const http = require('http'); const WebSocket = require('ws'); const BRIDGE_HOST = 'localhost'; const BRIDGE_PORT = 9850; const BRIDGE_BASE = `http://${BRIDGE_HOST}:${BRIDGE_PORT}`; class BridgeClient { constructor() { this.ws = null; this.connected = false; this.listeners = new Map(); this.reconnectTimer = null; } /** * Bridge 서버 상태 확인 */ async checkHealth() { try { const data = await this._get('/api/health'); return { available: true, ...data }; } catch { return { available: false }; } } /** * 대화 목록 (제목, stepCount, 수정일시) */ async getSessions() { return this._get('/api/sessions'); } /** * 전체 Cascade 데이터 (대화 내용, 스텝, notify_user 등) */ async getCascades() { return this._get('/api/cascades'); } /** * 개별 대화의 전체 스텝/메시지 (GetCascadeTrajectory RPC) */ async getTrajectory(cascadeId) { return this._post('/api/ls/rpc', { method: 'GetCascadeTrajectory', payload: { cascadeId }, }); } /** * 메시지 전송 */ async sendMessage(message, sessionId) { return this._post('/api/send', { message, sessionId }); } /** * 승인/거절 액션 (통합) * action: 'acceptStep' | 'rejectStep' | 'acceptCommand' | 'rejectCommand' | 'acceptTerminal' | 'rejectTerminal' */ async sendAction(action) { return this._post('/api/action', { action }); } /** * 에이전트 설정 조회 */ async getPreferences() { return this._get('/api/preferences'); } /** * WebSocket 실시간 이벤트 연결 */ connectWs(onMessage) { if (this.ws) { this.ws.close(); } try { this.ws = new WebSocket(`ws://${BRIDGE_HOST}:${BRIDGE_PORT}/ws`); } catch (err) { console.log(`[Bridge WS] 연결 실패: ${err.message}`); this._scheduleReconnect(onMessage); return; } this.ws.on('open', () => { this.connected = true; console.log('[Bridge WS] 연결됨'); }); this.ws.on('message', (data) => { try { const msg = JSON.parse(data.toString()); onMessage(msg); } catch { } }); this.ws.on('close', () => { this.connected = false; console.log('[Bridge WS] 연결 해제'); this._scheduleReconnect(onMessage); }); this.ws.on('error', () => { this.connected = false; }); } _scheduleReconnect(onMessage) { if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; console.log('[Bridge WS] 재연결 시도...'); this.connectWs(onMessage); }, 5000); } disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } } // --- HTTP 헬퍼 --- _get(path) { return new Promise((resolve, reject) => { http.get(`${BRIDGE_BASE}${path}`, (res) => { let body = ''; res.on('data', (c) => body += c); res.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('Invalid JSON')); } }); }).on('error', reject); }); } _post(path, data) { return new Promise((resolve, reject) => { const body = JSON.stringify(data); const req = http.request(`${BRIDGE_BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }, }, (res) => { let resBody = ''; res.on('data', (c) => resBody += c); res.on('end', () => { try { resolve(JSON.parse(resBody)); } catch { reject(new Error('Invalid JSON')); } }); }); req.on('error', reject); req.write(body); req.end(); }); } } module.exports = BridgeClient;