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

368 lines 15.6 kB
export class JavaScriptExecutionEngine { page = null; context = null; isTracingEnabled = false; currentTrace = { functionCalls: new Map(), variableStates: [], codeCoverage: [], errors: [], timeline: [] }; callIdCounter = 0; callStack = []; attach(page, context) { this.page = page; this.context = context; } async enableTracing() { if (!this.page) { throw new Error('No page attached'); } this.isTracingEnabled = true; this.currentTrace = { functionCalls: new Map(), variableStates: [], codeCoverage: [], errors: [], timeline: [] }; // Enable Chrome DevTools Protocol features const client = await this.page.context().newCDPSession(this.page); // Enable runtime await client.send('Runtime.enable'); // Enable debugger await client.send('Debugger.enable'); // Enable profiler for code coverage await client.send('Profiler.enable'); await client.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }); // Inject our tracing instrumentation await this.injectTracingCode(); // Set up error tracking this.page.on('pageerror', (error) => { this.currentTrace.errors.push({ message: error.message, stack: error.stack || '', timestamp: Date.now(), functionId: this.callStack[this.callStack.length - 1] }); }); } async disableTracing() { if (!this.page || !this.isTracingEnabled) { return; } const client = await this.page.context().newCDPSession(this.page); // Get final code coverage const coverage = await client.send('Profiler.takePreciseCoverage'); this.currentTrace.codeCoverage = coverage.result; // Stop profiling await client.send('Profiler.stopPreciseCoverage'); await client.send('Profiler.disable'); await client.send('Debugger.disable'); await client.send('Runtime.disable'); this.isTracingEnabled = false; } async injectTracingCode() { if (!this.page) return; await this.page.addInitScript(() => { // Create a global object to store our tracing data window.__jsTrace = { calls: new Map(), callIdCounter: 0, callStack: [], timeline: [], // Function to wrap other functions with tracing wrapFunction: function (fn, name) { return function (...args) { const callId = `call_${window.__jsTrace.callIdCounter++}`; const startTime = performance.now(); const parentId = window.__jsTrace.callStack[window.__jsTrace.callStack.length - 1]; // Record function call const callInfo = { id: callId, name: name, args: args.map(arg => { try { return JSON.parse(JSON.stringify(arg)); } catch { return String(arg); } }), startTime: startTime, callStack: [...window.__jsTrace.callStack], parentId: parentId, children: [] }; window.__jsTrace.calls.set(callId, callInfo); window.__jsTrace.callStack.push(callId); window.__jsTrace.timeline.push({ type: 'call', functionId: callId, timestamp: startTime }); // Update parent's children if (parentId) { const parent = window.__jsTrace.calls.get(parentId); if (parent) { parent.children.push(callId); } } try { const result = fn.apply(this, args); // Handle async functions if (result && typeof result.then === 'function') { return result.then((value) => { const endTime = performance.now(); callInfo.returnValue = value; callInfo.endTime = endTime; callInfo.duration = endTime - startTime; window.__jsTrace.callStack.pop(); window.__jsTrace.timeline.push({ type: 'return', functionId: callId, timestamp: endTime }); return value; }).catch((error) => { const endTime = performance.now(); callInfo.error = error; callInfo.endTime = endTime; callInfo.duration = endTime - startTime; window.__jsTrace.callStack.pop(); window.__jsTrace.timeline.push({ type: 'error', functionId: callId, timestamp: endTime }); throw error; }); } // Sync function const endTime = performance.now(); callInfo.returnValue = result; callInfo.endTime = endTime; callInfo.duration = endTime - startTime; window.__jsTrace.callStack.pop(); window.__jsTrace.timeline.push({ type: 'return', functionId: callId, timestamp: endTime }); return result; } catch (error) { const endTime = performance.now(); callInfo.error = error; callInfo.endTime = endTime; callInfo.duration = endTime - startTime; window.__jsTrace.callStack.pop(); window.__jsTrace.timeline.push({ type: 'error', functionId: callId, timestamp: endTime }); throw error; } }; }, // Auto-instrument global functions instrumentGlobals: function () { // Instrument setTimeout/setInterval const originalSetTimeout = window.setTimeout; window.setTimeout = window.__jsTrace.wrapFunction(originalSetTimeout, 'setTimeout'); const originalSetInterval = window.setInterval; window.setInterval = window.__jsTrace.wrapFunction(originalSetInterval, 'setInterval'); // Instrument Promise const OriginalPromise = window.Promise; window.Promise = new Proxy(OriginalPromise, { construct(target, args) { const executor = args[0]; const wrappedExecutor = window.__jsTrace.wrapFunction(executor, 'Promise.executor'); return new target(wrappedExecutor); } }); // Instrument fetch const originalFetch = window.fetch; window.fetch = window.__jsTrace.wrapFunction(originalFetch, 'fetch'); // Instrument console methods ['log', 'warn', 'error', 'info', 'debug'].forEach(method => { const original = console[method]; console[method] = window.__jsTrace.wrapFunction(original, `console.${method}`); }); } }; // Start instrumentation window.__jsTrace.instrumentGlobals(); }); } async getExecutionTrace() { if (!this.page) { throw new Error('No page attached'); } // Get trace data from the page const traceData = await this.page.evaluate(() => { const trace = window.__jsTrace; if (!trace) return null; return { calls: Array.from(trace.calls.entries()), timeline: trace.timeline }; }); if (traceData) { // Convert the data back to our format traceData.calls.forEach((entry) => { const [id, call] = entry; this.currentTrace.functionCalls.set(id, call); }); this.currentTrace.timeline = traceData.timeline; } return this.currentTrace; } async instrumentFunction(functionPath, functionName) { if (!this.page) { throw new Error('No page attached'); } await this.page.evaluate(({ path, name }) => { const parts = path.split('.'); let obj = window; for (let i = 0; i < parts.length - 1; i++) { obj = obj[parts[i]]; if (!obj) { throw new Error(`Cannot find object at path: ${parts.slice(0, i + 1).join('.')}`); } } const funcName = parts[parts.length - 1]; const originalFunc = obj[funcName]; if (typeof originalFunc !== 'function') { throw new Error(`${path} is not a function`); } obj[funcName] = window.__jsTrace.wrapFunction(originalFunc, name || path); }, { path: functionPath, name: functionName }); } async captureVariableState(variablePath, scope = 'global') { if (!this.page) { throw new Error('No page attached'); } const state = await this.page.evaluate(({ path, scope }) => { const parts = path.split('.'); let obj = scope === 'global' ? window : eval(scope); let value = obj; for (const part of parts) { value = value[part]; if (value === undefined) break; } return { name: path, value: value, type: typeof value, scope: scope, timestamp: Date.now() }; }, { path: variablePath, scope }); this.currentTrace.variableStates.push(state); return state; } async getCallGraph() { const trace = await this.getExecutionTrace(); // Build a hierarchical call graph const rootCalls = []; const callMap = new Map(trace.functionCalls); for (const [id, call] of callMap) { if (!call.parentId) { rootCalls.push(this.buildCallTree(id, callMap)); } } return { rootCalls, totalCalls: callMap.size, totalDuration: this.calculateTotalDuration(rootCalls), errors: trace.errors }; } buildCallTree(callId, callMap) { const call = callMap.get(callId); if (!call) return null; const children = call.children .map(childId => this.buildCallTree(childId, callMap)) .filter(child => child !== null); return { id: call.id, name: call.name, duration: call.duration, hasError: !!call.error, children }; } calculateTotalDuration(calls) { return calls.reduce((total, call) => { return total + (call.duration || 0); }, 0); } async getPerformanceProfile() { try { if (!this.page || this.page.isClosed()) { console.debug('⚠️ Performance profiling not available - page is closed or not attached'); return { functions: [], totalTime: 0, error: 'Page not available for profiling' }; } const trace = await this.getExecutionTrace(); // Aggregate performance data by function name const profile = new Map(); for (const [_, call] of trace.functionCalls) { if (!call.duration) continue; const existing = profile.get(call.name) || { callCount: 0, totalDuration: 0, avgDuration: 0, minDuration: Infinity, maxDuration: 0 }; existing.callCount++; existing.totalDuration += call.duration; existing.minDuration = Math.min(existing.minDuration, call.duration); existing.maxDuration = Math.max(existing.maxDuration, call.duration); existing.avgDuration = existing.totalDuration / existing.callCount; profile.set(call.name, existing); } // Sort by total duration const sorted = Array.from(profile.entries()) .sort((a, b) => b[1].totalDuration - a[1].totalDuration) .map(([name, stats]) => ({ name, ...stats })); const result = { functions: sorted, totalTime: sorted.reduce((sum, f) => sum + f.totalDuration, 0) }; console.debug(`📊 Performance profile generated: ${sorted.length} functions, ${result.totalTime}ms total time`); return result; } catch (error) { console.debug('⚠️ Error generating performance profile:', error); if (error.message.includes('Target page, context or browser has been closed')) { return { functions: [], totalTime: 0, error: 'Browser context closed during profiling' }; } return { functions: [], totalTime: 0, error: error.message || 'Unknown profiling error' }; } } } //# sourceMappingURL=javascript-execution-engine.js.map