/** * Bridge Listener - Connects to Agent Bridge for task polling * * Uses polling as Supabase Realtime requires direct SDK access * which is complex in CLI context. Polling every 5 seconds * provides good responsiveness for task-based workflows. */ import axios from 'axios'; import { EventEmitter } from 'events'; export interface BridgeListenerEvents { task: (task: BridgeTask) => void; ping: () => void; error: (error: Error) => void; connected: () => void; disconnected: () => void; } export interface BridgeTask { id: string; project_id: string; task_id: string | null; status: string; proposal: string | null; summary: string | null; created_at: string; } export interface BridgeListener extends EventEmitter { connect(): Promise; disconnect(): Promise; on(event: K, listener: BridgeListenerEvents[K]): this; emit(event: K, ...args: Parameters): boolean; } const POLL_INTERVAL_MS = 5000; // 5 seconds export function createBridgeListener( projectId: string, apiUrl: string, apiKey: string ): BridgeListener { const emitter = new EventEmitter() as BridgeListener; let pollInterval: NodeJS.Timeout | null = null; let isConnected = false; let lastCheckedId: string | null = null; const checkBridge = async () => { try { const response = await axios.get(`${apiUrl}/api/v1/agent/bridge`, { params: { project_id: projectId, action: 'check' }, headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }); if (response.data.success && response.data.data?.task) { const task = response.data.data.task; // Check if this is a new task if (task.id !== lastCheckedId) { lastCheckedId = task.id; // Check for ping/heartbeat if (task.proposal?.startsWith('ping')) { emitter.emit('ping'); // Auto-acknowledge ping await acknowledgePing(task.id); } else { // Emit task for processing emitter.emit('task', task); } } } } catch (error: any) { // Don't emit error for network issues during polling // as this is expected when offline if (error.code !== 'ECONNREFUSED' && error.code !== 'ETIMEDOUT') { emitter.emit('error', error); } } }; const acknowledgePing = async (taskId: string) => { try { await axios.post(`${apiUrl}/api/v1/agent/bridge`, { project_id: projectId, action: 'update', bridge_id: taskId, status: 'COMPLETED', summary: 'Pong! Guardian Daemon is active.' }, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 5000 }); } catch { // Silently fail ping acknowledgment } }; emitter.connect = async () => { if (isConnected) return; // Initial check await checkBridge(); // Start polling pollInterval = setInterval(checkBridge, POLL_INTERVAL_MS); isConnected = true; emitter.emit('connected'); }; emitter.disconnect = async () => { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } isConnected = false; emitter.emit('disconnected'); }; return emitter; }