diff --git a/extension/gravity-web-bridge-0.1.0.vsix b/extension/gravity-web-bridge-0.1.0.vsix new file mode 100644 index 0000000..be21882 Binary files /dev/null and b/extension/gravity-web-bridge-0.1.0.vsix differ diff --git a/server/bridge-client.js b/server/bridge-client.js new file mode 100644 index 0000000..6a4e623 --- /dev/null +++ b/server/bridge-client.js @@ -0,0 +1,182 @@ +/** + * 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'); + } + + /** + * 메시지 전송 + */ + async sendMessage(message, sessionId) { + return this._post('/api/send', { message, sessionId }); + } + + /** + * 코드 편집 승인 + */ + async acceptStep() { + return this._post('/api/accept', {}); + } + + /** + * 코드 편집 거절 + */ + async rejectStep() { + return this._post('/api/reject', {}); + } + + /** + * 터미널 명령 승인 + */ + async acceptTerminal() { + return this._post('/api/accept-terminal', {}); + } + + /** + * 에이전트 설정 조회 + */ + 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; diff --git a/server/index.js b/server/index.js index e571d33..57c9521 100644 --- a/server/index.js +++ b/server/index.js @@ -11,6 +11,7 @@ const path = require('path'); const { WebSocketServer, WebSocket } = require('ws'); const SessionManager = require('./session-manager'); const { AutoDiscovery } = require('./auto-discover'); +const BridgeClient = require('./bridge-client'); const PORT = process.env.PORT || 3300; const app = express(); @@ -252,7 +253,80 @@ function broadcastSessions() { } } -// ─── REST API ─────────────────────────────────────────── +// ─── Bridge API 프록시 ────────────────────────────────── + +const bridge = new BridgeClient(); + +app.get('/api/bridge/health', async (req, res) => { + try { + const health = await bridge.checkHealth(); + res.json(health); + } catch (e) { + res.json({ available: false, error: e.message }); + } +}); + +app.get('/api/bridge/sessions', async (req, res) => { + try { + const data = await bridge.getSessions(); + res.json(data); + } catch (e) { + res.status(502).json({ error: 'Bridge 연결 실패: ' + e.message }); + } +}); + +app.get('/api/bridge/cascades', async (req, res) => { + try { + const data = await bridge.getCascades(); + res.json(data); + } catch (e) { + res.status(502).json({ error: 'Bridge 연결 실패: ' + e.message }); + } +}); + +app.post('/api/bridge/send', async (req, res) => { + try { + const { message, sessionId } = req.body; + const result = await bridge.sendMessage(message, sessionId); + res.json(result); + } catch (e) { + res.status(502).json({ error: e.message }); + } +}); + +app.post('/api/bridge/accept', async (req, res) => { + try { + const result = await bridge.acceptStep(); + res.json(result); + } catch (e) { + res.status(502).json({ error: e.message }); + } +}); + +app.post('/api/bridge/reject', async (req, res) => { + try { + const result = await bridge.rejectStep(); + res.json(result); + } catch (e) { + res.status(502).json({ error: e.message }); + } +}); + +app.post('/api/bridge/accept-terminal', async (req, res) => { + try { + const result = await bridge.acceptTerminal(); + res.json(result); + } catch (e) { + res.status(502).json({ error: e.message }); + } +}); + +// Bridge WS 이벤트 → 프론트엔드 포워딩 +bridge.connectWs((msg) => { + broadcastToAll({ type: 'bridge_event', ...msg }); +}); + +// ─── CDP REST API (레거시) ────────────────────────────── app.get('/api/sessions', (req, res) => { res.json(sessionManager.listSessions());