289 lines
9.0 KiB
TypeScript
289 lines
9.0 KiB
TypeScript
/**
|
|
* 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.get('/api/conversation/:id', async (req: any, res: any) => {
|
|
try {
|
|
const conversation = await sdk.ls.getConversation(req.params.id);
|
|
res.json(conversation);
|
|
} catch (err: any) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// 임의 LS RPC 호출 (개발/디버그)
|
|
app.post('/api/ls/rpc', async (req: any, res: any) => {
|
|
try {
|
|
const { method, payload } = req.body;
|
|
const result = await sdk.ls.rawRPC(method, payload || {});
|
|
res.json(result);
|
|
} 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 });
|
|
}
|
|
});
|
|
|
|
// --- 승인/거절 액션 (Gravity Web에서 직접 조작) ---
|
|
|
|
app.post('/api/action', async (req: any, res: any) => {
|
|
try {
|
|
const { action } = req.body;
|
|
const commandMap: Record<string, string> = {
|
|
acceptStep: 'antigravity.agent.acceptAgentStep',
|
|
rejectStep: 'antigravity.agent.rejectAgentStep',
|
|
acceptCommand: 'antigravity.command.accept',
|
|
rejectCommand: 'antigravity.command.reject',
|
|
acceptTerminal: 'antigravity.terminalCommand.accept',
|
|
rejectTerminal: 'antigravity.terminalCommand.reject',
|
|
};
|
|
|
|
const cmd = commandMap[action];
|
|
if (!cmd) {
|
|
res.status(400).json({ error: `Unknown action: ${action}` });
|
|
return;
|
|
}
|
|
|
|
await vscode.commands.executeCommand(cmd);
|
|
output.appendLine(`[Action] ${action} → ${cmd} 실행`);
|
|
res.json({ success: true, action });
|
|
} catch (err: any) {
|
|
output.appendLine(`[Action] 실패: ${err.message}`);
|
|
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() { }
|