feat(extension): Gravity Web Bridge — antigravity-sdk based VS Code extension with REST/WS bridge
This commit is contained in:
2
extension/.gitignore
vendored
Normal file
2
extension/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
out/
|
||||||
|
node_modules/
|
||||||
1035
extension/package-lock.json
generated
Normal file
1035
extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
extension/package.json
Normal file
29
extension/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "gravity-web-bridge",
|
||||||
|
"displayName": "Gravity Web Bridge",
|
||||||
|
"description": "Antigravity ↔ Gravity Web 브릿지 — 대화 스트리밍, 메시지 전송, 승인/거절 제어",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"publisher": "variet",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.85.0"
|
||||||
|
},
|
||||||
|
"categories": ["Other"],
|
||||||
|
"activationEvents": ["onStartupFinished"],
|
||||||
|
"main": "./out/extension.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p .",
|
||||||
|
"watch": "tsc -watch -p .",
|
||||||
|
"package": "vsce package"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"antigravity-sdk": "latest",
|
||||||
|
"express": "^4.18.0",
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/vscode": "^1.85.0",
|
||||||
|
"@types/ws": "^8.5.0",
|
||||||
|
"@types/express": "^4.17.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
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() { }
|
||||||
20
extension/tsconfig.json
Normal file
20
extension/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2020",
|
||||||
|
"outDir": "out",
|
||||||
|
"lib": [
|
||||||
|
"ES2020"
|
||||||
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"out"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user