/** * Gravity Bridge — VS Code Extension (SDK Edition) * * Uses antigravity-sdk for: * - Real-time step/conversation monitoring via EventMonitor * - Full conversation content via LSBridge.getConversation() * - Message sending via CascadeManager.sendPrompt() * - Accept/Reject via CascadeManager.acceptStep()/rejectStep() * * Communication with Discord via file-based bridge protocol. */ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as cp from 'child_process'; import * as crypto from 'crypto'; // ─── File-based logging (AI can read directly) ─── function logToFile(msg: string) { const ts = new Date().toISOString().replace('T', ' ').substring(0, 19); const line = `${ts} ${msg}`; console.log(`Gravity Bridge: ${msg}`); try { if (!bridgePath) return; const logFile = path.join(bridgePath, 'extension.log'); fs.appendFileSync(logFile, line + '\n', 'utf-8'); } catch (e: any) { console.error(`Gravity Bridge LOG WRITE FAIL: ${e.message}`); } } // antigravity-sdk embedded locally (src/sdk/) let AntigravitySDK: any; let sdk: any; let statusBar: vscode.StatusBarItem; let bridgePath: string; let projectName: string; let isActive = false; let deterministicPort = 0; // derived from projectName, consistent across restarts let watcher: fs.FSWatcher | null = null; let commandsWatcher: fs.FSWatcher | null = null; const sentPendingIds = new Set(); // ─── Project Detection ─── function detectProjectName(): string { const config = vscode.workspace.getConfiguration('gravityBridge'); const configName = config.get('projectName'); if (configName) { return configName; } const folders = vscode.workspace.workspaceFolders; if (folders && folders.length > 0) { const cwd = folders[0].uri.fsPath; try { const remoteUrl = cp.execSync('git remote get-url origin', { cwd, encoding: 'utf-8', timeout: 3000 }).trim(); const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/); if (match && match[1]) { return match[1].toLowerCase().replace(/[\s\-]+/g, '_'); } } catch { } return path.basename(cwd).toLowerCase().replace(/[\s\-]+/g, '_'); } return 'default'; } // ─── Bridge File I/O ─── function ensureBridgeDir() { const dirs = ['', 'response', 'commands', 'chat_snapshots']; for (const d of dirs) { const p = path.join(bridgePath, d); if (!fs.existsSync(p)) { fs.mkdirSync(p, { recursive: true }); } } } // Module-level activeSessionId so writeChatSnapshot can register sessions lazily let activeSessionId = ''; function writeChatSnapshot(text: string) { try { // Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up const snapshotDir = path.join(bridgePath, 'chat_snapshots'); if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); } const id = Date.now().toString(); const data = { id, project_name: projectName, content: text, timestamp: Date.now() / 1000, }; const filePath = path.join(snapshotDir, `${id}.json`); fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`); // Lazily register session → project mapping (correct because projectName is per-window) if (activeSessionId) { writeRegistration(activeSessionId); } } catch (e: any) { console.log(`Gravity Bridge: snapshot write error: ${e.message}`); } } // ─── Command File Watcher (Discord → Antigravity) ─── function processCommandFile(filePath: string) { try { const content = fs.readFileSync(filePath, 'utf-8'); const cmd = JSON.parse(content); // Skip already consumed commands if (cmd.consumed) { try { fs.unlinkSync(filePath); } catch { } return; } // Ignore commands for other projects if (cmd.project_name && cmd.project_name !== projectName) { console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${projectName}")`); return; } // Bot writes 'text' field, not 'message' const text = cmd.text || cmd.message || ''; const action = cmd.action || ''; console.log(`Gravity Bridge: command — text="${text}" action="${action}"`); if (action === 'approve' && sdk) { sdk.cascade.acceptStep().catch((e: any) => console.log(`Gravity Bridge: approve error: ${e.message}`) ); } else if (action === 'reject' && sdk) { sdk.cascade.rejectStep().catch((e: any) => console.log(`Gravity Bridge: reject error: ${e.message}`) ); } else if (action === 'approve_terminal' && sdk) { sdk.cascade.acceptTerminalCommand().catch((e: any) => console.log(`Gravity Bridge: approve_terminal error: ${e.message}`) ); } else if (text === '!stop') { // Cancel current operation vscode.commands.executeCommand('antigravity.agent.rejectAgentStep') .then(() => console.log('Gravity Bridge: ✅ stop sent'), () => { }); } else if (text.startsWith('!auto ')) { // Auto-approve mode toggle const mode = text.includes('on') ? 'true' : 'false'; console.log(`Gravity Bridge: auto-approve → ${mode}`); } else if (text) { // Send message to Antigravity — use VS Code command (most reliable) vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text) .then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`), (e: any) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`)); } // Remove processed command file try { fs.unlinkSync(filePath); } catch { } } catch (e: any) { console.log(`Gravity Bridge: command processing error: ${e.message}`); } } function watchCommandsDir() { const cmdDir = path.join(bridgePath, 'commands'); // Process existing files try { for (const f of fs.readdirSync(cmdDir)) { if (f.endsWith('.json')) { processCommandFile(path.join(cmdDir, f)); } } } catch { } // Watch for new files try { commandsWatcher = fs.watch(cmdDir, (event, filename) => { if (filename && filename.endsWith('.json') && event === 'rename') { const fp = path.join(cmdDir, filename); if (fs.existsSync(fp)) { setTimeout(() => processCommandFile(fp), 200); } } }); } catch { } } // ─── SDK Integration ─── async function initSDK(context: vscode.ExtensionContext): Promise { try { const sdkModule = require('./sdk/index'); AntigravitySDK = sdkModule.AntigravitySDK; } catch (err: any) { console.log(`Gravity Bridge: antigravity-sdk load failed: ${err.message}`); return false; } try { sdk = new AntigravitySDK(context); await sdk.initialize(); console.log('Gravity Bridge: ✅ SDK initialized'); return true; } catch (err: any) { console.log(`Gravity Bridge: SDK init failed: ${err.message}`); return false; } } // ─── Approval Observer via SDK IntegrationManager ─── async function setupApprovalObserver() { if (!sdk) { logToFile('[OBSERVER] no SDK'); return; } try { const integration = sdk.integration; if (!integration) { logToFile('[OBSERVER] sdk.integration unavailable'); return; } // 1. Start HTTP bridge server in Extension Host const bridgePort = await startObserverHttpBridge(); if (!bridgePort) { logToFile('[OBSERVER] HTTP bridge failed'); return; } // 2. Register a TOP_BAR button so build() works try { integration.register({ id: 'gravity_bridge_status', point: 'topBar', icon: '🌉', tooltip: 'Gravity Bridge Active', }); } catch { /* already registered */ } // 3. Write renderer script with HTTP fetch() approach const observerJS = generateApprovalObserverScript(bridgePort); const patcher = (integration as any)._patcher; if (patcher && typeof patcher.getScriptPath === 'function') { let baseScript = ''; try { baseScript = integration.build(); } catch { baseScript = ''; } const combinedScript = baseScript + '\n' + observerJS; const scriptPath = patcher.getScriptPath(); fs.writeFileSync(scriptPath, combinedScript, 'utf8'); logToFile(`[OBSERVER] script written → ${scriptPath} (port=${bridgePort})`); if (!integration.isInstalled()) { patcher.install(combinedScript); logToFile('[OBSERVER] workbench.html patched (needs reload)'); } // Also patch workbench-jetski-agent.html (Antigravity's actual entry point!) // IMPORTANT: vscode-file:// does NOT serve custom .js files (silent 404), // so we MUST inline the script directly into the HTML. const scriptDir = path.dirname(scriptPath); const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html'); try { if (fs.existsSync(jetskiHtml)) { let html = fs.readFileSync(jetskiHtml, 'utf8'); // Remove old external script tag if present const extMarkerStart = ''; const extMarkerEnd = ''; if (html.includes(extMarkerStart)) { const extRe = new RegExp( '\\n?' + extMarkerStart.replace(/[[\]]/g, '\\$&') + '[\\s\\S]*?' + extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?' ); html = html.replace(extRe, ''); logToFile('[OBSERVER] removed external script tag from jetski HTML'); } // Insert or update inline script const inlineMarkerStart = ''; const inlineMarkerEnd = ''; if (html.includes(inlineMarkerStart)) { const re = new RegExp( inlineMarkerStart.replace(/[[\]]/g, '\\$&') + '[\\s\\S]*?' + inlineMarkerEnd.replace(/[[\]]/g, '\\$&') ); html = html.replace(re, `${inlineMarkerStart}\n\n${inlineMarkerEnd}`); logToFile('[OBSERVER] jetski HTML inline script UPDATED'); } else { html = html.replace('', `\n${inlineMarkerStart}\n\n${inlineMarkerEnd}\n`); logToFile('[OBSERVER] jetski HTML inline script INSERTED'); } fs.writeFileSync(jetskiHtml, html, 'utf8'); } } catch (e: any) { logToFile(`[OBSERVER] jetski patch error: ${e.message}`); } } // 4. Update product.json checksums so vscode-file:// serves our patched files updateProductChecksums(); try { integration.enableAutoRepair(); } catch { } setInterval(() => { try { integration.signalActive(); } catch { } }, 30_000); logToFile(`[OBSERVER] setup complete (HTTP bridge on port ${bridgePort})`); console.log(`Gravity Bridge: ✅ Approval observer installed (port ${bridgePort})`); } catch (err: any) { logToFile(`[OBSERVER] setup error: ${err.message}`); } } // ─── Product.json Checksum Auto-Update ─── // vscode-file:// protocol validates SHA256 checksums in product.json. // If a file's checksum doesn't match, Electron serves the ORIGINAL cached version. // This function recalculates checksums for files we modify (HTML files with