feat(extension): Gravity Web Bridge — antigravity-sdk based VS Code extension with REST/WS bridge
This commit is contained in:
238
extension/src/extension.ts
Normal file
238
extension/src/extension.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Gravity Web Bridge Extension
|
||||
*
|
||||
* Antigravity IDE 내부에서 동작하여:
|
||||
* 1. antigravity-sdk로 대화/상태 접근
|
||||
* 2. 로컬 HTTP/WS 서버를 열어 Gravity Web 서버와 통신
|
||||
*
|
||||
* API:
|
||||
* GET /api/sessions → 대화 목록
|
||||
* GET /api/session/:id → 대화 상세
|
||||
* POST /api/send → 메시지 전송
|
||||
* POST /api/accept → 스텝 승인
|
||||
* POST /api/reject → 스텝 거절
|
||||
* WS /ws → 실시간 이벤트 스트림
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// antigravity-sdk는 런타임에 로드 (VS Code Extension 환경에서만 동작)
|
||||
let AntigravitySDK: any;
|
||||
|
||||
const BRIDGE_PORT = 9850;
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const output = vscode.window.createOutputChannel('Gravity Web Bridge');
|
||||
output.appendLine('[Bridge] Extension 활성화...');
|
||||
|
||||
// antigravity-sdk 로드
|
||||
try {
|
||||
const sdkModule = require('antigravity-sdk');
|
||||
AntigravitySDK = sdkModule.AntigravitySDK;
|
||||
} catch (err: any) {
|
||||
output.appendLine(`[Bridge] antigravity-sdk 로드 실패: ${err.message}`);
|
||||
vscode.window.showErrorMessage('Gravity Web Bridge: antigravity-sdk를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// SDK 초기화
|
||||
const sdk = new AntigravitySDK(context);
|
||||
try {
|
||||
await sdk.initialize();
|
||||
output.appendLine('[Bridge] SDK 초기화 완료');
|
||||
} catch (err: any) {
|
||||
output.appendLine(`[Bridge] SDK 초기화 실패: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Express + WS 서버
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const { WebSocketServer } = require('ws');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocketServer({ server, path: '/ws' });
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// CORS 허용
|
||||
app.use((_req: any, res: any, next: any) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
next();
|
||||
});
|
||||
|
||||
// --- REST API ---
|
||||
|
||||
app.get('/api/sessions', async (_req: any, res: any) => {
|
||||
try {
|
||||
const sessions = await sdk.cascade.getSessions();
|
||||
res.json({ sessions });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/send', async (req: any, res: any) => {
|
||||
try {
|
||||
const { message, sessionId } = req.body;
|
||||
if (sessionId) {
|
||||
await sdk.cascade.focusSession(sessionId);
|
||||
}
|
||||
await sdk.cascade.sendPrompt(message);
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/accept', async (_req: any, res: any) => {
|
||||
try {
|
||||
await sdk.cascade.acceptStep();
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/reject', async (_req: any, res: any) => {
|
||||
try {
|
||||
await sdk.cascade.rejectStep();
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/accept-terminal', async (_req: any, res: any) => {
|
||||
try {
|
||||
await sdk.cascade.acceptTerminalCommand();
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/preferences', async (_req: any, res: any) => {
|
||||
try {
|
||||
const prefs = await sdk.cascade.getPreferences();
|
||||
res.json({ preferences: prefs });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/health', (_req: any, res: any) => {
|
||||
res.json({ status: 'ok', port: BRIDGE_PORT, version: '0.1.0' });
|
||||
});
|
||||
|
||||
// --- WebSocket 이벤트 스트림 ---
|
||||
|
||||
const wsClients = new Set<any>();
|
||||
|
||||
wss.on('connection', (ws: any) => {
|
||||
wsClients.add(ws);
|
||||
output.appendLine('[Bridge WS] 클라이언트 연결');
|
||||
|
||||
ws.on('close', () => {
|
||||
wsClients.delete(ws);
|
||||
output.appendLine('[Bridge WS] 클라이언트 해제');
|
||||
});
|
||||
|
||||
// 초기 세션 목록 전송
|
||||
sdk.cascade.getSessions().then((sessions: any) => {
|
||||
ws.send(JSON.stringify({ type: 'sessions', sessions }));
|
||||
}).catch(() => { });
|
||||
});
|
||||
|
||||
function broadcast(data: any) {
|
||||
const json = JSON.stringify(data);
|
||||
for (const ws of wsClients) {
|
||||
if (ws.readyState === 1) { // WebSocket.OPEN
|
||||
ws.send(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SDK 모니터 이벤트 → WS 브로드캐스트
|
||||
sdk.monitor.onStepCountChanged((e: any) => {
|
||||
broadcast({
|
||||
type: 'step_changed',
|
||||
title: e.title,
|
||||
delta: e.delta,
|
||||
newCount: e.newCount,
|
||||
sessionId: e.sessionId,
|
||||
});
|
||||
});
|
||||
|
||||
sdk.monitor.onActiveSessionChanged((e: any) => {
|
||||
broadcast({
|
||||
type: 'session_changed',
|
||||
title: e.title,
|
||||
sessionId: e.sessionId,
|
||||
});
|
||||
});
|
||||
|
||||
sdk.monitor.onNewConversation(() => {
|
||||
broadcast({ type: 'new_conversation' });
|
||||
// 세션 목록 갱신
|
||||
sdk.cascade.getSessions().then((sessions: any) => {
|
||||
broadcast({ type: 'sessions', sessions });
|
||||
}).catch(() => { });
|
||||
});
|
||||
|
||||
sdk.monitor.onStateChanged((e: any) => {
|
||||
broadcast({
|
||||
type: 'state_changed',
|
||||
key: e.key,
|
||||
previousSize: e.previousSize,
|
||||
newSize: e.newSize,
|
||||
});
|
||||
});
|
||||
|
||||
sdk.monitor.start(3000, 5000); // USS 3초, trajectory 5초 폴링
|
||||
|
||||
// --- LS Bridge (Language Server 직접 접근) ---
|
||||
|
||||
app.get('/api/cascades', async (_req: any, res: any) => {
|
||||
try {
|
||||
const cascades = await sdk.ls.listCascades();
|
||||
res.json({ cascades });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/ls/send', async (req: any, res: any) => {
|
||||
try {
|
||||
const { cascadeId, text, model } = req.body;
|
||||
await sdk.ls.sendMessage({ cascadeId, text, model });
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- 서버 시작 ---
|
||||
|
||||
server.listen(BRIDGE_PORT, () => {
|
||||
output.appendLine(`[Bridge] 서버 시작: http://localhost:${BRIDGE_PORT}`);
|
||||
vscode.window.showInformationMessage(
|
||||
`Gravity Web Bridge 활성화 — localhost:${BRIDGE_PORT}`
|
||||
);
|
||||
});
|
||||
|
||||
// 정리
|
||||
context.subscriptions.push(sdk);
|
||||
context.subscriptions.push({
|
||||
dispose: () => {
|
||||
server.close();
|
||||
sdk.monitor.stop();
|
||||
output.appendLine('[Bridge] 서버 종료');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function deactivate() { }
|
||||
Reference in New Issue
Block a user