"use strict"; /** * 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. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.activate = activate; exports.deactivate = deactivate; const vscode = __importStar(require("vscode")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const cp = __importStar(require("child_process")); const crypto = __importStar(require("crypto")); // ─── File-based logging (AI can read directly) ─── function logToFile(msg) { const ts = new Date().toISOString().replace('T', ' ').substring(0, 19); // Include projectName prefix so shared log can distinguish which extension instance logged const prefix = projectName ? `[${projectName}]` : ''; const line = `${ts} ${prefix} ${msg}`; console.log(`Gravity Bridge: ${prefix} ${msg}`); try { if (!bridgePath) return; const logFile = path.join(bridgePath, 'extension.log'); fs.appendFileSync(logFile, line + '\n', 'utf-8'); } catch (e) { console.error(`Gravity Bridge LOG WRITE FAIL: ${e.message}`); } } // antigravity-sdk embedded locally (src/sdk/) let AntigravitySDK; let sdk; let statusBar; let bridgePath; let projectName; let workspaceUri = ''; // filesystem path of the workspace folder (for session filtering) let isActive = false; let autoApproveEnabled = false; // toggled via !auto from Discord let deterministicPort = 0; // derived from projectName, consistent across restarts let watcher = null; let commandsWatcher = null; const sentPendingIds = new Set(); // ─── Project Detection ─── function detectProjectName() { 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 = ''; let activeTrajectoryId = ''; // Track recently sent Discord→AG texts to avoid echo relay const recentDiscordSentTexts = new Map(); function writeChatSnapshot(text) { 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`); logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars)`); logToFile(`[SNAPSHOT] content: ${text.substring(0, 200)}`); // Lazily register session → project mapping (correct because projectName is per-window) if (activeSessionId) { writeRegistration(activeSessionId); } } catch (e) { console.log(`Gravity Bridge: snapshot write error: ${e.message}`); } } function writeChatSnapshotWithFiles(text, files) { try { 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, attached_files: files, timestamp: Date.now() / 1000, }; const filePath = path.join(snapshotDir, `${id}.json`); fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars, ${files.length} files)`); if (activeSessionId) { writeRegistration(activeSessionId); } } catch (e) { console.log(`Gravity Bridge: snapshot+files write error: ${e.message}`); } } // ─── Command File Watcher (Discord → Antigravity) ─── function processCommandFile(filePath) { 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) => console.log(`Gravity Bridge: approve error: ${e.message}`)); } else if (action === 'reject' && sdk) { sdk.cascade.rejectStep().catch((e) => console.log(`Gravity Bridge: reject error: ${e.message}`)); } else if (action === 'approve_terminal' && sdk) { sdk.cascade.acceptTerminalCommand().catch((e) => 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 if (text === '!auto on') { autoApproveEnabled = true; } else if (text === '!auto off') { autoApproveEnabled = false; } else { // Toggle if no explicit on/off autoApproveEnabled = !autoApproveEnabled; } logToFile(`[AUTO] auto-approve toggled → ${autoApproveEnabled}`); // Confirm back to Discord const emoji = autoApproveEnabled ? '🟢' : '🔴'; const mode = autoApproveEnabled ? '자동 승인 활성' : '수동 승인 모드'; writeChatSnapshot(`${emoji} **Extension 확인**: ${mode} (project=${projectName})`); } else if (text) { // Send message to Antigravity — use VS Code command (most reliable) recentDiscordSentTexts.set(text.trim(), Date.now()); vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text) .then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`), (e) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`)); } // Remove processed command file try { fs.unlinkSync(filePath); } catch { } } catch (e) { console.log(`Gravity Bridge: command processing error: ${e.message}`); } } function watchCommandsDir() { const cmdDir = path.join(bridgePath, 'commands'); // Process existing files const processAllCommands = () => { try { for (const f of fs.readdirSync(cmdDir)) { if (f.endsWith('.json')) { processCommandFile(path.join(cmdDir, f)); } } } catch { } }; processAllCommands(); // Watch for new files (may not fire reliably on Windows) 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 { } // Polling fallback: fs.watch on Windows can silently fail setInterval(() => { processAllCommands(); }, 3000); } // ─── SDK Integration ─── async function initSDK(context) { try { const sdkModule = require('./sdk/index'); AntigravitySDK = sdkModule.AntigravitySDK; } catch (err) { 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'); // ── FIX: SDK's _findLSProcess() uses case-sensitive String.includes() ── // workspace_id in LS process has 'Desktop' (capital D), but SDK hint // generates 'desktop' (lowercase) → match fails → connects to WRONG LS. // Re-discover the correct LS using case-insensitive workspace_id matching. await fixLSConnection(); return true; } catch (err) { console.log(`Gravity Bridge: SDK init failed: ${err.message}`); return false; } } /** * Fix SDK's LS connection by finding the correct language_server process * for this workspace using case-insensitive matching. * * SDK bug: _findLSProcess() compares workspaceHint via JS String.includes() * which is case-sensitive. workspace_id in process args has original casing * (e.g., file_c_3A_Users_Certes_Desktop_variet_agent) but SDK hint is * lowercased (desktop_variet_agent) → no match → falls back to first LS * found (wrong workspace). */ async function fixLSConnection() { if (!sdk?.ls) return; try { const folders = vscode.workspace.workspaceFolders; if (!folders || folders.length === 0) return; // Generate the workspace hint the same way SDK does, but we'll match case-insensitively const folder = folders[0].uri.fsPath; const parts = folder.replace(/\\/g, '/').split('/'); const hint = parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase(); if (!hint) return; // Find all language_server processes with csrf_token const { exec } = cp; const { promisify } = require('util'); const execAsync = promisify(exec); let output; try { const psScript = `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'language_server' -and $_.CommandLine -match 'csrf_token' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }`; const encoded = Buffer.from(psScript, 'utf16le').toString('base64'); const result = await execAsync(`powershell.exe -NoProfile -EncodedCommand ${encoded}`, { encoding: 'utf8', timeout: 15000, windowsHide: true }); output = result.stdout; } catch { return; // Can't discover processes — leave SDK's choice } const lines = output.split('\n').filter((l) => l.trim().length > 0); if (lines.length <= 1) return; // Only one LS — no ambiguity // Find the line whose workspace_id matches our workspace (case-insensitive) let matchedLine = null; for (const line of lines) { const lower = line.toLowerCase(); // Match workspace_id arg against our hint const wsMatch = line.match(/--workspace_id[= ](\S+)/i); if (wsMatch) { const wsid = wsMatch[1].toLowerCase(); if (wsid.includes(hint)) { matchedLine = line; break; } } } if (!matchedLine) { logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`); return; } // Extract port and csrf_token from matched line const csrfMatch = matchedLine.match(/--csrf_token[= ](\S+)/); const extPortMatch = matchedLine.match(/--extension_server_port[= ](\d+)/); const pidMatch = matchedLine.split('|')[0]?.trim(); if (!csrfMatch || !extPortMatch) { logToFile(`[LS-FIX] Matched LS but missing csrf/port args`); return; } const csrfToken = csrfMatch[1]; const extPort = parseInt(extPortMatch[1], 10); const pid = parseInt(pidMatch || '0', 10); // Check if SDK already connected to this LS if (sdk.ls.port === extPort) { logToFile(`[LS-FIX] SDK already on correct LS port=${extPort}`); return; } // Find ConnectRPC port via netstat (same as SDK logic) let netstatOutput; try { const result = await execAsync(`netstat -aon | findstr "LISTENING" | findstr "${pid}"`, { encoding: 'utf8', timeout: 5000, windowsHide: true }); netstatOutput = result.stdout; } catch { // Netstat failed — try extension_server_port as fallback logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`); sdk.ls.setConnection(extPort, csrfToken, false); logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${extPort} hint="${hint}" PID=${pid}`); return; } const portMatches = netstatOutput.matchAll(/127\.0\.0\.1:(\d+)/g); const ports = []; for (const m of portMatches) { const p = parseInt(m[1], 10); if (p !== extPort && !ports.includes(p)) { ports.push(p); } } // Try each port — prefer HTTPS, fall back to HTTP const httpModule = require('http'); const httpsModule = require('https'); for (const useTls of [true, false]) { const mod = useTls ? httpsModule : httpModule; const proto = useTls ? 'https' : 'http'; for (const port of ports) { try { const ok = await new Promise((resolve) => { const req = mod.request(`${proto}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/GetUserStatus`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': 2 }, rejectUnauthorized: false, timeout: 2000, }, (res) => resolve(res.statusCode === 200 || res.statusCode === 401)); req.on('error', () => resolve(false)); req.on('timeout', () => { req.destroy(); resolve(false); }); req.write('{}'); req.end(); }); if (ok) { sdk.ls.setConnection(port, csrfToken, useTls); logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${port} ${proto} hint="${hint}" PID=${pid}`); return; } } catch { /* try next */ } } } // Last resort: use extension_server_port sdk.ls.setConnection(extPort, csrfToken, false); logToFile(`[LS-FIX] ✅ Reconnected via ext_port=${extPort} hint="${hint}" PID=${pid}`); } catch (err) { logToFile(`[LS-FIX] error: ${err.message}`); } } // ─── 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._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] patcher.install() called (needs reload)'); } // Patch BOTH HTML files with inline script injection. // CRITICAL: vscode-file:// does NOT serve custom .js files (silent 404), // so we MUST inline the script directly into BOTH HTML files. // workbench.html — loaded by DevTools/standard mode // workbench-jetski-agent.html — loaded by AG agent mode const scriptDir = path.dirname(scriptPath); // Each HTML file has DIFFERENT CSS/JS entry points — they are NOT interchangeable: // workbench.html → workbench.desktop.main.css + workbench.js // workbench-jetski-agent.html → tw-base.tailwind.css + jetskiMain.tailwind.css + jetskiAgent.js // Cross-restoring between them causes CSS to not load → layout broken (elements visible but all shifted left). const htmlFileSpecs = [ { name: 'workbench.html', requiredMarker: 'workbench.desktop.main.css', // CSS unique to this file requiredScript: 'workbench.js', // JS entry point }, { name: 'workbench-jetski-agent.html', requiredMarker: 'jetskiMain.tailwind.css', // CSS unique to this file requiredScript: 'jetskiAgent.js', // JS entry point }, ]; // ── FIX #1: File lock to prevent multi-instance HTML patching race ── const lockFile = path.join(scriptDir, '.patch-lock'); let lockAcquired = false; try { if (fs.existsSync(lockFile)) { const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs; if (lockAge < 30_000) { logToFile(`[OBSERVER] another instance is patching (lock age=${Math.round(lockAge / 1000)}s) — skipping`); return; // Exit setupApprovalObserver entirely } logToFile(`[OBSERVER] stale lock (age=${Math.round(lockAge / 1000)}s) — force-acquiring`); } fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, ts: Date.now() }), 'utf-8'); lockAcquired = true; } catch (lockErr) { logToFile(`[OBSERVER] lock acquire error: ${lockErr.message} — proceeding anyway`); } for (const spec of htmlFileSpecs) { const htmlPath = path.join(scriptDir, spec.name); const backupPath = htmlPath + '.orig'; try { if (!fs.existsSync(htmlPath)) { logToFile(`[OBSERVER] ${spec.name} not found — skipping`); continue; } let html = fs.readFileSync(htmlPath, 'utf8'); // ── BACKUP: Save original before first-ever patch ── // Only backup if the file looks valid AND hasn't been backed up yet. if (!fs.existsSync(backupPath) && html.length >= 500 && html.includes('') && html.includes(spec.requiredMarker)) { fs.writeFileSync(backupPath, html, 'utf8'); logToFile(`[OBSERVER] ${spec.name} backed up to .orig (${html.length} bytes)`); } // ── SAFETY: Refuse to patch if file is corrupt, empty, or wrong type ── // Race condition: another extension instance may be mid-write (0-byte). // Wrong type: restored from the other HTML file (different CSS/JS refs). const isCorrupt = html.length < 500 || !html.includes(''); const isWrongType = !isCorrupt && !html.includes(spec.requiredMarker); if (isCorrupt || isWrongType) { const reason = isCorrupt ? `corrupt/empty (${html.length} bytes)` : `wrong type (missing ${spec.requiredMarker})`; logToFile(`[OBSERVER] ${spec.name} detected ${reason}`); // Try to restore from backup if (fs.existsSync(backupPath)) { const backup = fs.readFileSync(backupPath, 'utf8'); if (backup.length >= 500 && backup.includes('') && backup.includes(spec.requiredMarker)) { fs.writeFileSync(htmlPath, backup, 'utf8'); html = backup; logToFile(`[OBSERVER] ${spec.name} RESTORED from .orig backup (${backup.length} bytes) ✅`); } else { logToFile(`[OBSERVER] ${spec.name} .orig backup also invalid — SKIPPING`); continue; } } else { logToFile(`[OBSERVER] ${spec.name} no .orig backup available — SKIPPING to prevent further damage`); continue; } } // CRITICAL: Patch CSP to allow inline scripts. // Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'. // Without 'unsafe-inline', all inline \n${inlineMarkerEnd}`); logToFile(`[OBSERVER] ${spec.name} inline script UPDATED`); } else { html = html.replace('', `\n${inlineMarkerStart}\n\n${inlineMarkerEnd}\n`); logToFile(`[OBSERVER] ${spec.name} inline script INSERTED`); } // SAFETY: Final validation before write if (html.length < 500 || !html.includes('') || !html.includes(spec.requiredMarker)) { logToFile(`[OBSERVER] ${spec.name} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`); continue; } fs.writeFileSync(htmlPath, html, 'utf8'); } catch (e) { logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`); } } // Release patch lock if (lockAcquired) { try { fs.unlinkSync(lockFile); } catch { } logToFile('[OBSERVER] patch lock released'); } } // 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) { 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