Compare commits
6 Commits
67619e8950
...
e4b98af308
| Author | SHA1 | Date | |
|---|---|---|---|
| e4b98af308 | |||
| be6fae71de | |||
| f2ed431aa5 | |||
| 91b3a7ef20 | |||
| 150967deee | |||
| 952883d3d2 |
@@ -232,3 +232,37 @@
|
||||
| `git.antigravityCloneNonInteractive` | Clone non-interactive |
|
||||
| `git.antigravityGetRemoteUrl` | Get remote URL |
|
||||
| `git.antigravityReportCloneProgress` | Report clone progress |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Language Server ConnectRPC (Direct RPC)
|
||||
|
||||
> LS Go 바이너리에서 추출한 RPC 메서드. ConnectRPC(HTTPS) 프로토콜 사용.
|
||||
> 서비스: `exa.language_server_pb.LanguageServerService`
|
||||
> 상세: [ls-rpc-reference.md](ls-rpc-reference.md)
|
||||
|
||||
### 대화/Trajectory
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `GetUserTrajectoryDescriptions` | 모든 대화 목록 (googleAgentId, summary, lastStepIndex) |
|
||||
| `GetCascadeTrajectory` | 대화 전체 내용 |
|
||||
| `GetCascadeTrajectorySteps` | 대화 단계별 데이터 (startStepIndex 지원) |
|
||||
| `StartCascade` | 새 대화 생성 (headless) |
|
||||
| `SendUserCascadeMessage` | 기존 대화에 메시지 전송 |
|
||||
|
||||
### 실시간 스트림
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `StreamCascadeReactiveUpdates` | Cascade 실시간 업데이트 |
|
||||
| `StreamAgentStateUpdates` | 에이전트 상태 스트림 |
|
||||
|
||||
### 에이전트 제어
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `HandleCascadeUserInteraction` | 사용자 상호작용 (승인/거절) |
|
||||
| `AcknowledgeCascadeCodeEdit` | 코드 편집 승인 |
|
||||
| `ResolveOutstandingSteps` | 미해결 단계 처리 |
|
||||
|
||||
|
||||
@@ -14,3 +14,6 @@
|
||||
| 10 | 15:00 | @bridge→@gravity 이름 변경 + 슬래시 명령어 /stop /auto /send | `02e9e4d`~`0bd525a` | ✅ |
|
||||
| 11 | 16:00 | sendTextToChat 탐색 → sendPromptToAgentPanel 발견 | `e4eb756`~`8d5e59c` | ✅ |
|
||||
| 12 | 17:15 | 양방향 통신 완성 + 171개 명령어 문서화 + scanner 시작 수정 | `befa5d7` | ✅ |
|
||||
| 13 | 17:50 | getDiagnostics 구조 프로브 → 대화 텍스트 미포함 확인 | `952883d`~`150967d` | 🔧 |
|
||||
| 14 | 18:15 | LS ConnectRPC 브릿지 구현 + HTTPS 프로토콜 감지 수정 | `91b3a7e`~`f2ed431` | 🔧 |
|
||||
| 15 | 18:45 | Go 바이너리에서 100+ RPC 메서드 추출 → 정확한 메서드명 적용 | `be6fae7` | 🔧 |
|
||||
|
||||
38
docs/devlog/entries/20260307-003.md
Normal file
38
docs/devlog/entries/20260307-003.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# LS ConnectRPC 브릿지 역설계 + AI 응답 릴레이 구현
|
||||
|
||||
- **시간**: 2026-03-07 17:30~19:00
|
||||
- **Commit**: `952883d`~`be6fae7` (6 commits)
|
||||
- **Vikunja**: #223 → 진행중
|
||||
|
||||
## 핵심 발견
|
||||
|
||||
### AI 대화 텍스트는 brain/ 파일에 없다
|
||||
- brain/에는 artifact (task.md, walkthrough.md, implementation_plan.md)만 저장
|
||||
- .system_generated/logs/ 디렉토리도 없음
|
||||
- AI 응답 텍스트는 **LS 내부 메모리**에만 존재
|
||||
- getDiagnostics에는 시스템 로그와 대화 **메타데이터**(제목, step 수)만 포함
|
||||
|
||||
### LS ConnectRPC 접속
|
||||
- LS 바이너리: `language_server_windows_x64.exe` (160MB Go)
|
||||
- 프로토콜: ConnectRPC over HTTPS (자체 서명 인증서)
|
||||
- 인증: `x-codeium-csrf-token` 헤더 + `--csrf_token` CLI값
|
||||
- 포트: `--random_port` 사용 → netstat + PID 기반 탐색 필요
|
||||
|
||||
### Go 바이너리에서 100+ RPC 메서드 추출
|
||||
- Python regex로 `/exa.language_server_pb.LanguageServerService/` 패턴 추출
|
||||
- 핵심 메서드: `GetUserTrajectoryDescriptions`, `GetCascadeTrajectorySteps`, `StreamCascadeReactiveUpdates`
|
||||
- `GetTrajectoryDescriptions`는 존재하지 않음 → SDK 타입 정의가 부정확
|
||||
|
||||
## 실패한 접근법
|
||||
|
||||
1. `onDidChangeTextDocument` — WebView 기반 채팅은 TextDocument 이벤트 미발생
|
||||
2. `getManagerTrace` / `getWorkbenchTrace` — undefined 반환
|
||||
3. brain/ 파일 모니터링 — artifact만 존재, 대화 텍스트 없음
|
||||
4. getDiagnostics → 메타데이터만, 대화 내용 없음
|
||||
5. antigravity-sdk — d.ts만 있고 JS 구현 없는 vaporware
|
||||
|
||||
## 미완료
|
||||
|
||||
- [ ] LS ConnectRPC 호출이 실제로 대화 데이터 반환하는지 검증 필요
|
||||
- [ ] `GetCascadeTrajectorySteps` 응답 구조 파악 → AI 텍스트 추출 로직
|
||||
- [ ] `StreamCascadeReactiveUpdates`로 실시간 릴레이 가능성 탐색
|
||||
175
docs/ls-rpc-reference.md
Normal file
175
docs/ls-rpc-reference.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Antigravity Language Server RPC Reference
|
||||
|
||||
> Extracted from `language_server_windows_x64.exe` (160MB Go binary, v2026-03)
|
||||
> Protocol: ConnectRPC over HTTPS on localhost
|
||||
> Service: `exa.language_server_pb.LanguageServerService`
|
||||
|
||||
---
|
||||
|
||||
## 연결 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| **프로세스명** | `language_server_windows_x64.exe` |
|
||||
| **CSRF 헤더** | `x-codeium-csrf-token` |
|
||||
| **CSRF 값** | `--csrf_token` CLI 인자에서 추출 |
|
||||
| **프로토콜** | HTTPS (자체 서명 인증서) |
|
||||
| **포트** | `--random_port` → netstat에서 PID 기반 탐색 |
|
||||
| **Content-Type** | `application/json` |
|
||||
|
||||
### 포트 발견 절차
|
||||
|
||||
1. `Get-CimInstance Win32_Process` → LS PID + `--csrf_token` 추출
|
||||
2. `netstat -ano | findstr LISTENING | findstr <PID>` → 리스닝 포트 목록
|
||||
3. 각 포트에 `Heartbeat` 요청 시도 (HTTP → HTTPS fallback)
|
||||
4. 200 응답 포트 = ConnectRPC 포트
|
||||
|
||||
### 주의사항
|
||||
|
||||
- LS 포트 3개: LSP JSON-RPC, HTTP ConnectRPC, HTTPS ConnectRPC
|
||||
- `--extension_server_port`는 별도 (Extension Host IPC 전용)
|
||||
- `port 404 page not found` = 해당 경로에 서비스 없음 (Go HTTP mux)
|
||||
- HTTP 요청 → "Client sent an HTTP request to an HTTPS server." → HTTPS 사용
|
||||
|
||||
---
|
||||
|
||||
## 🎯 핵심 RPC 메서드 (채팅/대화)
|
||||
|
||||
| 메서드 | 용도 | 비고 |
|
||||
|--------|------|------|
|
||||
| `GetUserTrajectoryDescriptions` | 모든 대화 목록 조회 | googleAgentId, summary, lastStepIndex |
|
||||
| `GetCascadeTrajectory` | 대화 전체 내용 | 무거움 |
|
||||
| `GetCascadeTrajectorySteps` | 대화 단계 데이터 | startStepIndex로 범위 지정 가능 |
|
||||
| `StartCascade` | 새 대화 생성 | headless 지원 |
|
||||
| `SendUserCascadeMessage` | 기존 대화에 메시지 전송 | |
|
||||
| `AddCascadeInput` | Cascade에 입력 추가 | |
|
||||
| `HandleCascadeUserInteraction` | 사용자 상호작용 처리 | 승인/거절 등 |
|
||||
| `LoadTrajectory` | 대화 로드 | |
|
||||
| `LoadReplayConversation*` | 대화 리플레이 | |
|
||||
| `SmartFocusConversation` | UI에서 대화 포커스 | |
|
||||
|
||||
## 📡 스트리밍 RPC (실시간)
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `StreamCascadeReactiveUpdates` | Cascade 실시간 업데이트 스트림 |
|
||||
| `StreamCascadePanelReactiveUpdates*` | 패널 상태 실시간 스트림 |
|
||||
| `StreamCascadeSummariesReactiveUpdates` | 대화 요약 실시간 스트림 |
|
||||
| `StreamAgentStateUpdates` | 에이전트 상태 스트림 |
|
||||
| `StreamUserTrajectoryReactiveUpdates*` | 사용자 trajectory 실시간 스트림 |
|
||||
| `StreamTerminalShellCommand` | 터미널 명령어 스트림 |
|
||||
|
||||
## 🛠️ 에이전트 제어
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `ResolveOutstandingSteps` | 미해결 단계 처리 |
|
||||
| `AcknowledgeCascadeCodeEdit` | 코드 편집 승인 |
|
||||
| `RevertToCascadeStep` | 특정 단계로 롤백 |
|
||||
| `GetCascadeTrajectoryGeneratorMetadata` | 생성기 메타데이터 |
|
||||
| `RecordUserStepSnapshot` | 사용자 단계 스냅샷 기록 |
|
||||
| `SendActionToChatPanel` | 채팅 패널에 액션 전송 |
|
||||
|
||||
## 📋 설정/상태
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `Heartbeat` | 연결 검증 |
|
||||
| `GetStatus` | LS 상태 |
|
||||
| `GetUserStatus` | 사용자 상태 (크레딧 등) |
|
||||
| `GetUserSettings` | 사용자 설정 |
|
||||
| `SetUserSettings` | 사용자 설정 변경 |
|
||||
| `GetWorkingDirectories` | 작업 디렉토리 |
|
||||
| `SetWorkingDirectories` | 작업 디렉토리 설정 |
|
||||
| `GetDebugDiagnostics` | 디버그 진단 |
|
||||
| `GetModelStatuses` | 모델 상태 |
|
||||
| `GetCascadeModelConfigs` | Cascade 모델 설정 |
|
||||
|
||||
## 🧩 MCP / 플러그인
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `GetMcpServerStates` | MCP 서버 상태 |
|
||||
| `RefreshMcpServers` | MCP 서버 새로고침 |
|
||||
| `ListMcpResources` | MCP 리소스 목록 |
|
||||
| `ListMcpPrompts` | MCP 프롬프트 목록 |
|
||||
| `GetAvailableCascadePlugins` | 사용 가능한 플러그인 |
|
||||
| `InstallCascadePlugin` | 플러그인 설치 |
|
||||
| `GetCascadePluginById` | 플러그인 상세 |
|
||||
|
||||
## 📊 분석/기록
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `RecordAnalyticsEvent` | 분석 이벤트 기록 |
|
||||
| `RecordChatFeedback` | 채팅 피드백 기록 |
|
||||
| `RecordEvent` | 일반 이벤트 기록 |
|
||||
| `GetUserAnalyticsSummary` | 사용자 분석 요약 |
|
||||
|
||||
## 🗂️ Artifact / Memory
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `GetArtifactSnapshots` | 아티팩트 스냅샷 |
|
||||
| `GetRevisionArtifact` | 특정 리비전 아티팩트 |
|
||||
| `SaveMediaAsArtifact` | 미디어 → 아티팩트 저장 |
|
||||
| `GetCascadeMemories` | Cascade 메모리 |
|
||||
| `UpdateCascadeMemory` | Cascade 메모리 업데이트 |
|
||||
| `GetUserMemories` | 사용자 메모리 |
|
||||
| `UpdateConversationAnnotations` | 대화 주석(태그,제목) 업데이트 |
|
||||
|
||||
## 🔧 기타
|
||||
|
||||
| 메서드 | 용도 |
|
||||
|--------|------|
|
||||
| `GetRepoInfos` | 저장소 정보 |
|
||||
| `GetWorkspaceInfos` | 워크스페이스 정보 |
|
||||
| `GetAllRules` | 모든 규칙 |
|
||||
| `GetAllSkills` | 모든 스킬 |
|
||||
| `GetAllWorkflows` | 모든 워크플로우 |
|
||||
| `GetAllCustomAgentConfigs` | 커스텀 에이전트 설정 |
|
||||
| `GetBrowserOpenConversation` | 브라우저에 열린 대화 |
|
||||
| `SkipBrowserSubagent` | 브라우저 서브에이전트 스킵 |
|
||||
|
||||
---
|
||||
|
||||
## getDiagnostics 구조 (VS Code 명령)
|
||||
|
||||
`antigravity.getDiagnostics` → 176KB JSON string:
|
||||
|
||||
| 키 | 타입 | 내용 |
|
||||
|-----|------|------|
|
||||
| `isRemote` | boolean | 원격 여부 |
|
||||
| `systemInfo` | object | OS, 타임스탬프, 이메일, 사용자명 |
|
||||
| `extensionLogs` | Array(426) | Extension 로그 문자열 배열 |
|
||||
| `rendererLogs` | object | 렌더러별 로그 (artifacts, interactive-editor) |
|
||||
| `mainThreadLogs` | object | 메인 스레드 로그 (auth, cloudcode) |
|
||||
| `languageServerLogs` | object | LS 디버그 로그 |
|
||||
| `recentTrajectories` | Array(10) | 최근 대화 메타데이터 |
|
||||
|
||||
### recentTrajectories 항목 구조
|
||||
|
||||
```json
|
||||
{
|
||||
"googleAgentId": "uuid",
|
||||
"trajectoryId": "uuid",
|
||||
"summary": "대화 제목",
|
||||
"lastStepIndex": 56,
|
||||
"lastModifiedTime": "2026-03-07T05:53:11.1492Z"
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ `recentTrajectories`는 **대화 텍스트를 포함하지 않음** — 메타데이터만.
|
||||
> AI 응답 텍스트를 얻으려면 LS ConnectRPC의 `GetCascadeTrajectorySteps` 필요.
|
||||
|
||||
---
|
||||
|
||||
## 실패한 접근법 (참고)
|
||||
|
||||
| 접근 | 결과 | 이유 |
|
||||
|------|------|------|
|
||||
| `onDidChangeTextDocument` | ❌ | Agent 패널은 WebView, TextDocument 이벤트 미발생 |
|
||||
| `antigravity.getManagerTrace` | ❌ undefined | 가용 데이터 없음 |
|
||||
| `antigravity.getWorkbenchTrace` | ❌ undefined | 가용 데이터 없음 |
|
||||
| `brain/` 파일 모니터링 | △ partial | task.md/walkthrough.md만 — AI 응답 텍스트 없음 |
|
||||
| `getDiagnostics.agentWindowConsoleLogs` | ❌ 없음 | 시스템 로그뿐 |
|
||||
Binary file not shown.
@@ -118,6 +118,265 @@ function activate(context) {
|
||||
context.subscriptions.push(statusBar);
|
||||
// Register commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('gravityBridge.start', startBridge), vscode.commands.registerCommand('gravityBridge.stop', stopBridge), vscode.commands.registerCommand('gravityBridge.connect', connectSession), vscode.commands.registerCommand('gravityBridge.approve', () => handleManualAction(true)), vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)));
|
||||
// === LS ConnectRPC Bridge: Relay AI responses to Discord ===
|
||||
let lsPort = null;
|
||||
let lsCsrf = '';
|
||||
let lsPid = null;
|
||||
let lsUseTls = false; // track detected protocol
|
||||
let lastStepIndex = {}; // cascadeId → last known step index
|
||||
async function discoverLS() {
|
||||
return new Promise((resolve) => {
|
||||
// Phase 1: Find LS process → PID + CSRF token
|
||||
cp.exec('powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object {$_.Name -eq \'language_server_exe.exe\' -or ($_.CommandLine -and $_.CommandLine -like \'*language_server*\' -and $_.CommandLine -notlike \'*powershell*\')} | Select-Object ProcessId, CommandLine | ConvertTo-Json"', { maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
console.log(`Gravity Bridge: [LS] process not found`);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let procs = JSON.parse(stdout.trim());
|
||||
if (!Array.isArray(procs)) {
|
||||
procs = [procs];
|
||||
}
|
||||
// Find a process with csrf_token in command line
|
||||
for (const proc of procs) {
|
||||
const cmd = proc.CommandLine || '';
|
||||
const csrfM = cmd.match(/--csrf_token[= ]([^\s"]+)/);
|
||||
if (csrfM) {
|
||||
lsPid = proc.ProcessId;
|
||||
lsCsrf = csrfM[1];
|
||||
console.log(`Gravity Bridge: [LS] PID=${lsPid}, CSRF=${lsCsrf.substring(0, 12)}...`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Gravity Bridge: [LS] parse error: ${e}`);
|
||||
}
|
||||
if (!lsPid || !lsCsrf) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
// Phase 2: netstat → find LS listening ports
|
||||
cp.exec(`netstat -ano | findstr "LISTENING" | findstr " ${lsPid}"`, { maxBuffer: 512 * 1024 }, async (err2, stdout2) => {
|
||||
if (err2 || !stdout2.trim()) {
|
||||
console.log(`Gravity Bridge: [LS] no listening ports found for PID ${lsPid}`);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
// Parse ports
|
||||
const ports = [];
|
||||
for (const line of stdout2.split('\n')) {
|
||||
const m = line.match(/:(\d+)\s+.*LISTENING/);
|
||||
if (m) {
|
||||
ports.push(parseInt(m[1]));
|
||||
}
|
||||
}
|
||||
const uniquePorts = [...new Set(ports)].sort((a, b) => a - b);
|
||||
console.log(`Gravity Bridge: [LS] ports for PID ${lsPid}: ${uniquePorts.join(', ')}`);
|
||||
// Try ConnectRPC probe on each port (HTTP first, then HTTPS)
|
||||
for (const port of uniquePorts) {
|
||||
const ok = await probeLSPort(port);
|
||||
if (ok) {
|
||||
lsPort = port;
|
||||
console.log(`Gravity Bridge: [LS] ✅ ConnectRPC active on port ${port}`);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`Gravity Bridge: [LS] no ConnectRPC port responded`);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function probeLSPort(port) {
|
||||
return new Promise((resolve) => {
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
// Try HTTP first (extension_server uses HTTP)
|
||||
const tryProto = (proto, useTls) => {
|
||||
const req = proto.request({
|
||||
hostname: '127.0.0.1',
|
||||
port,
|
||||
path: '/exa.language_server_pb.LanguageServerService/Heartbeat',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-codeium-csrf-token': lsCsrf,
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
timeout: 2000,
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
console.log(`Gravity Bridge: [LS] port ${port} (${useTls ? 'https' : 'http'}) status=${res.statusCode} body=${data.substring(0, 200)}`);
|
||||
// If HTTP got "HTTPS server" response, retry with HTTPS
|
||||
if (!useTls && data.includes('HTTPS server')) {
|
||||
tryProto(https, true);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode !== 404) {
|
||||
lsUseTls = useTls; // remember which protocol worked
|
||||
}
|
||||
resolve(res.statusCode !== 404);
|
||||
});
|
||||
});
|
||||
req.on('error', () => {
|
||||
if (!useTls) {
|
||||
// Try HTTPS
|
||||
tryProto(https, true);
|
||||
}
|
||||
else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||||
req.write('{}');
|
||||
req.end();
|
||||
};
|
||||
tryProto(http, false);
|
||||
});
|
||||
}
|
||||
async function lsRPC(method, payload = {}) {
|
||||
if (!lsPort || !lsCsrf) {
|
||||
return null;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const proto = lsUseTls ? https : http; // use detected protocol
|
||||
const body = JSON.stringify(payload);
|
||||
const req = proto.request({
|
||||
hostname: '127.0.0.1',
|
||||
port: lsPort,
|
||||
path: `/exa.language_server_pb.LanguageServerService/${method}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-codeium-csrf-token': lsCsrf,
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
timeout: 5000,
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
}
|
||||
catch {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (e) => {
|
||||
console.log(`Gravity Bridge: [LS RPC] ${method} error: ${e.message}`);
|
||||
resolve(null);
|
||||
});
|
||||
req.on('timeout', () => { req.destroy(); resolve(null); });
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
async function pollConversations() {
|
||||
if (!lsPort) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Get trajectory descriptions — lightweight list of all conversations
|
||||
const result = await lsRPC('GetUserTrajectoryDescriptions');
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
// Debug: log structure on first call
|
||||
if (Object.keys(lastStepIndex).length === 0) {
|
||||
console.log(`Gravity Bridge: [LS] trajectories response keys: ${typeof result === 'object' ? Object.keys(result).join(', ') : typeof result}`);
|
||||
console.log(`Gravity Bridge: [LS] trajectories sample: ${JSON.stringify(result).substring(0, 600)}`);
|
||||
}
|
||||
const trajectories = result.trajectories || result.trajectory_descriptions || result.userTrajectoryDescriptions || result.user_trajectory_descriptions || [];
|
||||
if (!Array.isArray(trajectories)) {
|
||||
return;
|
||||
}
|
||||
for (const traj of trajectories) {
|
||||
const id = traj.googleAgentId || traj.google_agent_id || traj.id || '';
|
||||
const stepIdx = traj.lastStepIndex ?? traj.last_step_index ?? traj.step_count ?? 0;
|
||||
const prev = lastStepIndex[id];
|
||||
if (prev !== undefined && stepIdx > prev) {
|
||||
// New steps! Fetch full trajectory to get AI response
|
||||
console.log(`Gravity Bridge: [LS] ${id.substring(0, 8)} new steps: ${prev} → ${stepIdx}`);
|
||||
const full = await lsRPC('GetCascadeTrajectorySteps', {
|
||||
googleAgentId: id,
|
||||
trajectoryId: traj.trajectoryId || traj.trajectory_id || '',
|
||||
startStepIndex: prev,
|
||||
});
|
||||
// Also log structure on first fetch
|
||||
if (full) {
|
||||
console.log(`Gravity Bridge: [LS] steps response keys: ${typeof full === 'object' ? Object.keys(full).join(', ') : typeof full}`);
|
||||
console.log(`Gravity Bridge: [LS] steps sample: ${JSON.stringify(full).substring(0, 600)}`);
|
||||
}
|
||||
if (full) {
|
||||
extractAndRelay(full, prev, stepIdx);
|
||||
}
|
||||
}
|
||||
lastStepIndex[id] = stepIdx;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Gravity Bridge: [LS poll] error: ${e}`);
|
||||
}
|
||||
}
|
||||
function extractAndRelay(trajectory, fromStep, toStep) {
|
||||
// Extract PlannerResponse or assistant messages from trajectory steps
|
||||
const steps = trajectory.steps || trajectory.cortex_steps || [];
|
||||
const messages = [];
|
||||
for (const step of steps) {
|
||||
const idx = step.index ?? step.step_index ?? 0;
|
||||
if (idx <= fromStep) {
|
||||
continue;
|
||||
}
|
||||
const type = step.type || step.step_type || '';
|
||||
const content = step.content || step.summary || step.text || '';
|
||||
// PlannerResponse = AI's text output to user
|
||||
if ((type === 'PlannerResponse' || type === 'planner_response') && content) {
|
||||
messages.push(content);
|
||||
}
|
||||
// Also capture user-facing messages
|
||||
if (step.data?.content && typeof step.data.content === 'string') {
|
||||
messages.push(step.data.content);
|
||||
}
|
||||
}
|
||||
// Fallback: if no detailed steps, try messages array
|
||||
if (messages.length === 0) {
|
||||
const msgs = trajectory.messages || trajectory.chat_messages || [];
|
||||
for (const msg of msgs) {
|
||||
if (msg.role === 'assistant' && msg.content) {
|
||||
messages.push(msg.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (messages.length > 0) {
|
||||
const combined = messages.join('\n\n---\n\n');
|
||||
writeChatSnapshot(combined);
|
||||
console.log(`Gravity Bridge: [LS] relayed ${messages.length} response(s) to Discord`);
|
||||
}
|
||||
}
|
||||
// Start LS bridge after a delay
|
||||
setTimeout(async () => {
|
||||
const found = await discoverLS();
|
||||
if (found) {
|
||||
console.log(`Gravity Bridge: [LS] bridge active — polling every 5s`);
|
||||
// Initialize step counts
|
||||
await pollConversations();
|
||||
// Start polling loop
|
||||
setInterval(pollConversations, 5000);
|
||||
}
|
||||
else {
|
||||
console.log(`Gravity Bridge: [LS] bridge NOT available — AI responses won't relay`);
|
||||
}
|
||||
}, 8000);
|
||||
// Chat document change listener — captures AI text responses
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((event) => {
|
||||
handleChatDocumentChange(event);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -97,6 +97,272 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)),
|
||||
);
|
||||
|
||||
// === LS ConnectRPC Bridge: Relay AI responses to Discord ===
|
||||
let lsPort: number | null = null;
|
||||
let lsCsrf: string = '';
|
||||
let lsPid: number | null = null;
|
||||
let lsUseTls: boolean = false; // track detected protocol
|
||||
let lastStepIndex: Record<string, number> = {}; // cascadeId → last known step index
|
||||
|
||||
async function discoverLS(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// Phase 1: Find LS process → PID + CSRF token
|
||||
cp.exec(
|
||||
'powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object {$_.Name -eq \'language_server_exe.exe\' -or ($_.CommandLine -and $_.CommandLine -like \'*language_server*\' -and $_.CommandLine -notlike \'*powershell*\')} | Select-Object ProcessId, CommandLine | ConvertTo-Json"',
|
||||
{ maxBuffer: 2 * 1024 * 1024 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
console.log(`Gravity Bridge: [LS] process not found`);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let procs = JSON.parse(stdout.trim());
|
||||
if (!Array.isArray(procs)) { procs = [procs]; }
|
||||
// Find a process with csrf_token in command line
|
||||
for (const proc of procs) {
|
||||
const cmd = proc.CommandLine || '';
|
||||
const csrfM = cmd.match(/--csrf_token[= ]([^\s"]+)/);
|
||||
if (csrfM) {
|
||||
lsPid = proc.ProcessId;
|
||||
lsCsrf = csrfM[1];
|
||||
console.log(`Gravity Bridge: [LS] PID=${lsPid}, CSRF=${lsCsrf.substring(0, 12)}...`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Gravity Bridge: [LS] parse error: ${e}`);
|
||||
}
|
||||
|
||||
if (!lsPid || !lsCsrf) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 2: netstat → find LS listening ports
|
||||
cp.exec(
|
||||
`netstat -ano | findstr "LISTENING" | findstr " ${lsPid}"`,
|
||||
{ maxBuffer: 512 * 1024 },
|
||||
async (err2, stdout2) => {
|
||||
if (err2 || !stdout2.trim()) {
|
||||
console.log(`Gravity Bridge: [LS] no listening ports found for PID ${lsPid}`);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
// Parse ports
|
||||
const ports: number[] = [];
|
||||
for (const line of stdout2.split('\n')) {
|
||||
const m = line.match(/:(\d+)\s+.*LISTENING/);
|
||||
if (m) { ports.push(parseInt(m[1])); }
|
||||
}
|
||||
const uniquePorts = [...new Set(ports)].sort((a, b) => a - b);
|
||||
console.log(`Gravity Bridge: [LS] ports for PID ${lsPid}: ${uniquePorts.join(', ')}`);
|
||||
|
||||
// Try ConnectRPC probe on each port (HTTP first, then HTTPS)
|
||||
for (const port of uniquePorts) {
|
||||
const ok = await probeLSPort(port);
|
||||
if (ok) {
|
||||
lsPort = port;
|
||||
console.log(`Gravity Bridge: [LS] ✅ ConnectRPC active on port ${port}`);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`Gravity Bridge: [LS] no ConnectRPC port responded`);
|
||||
resolve(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function probeLSPort(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
// Try HTTP first (extension_server uses HTTP)
|
||||
const tryProto = (proto: any, useTls: boolean) => {
|
||||
const req = proto.request({
|
||||
hostname: '127.0.0.1',
|
||||
port,
|
||||
path: '/exa.language_server_pb.LanguageServerService/Heartbeat',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-codeium-csrf-token': lsCsrf,
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
timeout: 2000,
|
||||
}, (res: any) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: any) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
console.log(`Gravity Bridge: [LS] port ${port} (${useTls ? 'https' : 'http'}) status=${res.statusCode} body=${data.substring(0, 200)}`);
|
||||
// If HTTP got "HTTPS server" response, retry with HTTPS
|
||||
if (!useTls && data.includes('HTTPS server')) {
|
||||
tryProto(https, true);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode !== 404) {
|
||||
lsUseTls = useTls; // remember which protocol worked
|
||||
}
|
||||
resolve(res.statusCode !== 404);
|
||||
});
|
||||
});
|
||||
req.on('error', () => {
|
||||
if (!useTls) {
|
||||
// Try HTTPS
|
||||
tryProto(https, true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||||
req.write('{}');
|
||||
req.end();
|
||||
};
|
||||
tryProto(http, false);
|
||||
});
|
||||
}
|
||||
|
||||
async function lsRPC(method: string, payload: any = {}): Promise<any> {
|
||||
if (!lsPort || !lsCsrf) { return null; }
|
||||
return new Promise((resolve) => {
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const proto = lsUseTls ? https : http; // use detected protocol
|
||||
const body = JSON.stringify(payload);
|
||||
const req = proto.request({
|
||||
hostname: '127.0.0.1',
|
||||
port: lsPort,
|
||||
path: `/exa.language_server_pb.LanguageServerService/${method}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-codeium-csrf-token': lsCsrf,
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
timeout: 5000,
|
||||
}, (res: any) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: any) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||
});
|
||||
});
|
||||
req.on('error', (e: any) => {
|
||||
console.log(`Gravity Bridge: [LS RPC] ${method} error: ${e.message}`);
|
||||
resolve(null);
|
||||
});
|
||||
req.on('timeout', () => { req.destroy(); resolve(null); });
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function pollConversations() {
|
||||
if (!lsPort) { return; }
|
||||
try {
|
||||
// Get trajectory descriptions — lightweight list of all conversations
|
||||
const result = await lsRPC('GetUserTrajectoryDescriptions');
|
||||
if (!result) { return; }
|
||||
|
||||
// Debug: log structure on first call
|
||||
if (Object.keys(lastStepIndex).length === 0) {
|
||||
console.log(`Gravity Bridge: [LS] trajectories response keys: ${typeof result === 'object' ? Object.keys(result).join(', ') : typeof result}`);
|
||||
console.log(`Gravity Bridge: [LS] trajectories sample: ${JSON.stringify(result).substring(0, 600)}`);
|
||||
}
|
||||
|
||||
const trajectories = result.trajectories || result.trajectory_descriptions || result.userTrajectoryDescriptions || result.user_trajectory_descriptions || [];
|
||||
if (!Array.isArray(trajectories)) { return; }
|
||||
|
||||
for (const traj of trajectories) {
|
||||
const id = traj.googleAgentId || traj.google_agent_id || traj.id || '';
|
||||
const stepIdx = traj.lastStepIndex ?? traj.last_step_index ?? traj.step_count ?? 0;
|
||||
const prev = lastStepIndex[id];
|
||||
|
||||
if (prev !== undefined && stepIdx > prev) {
|
||||
// New steps! Fetch full trajectory to get AI response
|
||||
console.log(`Gravity Bridge: [LS] ${id.substring(0, 8)} new steps: ${prev} → ${stepIdx}`);
|
||||
const full = await lsRPC('GetCascadeTrajectorySteps', {
|
||||
googleAgentId: id,
|
||||
trajectoryId: traj.trajectoryId || traj.trajectory_id || '',
|
||||
startStepIndex: prev,
|
||||
});
|
||||
// Also log structure on first fetch
|
||||
if (full) {
|
||||
console.log(`Gravity Bridge: [LS] steps response keys: ${typeof full === 'object' ? Object.keys(full).join(', ') : typeof full}`);
|
||||
console.log(`Gravity Bridge: [LS] steps sample: ${JSON.stringify(full).substring(0, 600)}`);
|
||||
}
|
||||
|
||||
if (full) {
|
||||
extractAndRelay(full, prev, stepIdx);
|
||||
}
|
||||
}
|
||||
lastStepIndex[id] = stepIdx;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Gravity Bridge: [LS poll] error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
function extractAndRelay(trajectory: any, fromStep: number, toStep: number) {
|
||||
// Extract PlannerResponse or assistant messages from trajectory steps
|
||||
const steps = trajectory.steps || trajectory.cortex_steps || [];
|
||||
const messages: string[] = [];
|
||||
|
||||
for (const step of steps) {
|
||||
const idx = step.index ?? step.step_index ?? 0;
|
||||
if (idx <= fromStep) { continue; }
|
||||
|
||||
const type = step.type || step.step_type || '';
|
||||
const content = step.content || step.summary || step.text || '';
|
||||
|
||||
// PlannerResponse = AI's text output to user
|
||||
if ((type === 'PlannerResponse' || type === 'planner_response') && content) {
|
||||
messages.push(content);
|
||||
}
|
||||
// Also capture user-facing messages
|
||||
if (step.data?.content && typeof step.data.content === 'string') {
|
||||
messages.push(step.data.content);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no detailed steps, try messages array
|
||||
if (messages.length === 0) {
|
||||
const msgs = trajectory.messages || trajectory.chat_messages || [];
|
||||
for (const msg of msgs) {
|
||||
if (msg.role === 'assistant' && msg.content) {
|
||||
messages.push(msg.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
const combined = messages.join('\n\n---\n\n');
|
||||
writeChatSnapshot(combined);
|
||||
console.log(`Gravity Bridge: [LS] relayed ${messages.length} response(s) to Discord`);
|
||||
}
|
||||
}
|
||||
|
||||
// Start LS bridge after a delay
|
||||
setTimeout(async () => {
|
||||
const found = await discoverLS();
|
||||
if (found) {
|
||||
console.log(`Gravity Bridge: [LS] bridge active — polling every 5s`);
|
||||
// Initialize step counts
|
||||
await pollConversations();
|
||||
// Start polling loop
|
||||
setInterval(pollConversations, 5000);
|
||||
} else {
|
||||
console.log(`Gravity Bridge: [LS] bridge NOT available — AI responses won't relay`);
|
||||
}
|
||||
}, 8000);
|
||||
|
||||
|
||||
// Chat document change listener — captures AI text responses
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||
|
||||
Reference in New Issue
Block a user