UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

430 lines 14.7 kB
/** * WebSocket Automation Server * Zero-latency bidirectional automation with real-time streaming */ import { WebSocketServer, WebSocket } from 'ws'; import { EventEmitter } from 'events'; import { nativeAutomation } from './native-automation-executor.js'; import { uiCache } from './advanced-ui-cache.js'; import { performanceProfiler } from './automation-performance-profiler.js'; export class WebSocketAutomationServer extends EventEmitter { wss = null; clients = new Map(); commandQueue = new Map(); activeStreams = new Map(); performanceMetrics = new Map(); PORT = 9876; HEARTBEAT_INTERVAL = 30000; // 30 seconds constructor() { super(); } /** * Start the WebSocket server */ async start() { // Initialize native automation await nativeAutomation.initialize(); this.wss = new WebSocketServer({ port: this.PORT, perMessageDeflate: { zlibDeflateOptions: { chunkSize: 1024, memLevel: 7, level: 3 }, zlibInflateOptions: { chunkSize: 10 * 1024 }, clientNoContextTakeover: true, serverNoContextTakeover: true, serverMaxWindowBits: 10, concurrencyLimit: 10, threshold: 1024 } }); this.wss.on('connection', (ws, req) => { const clientId = this.generateClientId(); this.handleNewConnection(clientId, ws); }); console.log(`🚀 WebSocket Automation Server listening on port ${this.PORT}`); // Start heartbeat monitoring this.startHeartbeat(); } /** * Handle new WebSocket connection */ handleNewConnection(clientId, ws) { console.log(`🔌 New WebSocket client connected: ${clientId}`); this.clients.set(clientId, ws); // Send welcome message with capabilities ws.send(JSON.stringify({ type: 'welcome', clientId, capabilities: { nativeAutomation: true, caching: true, streaming: true, batch: true, performance: true }, latency_ms: 0.5 // Average latency })); // Handle incoming messages ws.on('message', async (data) => { try { const command = JSON.parse(data.toString()); await this.handleCommand(clientId, command); } catch (error) { this.sendError(clientId, 'invalid-command', error); } }); // Handle close ws.on('close', () => { this.handleDisconnection(clientId); }); // Handle errors ws.on('error', (error) => { console.error(`❌ WebSocket error for client ${clientId}:`, error); }); // Setup ping/pong for connection health ws.on('pong', () => { this.performanceMetrics.set(`${clientId}-last-pong`, Date.now()); }); } /** * Handle automation command */ async handleCommand(clientId, command) { const startTime = performance.now(); performanceProfiler.startOperation(`ws-command-${command.id}`, 'mcp', { type: command.type }); try { switch (command.type) { case 'execute': await this.executeAction(clientId, command); break; case 'batch': await this.executeBatch(clientId, command); break; case 'stream': await this.startStreaming(clientId, command); break; case 'subscribe': this.subscribeToEvents(clientId, command); break; case 'unsubscribe': this.unsubscribeFromEvents(clientId, command); break; default: throw new Error(`Unknown command type: ${command.type}`); } } catch (error) { this.sendError(clientId, command.id, error); } finally { performanceProfiler.endOperation(`ws-command-${command.id}`); const duration = performance.now() - startTime; this.recordLatency(clientId, duration); } } /** * Execute single action with caching */ async executeAction(clientId, command) { if (!command.action) { throw new Error('No action provided'); } const action = command.action; let result; let cached = false; // Check cache for click targets if (action.type === 'click' && action.x && action.y) { const cacheKey = `click-${action.x}-${action.y}`; const cachedElement = uiCache.get(cacheKey); if (cachedElement) { // Use cached coordinates with confidence check if (cachedElement.confidence > 0.8) { action.x = cachedElement.x; action.y = cachedElement.y; cached = true; } } } // Execute with native automation switch (action.type) { case 'click': result = await nativeAutomation.click(action.x || 0, action.y || 0, action.button || 'left', action.clicks || 1); break; case 'move': result = await nativeAutomation.move(action.x || 0, action.y || 0, action.duration || 0); break; case 'key': result = await nativeAutomation.key(action.key || 'space', action.modifiers || []); break; case 'type': result = await nativeAutomation.type(action.text || '', action.delay_ms || 0); break; default: throw new Error(`Unsupported action type: ${action.type}`); } // Update cache if successful if (result.success && action.type === 'click') { const cacheKey = `click-${action.x}-${action.y}`; uiCache.set(cacheKey, { x: action.x || 0, y: action.y || 0, type: 'click-target', confidence: 0.9 }); } // Send response this.sendResponse(clientId, { id: command.id, success: result.success, duration_ms: result.duration_ms, result: result.details, cached }); } /** * Execute batch of actions */ async executeBatch(clientId, command) { if (!command.actions || command.actions.length === 0) { throw new Error('No actions provided for batch'); } const result = await nativeAutomation.executeBatch(command.actions); // Cache successful click locations if (result.success) { const clickActions = command.actions.filter(a => a.type === 'click'); for (const action of clickActions) { if (action.x && action.y) { const cacheKey = `click-${action.x}-${action.y}`; uiCache.set(cacheKey, { x: action.x, y: action.y, type: 'click-target', confidence: 0.9 }); } } } this.sendResponse(clientId, { id: command.id, success: result.success, duration_ms: result.duration_ms, result: result.details }); } /** * Start streaming automation events */ async startStreaming(clientId, command) { const streamId = `${clientId}-${command.id}`; // Stop existing stream if any if (this.activeStreams.has(streamId)) { clearInterval(this.activeStreams.get(streamId)); } // Start new stream const interval = setInterval(() => { this.sendStreamEvent(clientId, { type: 'performance', data: { cache: uiCache.getStats(), latency: this.getAverageLatency(clientId), queueSize: this.commandQueue.size }, timestamp: Date.now() }); }, 1000); // Stream every second this.activeStreams.set(streamId, interval); this.sendResponse(clientId, { id: command.id, success: true, result: { streamId, status: 'streaming' } }); } /** * Subscribe to automation events */ subscribeToEvents(clientId, command) { const eventName = command.event || 'all'; // Subscribe to cache events if (eventName === 'cache' || eventName === 'all') { uiCache.on('cache-update', (data) => { this.sendStreamEvent(clientId, { type: 'screen', data: { event: 'cache-update', ...data }, timestamp: Date.now() }); }); } this.sendResponse(clientId, { id: command.id, success: true, result: { subscribed: eventName } }); } /** * Unsubscribe from events */ unsubscribeFromEvents(clientId, command) { const eventName = command.event || 'all'; // Stop any active streams for (const [streamId, interval] of this.activeStreams.entries()) { if (streamId.startsWith(clientId)) { clearInterval(interval); this.activeStreams.delete(streamId); } } this.sendResponse(clientId, { id: command.id, success: true, result: { unsubscribed: eventName } }); } /** * Handle client disconnection */ handleDisconnection(clientId) { console.log(`🔌 WebSocket client disconnected: ${clientId}`); // Clean up client resources this.clients.delete(clientId); // Stop any active streams for (const [streamId, interval] of this.activeStreams.entries()) { if (streamId.startsWith(clientId)) { clearInterval(interval); this.activeStreams.delete(streamId); } } // Clean up performance metrics for (const key of this.performanceMetrics.keys()) { if (key.startsWith(clientId)) { this.performanceMetrics.delete(key); } } } /** * Send response to client */ sendResponse(clientId, response) { const client = this.clients.get(clientId); if (client && client.readyState === WebSocket.OPEN) { const message = { type: 'response', ...response }; client.send(JSON.stringify(message)); } } /** * Send stream event to client */ sendStreamEvent(clientId, event) { const client = this.clients.get(clientId); if (client && client.readyState === WebSocket.OPEN) { // event already contains 'type' property, so we need to handle this differently const message = { ...event, type: 'stream' // Override the event type with 'stream' }; client.send(JSON.stringify(message)); } } /** * Send error to client */ sendError(clientId, commandId, error) { const client = this.clients.get(clientId); if (client && client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type: 'error', id: commandId, error: error instanceof Error ? error.message : String(error) })); } } /** * Record latency metrics */ recordLatency(clientId, latency) { const key = `${clientId}-latencies`; const latencies = this.performanceMetrics.get(key) || []; latencies.push(latency); // Keep only last 100 measurements if (latencies.length > 100) { latencies.shift(); } this.performanceMetrics.set(key, latencies); } /** * Get average latency for client */ getAverageLatency(clientId) { const key = `${clientId}-latencies`; const latencies = this.performanceMetrics.get(key) || []; if (latencies.length === 0) return 0; const sum = latencies.reduce((a, b) => a + b, 0); return sum / latencies.length; } /** * Start heartbeat monitoring */ startHeartbeat() { setInterval(() => { for (const [clientId, client] of this.clients.entries()) { if (client.readyState === WebSocket.OPEN) { client.ping(); } } }, this.HEARTBEAT_INTERVAL); } /** * Generate unique client ID */ generateClientId() { return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Get server status */ getStatus() { return { running: this.wss !== null, port: this.PORT, clients: this.clients.size, activeStreams: this.activeStreams.size, commandQueueSize: this.commandQueue.size, cacheStats: uiCache.getStats(), averageLatency: Array.from(this.clients.keys()) .map(id => this.getAverageLatency(id)) .reduce((a, b) => a + b, 0) / Math.max(this.clients.size, 1) }; } /** * Stop the server */ stop() { // Stop all active streams for (const interval of this.activeStreams.values()) { clearInterval(interval); } this.activeStreams.clear(); // Close all client connections for (const client of this.clients.values()) { client.close(); } this.clients.clear(); // Close server if (this.wss) { this.wss.close(); this.wss = null; } console.log('🛑 WebSocket Automation Server stopped'); } } // Export singleton instance export const wsAutomationServer = new WebSocketAutomationServer(); //# sourceMappingURL=websocket-automation-server.js.map