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

386 lines • 14.7 kB
import { EventEmitter } from 'events'; import * as inspector from 'inspector'; import WebSocket from 'ws'; import { EventListenerManager } from './utils/event-listener-manager.js'; export class BackendDebugEngine extends EventEmitter { sessions = new Map(); inspectorSession; metricsInterval; queryCaptures = new Map(); apiTraces = new Map(); eventListenerManager = new EventListenerManager(); webSocketListeners = new Map(); cdpListeners = new Map(); async attachToProcess(options) { const sessionId = this.generateSessionId(); try { // If attaching to current process if (!options.pid && !options.port) { inspector.open(options.port || 9229, options.host || '127.0.0.1', true); } const session = new inspector.Session(); session.connect(); // Enable necessary domains await this.enableDebugDomains(session); const processInfo = await this.getProcessInfo(); this.sessions.set(sessionId, { id: sessionId, url: `${options.host || 'localhost'}:${options.port || 9229}`, events: [], nodeSession: session, processInfo, startTime: new Date() }); // Start collecting metrics this.startMetricsCollection(sessionId); return { sessionId, processInfo }; } catch (error) { throw new Error(`Failed to attach to process: ${error}`); } } async profileMemory(options) { const session = this.sessions.get(options.sessionId); if (!session?.nodeSession) { throw new Error('Session not found or not attached to process'); } const startMetrics = process.memoryUsage(); const profile = { samples: [], timestamps: [] }; // Start heap profiling if (options.trackAllocations) { await this.sendCommand(session.nodeSession, 'HeapProfiler.startTrackingHeapObjects', { trackAllocations: true }); } // Collect samples over duration const sampleInterval = setInterval(() => { const memory = process.memoryUsage(); profile.samples.push(memory); profile.timestamps.push(Date.now()); }, 100); await new Promise(resolve => setTimeout(resolve, options.duration)); clearInterval(sampleInterval); // Take heap snapshot if requested let heapSnapshot; if (options.heapSnapshot) { heapSnapshot = await this.takeHeapSnapshot(session.nodeSession); } // Stop heap profiling if (options.trackAllocations) { await this.sendCommand(session.nodeSession, 'HeapProfiler.stopTrackingHeapObjects'); } const endMetrics = process.memoryUsage(); return { profile, heapSnapshot, summary: { totalHeapSize: endMetrics.heapTotal, usedHeapSize: endMetrics.heapUsed, peakHeapSize: Math.max(...profile.samples.map((s) => s.heapUsed)), gcCount: profile.samples.length, // Simplified gcTime: options.duration } }; } async traceAsyncOperations(options) { const session = this.sessions.get(options.sessionId); if (!session?.nodeSession) { throw new Error('Session not found'); } const traces = []; // Enable async stack traces await this.sendCommand(session.nodeSession, 'Debugger.setAsyncCallStackDepth', { maxDepth: 32 }); // Set up async hooks const asyncHooks = await import('async_hooks'); const hook = asyncHooks.createHook({ init(asyncId, type, triggerAsyncId) { if ((options.includePromises && type === 'PROMISE') || (options.includeTimers && type.includes('TIMEOUT')) || (options.includeCallbacks && type === 'ASYNCRESOURCE')) { traces.push({ id: asyncId, type, triggerId: triggerAsyncId, timestamp: Date.now(), stack: new Error().stack }); } }, destroy(asyncId) { const trace = traces.find(t => t.id === asyncId); if (trace) { trace.endTime = Date.now(); trace.duration = trace.endTime - trace.timestamp; } } }); hook.enable(); await new Promise(resolve => setTimeout(resolve, options.duration)); hook.disable(); return { traces, summary: { totalOperations: traces.length, pendingPromises: traces.filter(t => t.type === 'PROMISE' && !t.endTime).length, activeTimers: traces.filter(t => t.type.includes('TIMEOUT') && !t.endTime).length, callbackQueue: traces.filter(t => t.type === 'ASYNCRESOURCE' && !t.endTime).length } }; } async captureQueries(options) { const sessionId = options.sessionId; const queries = []; // Store for this session this.queryCaptures.set(sessionId, queries); // Monkey-patch common database libraries this.patchDatabaseLibraries(sessionId, options); await new Promise(resolve => setTimeout(resolve, options.duration)); // Unpatch this.unpatchDatabaseLibraries(); const capturedQueries = this.queryCaptures.get(sessionId) || []; const minTime = options.minExecutionTime; const filtered = minTime !== undefined ? capturedQueries.filter(q => q.duration >= minTime) : capturedQueries; return { queries: filtered, summary: { total: filtered.length, slowest: filtered.sort((a, b) => b.duration - a.duration)[0], totalTime: filtered.reduce((sum, q) => sum + q.duration, 0), averageTime: filtered.length ? filtered.reduce((sum, q) => sum + q.duration, 0) / filtered.length : 0 } }; } async traceAPIRequest(options) { const trace = { method: options.method, path: options.path, statusCode: 0, duration: 0, middleware: [], queries: [], timestamp: Date.now() }; // This would integrate with Express/Fastify/Koa middleware // For now, returning a mock structure return trace; } async monitorWebSocket(options) { const ws = new WebSocket(options.url); const messages = []; const latency = []; let connections = 0; // Store WebSocket for cleanup tracking const wsId = `${options.sessionId}-${Date.now()}`; this.webSocketListeners.set(wsId, ws); // Use EventListenerManager to track WebSocket listeners const openHandler = () => { connections++; if (options.trackConnections) { // console.log('WebSocket connected'); } }; const messageHandler = (data) => { if (options.captureMessages) { messages.push({ type: 'received', data: data.toString(), timestamp: Date.now() }); } }; // Track listeners with EventListenerManager this.eventListenerManager.addEventListener(ws, 'open', openHandler); this.eventListenerManager.addEventListener(ws, 'message', messageHandler); // Monitor for duration await new Promise(resolve => setTimeout(resolve, options.duration)); // Cleanup: Remove tracked listeners this.eventListenerManager.removeEventListener(ws, 'open', openHandler); this.eventListenerManager.removeEventListener(ws, 'message', messageHandler); this.webSocketListeners.delete(wsId); ws.close(); return { connections, messages, latency }; } // Helper methods generateSessionId() { return `backend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } async enableDebugDomains(session) { await this.sendCommand(session, 'Runtime.enable'); await this.sendCommand(session, 'Debugger.enable'); await this.sendCommand(session, 'HeapProfiler.enable'); await this.sendCommand(session, 'Profiler.enable'); } sendCommand(session, method, params) { return new Promise((resolve, reject) => { session.post(method, params, (err, result) => { if (err) reject(err); else resolve(result); }); }); } async getProcessInfo() { return { pid: process.pid, title: process.title, version: process.version, platform: process.platform, memory: process.memoryUsage(), uptime: process.uptime() }; } startMetricsCollection(sessionId) { this.metricsInterval = setInterval(() => { const session = this.sessions.get(sessionId); if (!session) return; const metrics = { cpu: this.getCPUUsage(), memory: process.memoryUsage(), eventLoop: { latency: 0, // Would use perf_hooks activeHandles: process._getActiveHandles?.().length || 0, activeRequests: process._getActiveRequests?.().length || 0 } }; session.metrics = metrics; this.emit('metrics', { sessionId, metrics }); }, 1000); } getCPUUsage() { const usage = process.cpuUsage(); return { user: usage.user, system: usage.system, percent: (usage.user + usage.system) / 1000000 // Simplified }; } async takeHeapSnapshot(session) { const chunks = []; // Store session for cleanup tracking const sessionId = `heap-${Date.now()}`; this.cdpListeners.set(sessionId, session); const chunkHandler = (params) => { chunks.push(params.chunk); }; // Track CDP session listener with EventListenerManager this.eventListenerManager.addEventListener(session, 'HeapProfiler.addHeapSnapshotChunk', chunkHandler); await this.sendCommand(session, 'HeapProfiler.takeHeapSnapshot'); // Cleanup: Remove tracked listener this.eventListenerManager.removeEventListener(session, 'HeapProfiler.addHeapSnapshotChunk', chunkHandler); this.cdpListeners.delete(sessionId); return chunks.join(''); } patchDatabaseLibraries(sessionId, options) { // This would patch common database libraries like: // - pg (PostgreSQL) // - mysql2 // - mongodb // - prisma // For now, this is a placeholder } unpatchDatabaseLibraries() { // Restore original methods } async cleanup(sessionId) { const session = this.sessions.get(sessionId); if (session?.nodeSession) { session.nodeSession.disconnect(); } if (session?.websocket) { session.websocket.close(); } this.sessions.delete(sessionId); // Cleanup EventListenerManager tracked listeners for this session const listenersToRemove = []; for (const [id, ws] of this.webSocketListeners.entries()) { if (id.startsWith(sessionId)) { listenersToRemove.push(id); } } listenersToRemove.forEach(id => this.webSocketListeners.delete(id)); // Cleanup CDP listeners for this session const cdpListenersToRemove = []; for (const [id, cdpSession] of this.cdpListeners.entries()) { if (id.startsWith(sessionId)) { cdpListenersToRemove.push(id); } } cdpListenersToRemove.forEach(id => this.cdpListeners.delete(id)); if (this.sessions.size === 0 && this.metricsInterval) { clearInterval(this.metricsInterval); } } /** * Get count of active WebSocket listeners */ getActiveWebSocketListenerCount() { return this.webSocketListeners.size; } /** * Get count of active CDP session listeners */ getActiveCDPListenerCount() { return this.cdpListeners.size; } /** * Get detailed listener information for debugging */ getDetailedListenerInfo() { const webSocketListeners = this.webSocketListeners.size; const cdpListeners = this.cdpListeners.size; const eventManagerListeners = this.eventListenerManager.getActiveListenerCount(); const listenerDetails = [ ...Array.from(this.webSocketListeners.keys()).map(id => ({ type: 'WebSocket', id, addedAt: new Date() })), ...Array.from(this.cdpListeners.keys()).map(id => ({ type: 'CDP', id, addedAt: new Date() })) ]; return { webSocketListeners, cdpListeners, totalListeners: webSocketListeners + cdpListeners + eventManagerListeners, listenerDetails }; } /** * Cleanup all listeners across all sessions */ async cleanupAll() { // Cleanup all EventListenerManager tracked listeners this.eventListenerManager.cleanup(); // Close all WebSocket connections for (const [id, ws] of this.webSocketListeners.entries()) { try { ws.close(); } catch (error) { console.warn(`Failed to close WebSocket ${id}:`, error); } } this.webSocketListeners.clear(); // Cleanup all CDP sessions this.cdpListeners.clear(); // Cleanup all sessions const sessionIds = Array.from(this.sessions.keys()); for (const sessionId of sessionIds) { await this.cleanup(sessionId); } } } //# sourceMappingURL=backend-debug-engine.js.map