import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { WSBridgeClient } from './ws-client'; export interface BrainWatcherContext { logToFile: (msg: string) => void; wsBridge: WSBridgeClient; projectName: string; } export class BrainWatcher { private brainDir: string; private ctx: BrainWatcherContext; private currentSessionId: string = ''; private watcher: fs.FSWatcher | null = null; private lastEventTimes: Map = new Map(); constructor(ctx: BrainWatcherContext) { this.ctx = ctx; // The bridgePath is ~/.gemini/antigravity/bridge, so brain is sibling this.brainDir = path.join(os.homedir(), '.gemini', 'antigravity', 'brain'); } public updateSession(sessionId: string) { if (!sessionId || this.currentSessionId === sessionId) { return; } this.currentSessionId = sessionId; this.startWatching(sessionId); } private startWatching(sessionId: string) { this.stop(); const sessionDir = path.join(this.brainDir, sessionId); if (!fs.existsSync(sessionDir)) { // It might not be created yet, poll gently setTimeout(() => this.startWatching(sessionId), 2000); return; } try { this.watcher = fs.watch(sessionDir, { persistent: false }, (eventType, filename) => { if (!filename || !filename.endsWith('.md')) return; // Dedup rapid events const now = Date.now(); const last = this.lastEventTimes.get(filename) || 0; if (now - last < 500) return; // 500ms debounce this.lastEventTimes.set(filename, now); this.handleFileChange(sessionDir, filename, eventType); }); this.ctx.logToFile(`[BRAIN-WATCHER] Started watching session: ${sessionId.substring(0, 8)}`); } catch (e: any) { this.ctx.logToFile(`[BRAIN-WATCHER] Failed to watch ${sessionId}: ${e.message}`); } } private handleFileChange(dir: string, filename: string, rawEventType: string) { const filePath = path.join(dir, filename); let content = ''; let eventType = 'file_changed'; try { if (fs.existsSync(filePath)) { content = fs.readFileSync(filePath, 'utf-8'); } else { eventType = 'file_deleted'; } } catch (e) { // File might be locked or deleted during read return; } if (this.ctx.wsBridge && this.ctx.wsBridge.isConnected()) { this.ctx.wsBridge.sendBrainEvent({ event_type: eventType, conversation_id: this.currentSessionId, file_name: filename, content: content, timestamp: Date.now() / 1000, project_name: this.ctx.projectName, }); this.ctx.logToFile(`[BRAIN-WATCHER] Sent ${eventType} for ${filename}`); } } public stop() { if (this.watcher) { this.watcher.close(); this.watcher = null; } } }