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
JavaScript
/**
* 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