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
937 lines (789 loc) • 28.4 kB
text/typescript
import { Page, Browser } from 'playwright';
import { DebugState, DOMSnapshot, NetworkRequest } from '../types';
import { LogTruncator } from '../utils/log-truncator';
import { FlutterDebugEngine } from './flutter-debug-engine';
import { ProblemDebugEngine } from './problem-debug-engine';
import { NextJSDebugEngine } from './nextjs-debug-engine';
import { ServerFrameworkDebugEngine } from './server-framework-debug-engine';
import { MetaFrameworkDebugEngine } from './meta-framework-debug-engine';
import { tidewave, TidewaveCapabilities } from '../utils/tidewave-integration';
export class LocalDebugEngine {
private browser?: Browser;
private page?: Page;
private consoleMessages: Array<{ type: string; text: string; timestamp: Date }> = [];
private networkRequests: NetworkRequest[] = [];
private mockedResponses: Map<string, {
status: number;
body: any;
headers?: Record<string, string>;
delay?: number;
}> = new Map();
private flutterEngine?: FlutterDebugEngine;
private problemEngine: ProblemDebugEngine;
private nextjsEngine?: NextJSDebugEngine;
private serverFrameworkEngine?: ServerFrameworkDebugEngine;
private metaFrameworkEngine?: MetaFrameworkDebugEngine;
private tidewaveCapabilities?: TidewaveCapabilities;
constructor() {
this.problemEngine = new ProblemDebugEngine();
}
async attachToPage(page: Page): Promise<void> {
this.page = page;
// Set up console monitoring
page.on('console', (msg) => {
this.consoleMessages.push({
type: msg.type(),
text: msg.text(),
timestamp: new Date()
});
});
// Set up network monitoring and mocking
page.on('request', (request) => {
this.networkRequests.push({
url: request.url(),
method: request.method(),
headers: request.headers(),
timestamp: new Date(),
status: 'pending'
});
});
page.on('response', (response) => {
const index = this.networkRequests.findIndex(
req => req.url === response.url() && req.status === 'pending'
);
if (index !== -1) {
this.networkRequests[index] = {
...this.networkRequests[index],
status: 'complete',
responseStatus: response.status(),
responseHeaders: response.headers()
};
}
});
// Set up route interception for mocking
await this.setupNetworkMocking();
// Attach problem detection engine
await this.problemEngine.attachToPage(page);
// Detect and attach framework-specific engines
await this.attachFrameworkEngines(page);
}
private async setupNetworkMocking(): Promise<void> {
if (!this.page) return;
await this.page.route('**/*', async (route) => {
const url = route.request().url();
// Check if we have a mock for this URL
for (const [pattern, mockData] of this.mockedResponses) {
if (this.matchesPattern(url, pattern)) {
// Apply delay if specified
if (mockData.delay) {
await new Promise(resolve => setTimeout(resolve, mockData.delay));
}
// Return mocked response
await route.fulfill({
status: mockData.status,
headers: mockData.headers || {},
body: typeof mockData.body === 'string'
? mockData.body
: JSON.stringify(mockData.body),
contentType: mockData.headers?.['content-type'] || 'application/json'
});
console.log(`Mocked response for ${url}`);
return;
}
}
// Continue with normal request
await route.continue();
});
}
private matchesPattern(url: string, pattern: string): boolean {
// Support wildcards and simple patterns
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
.replace(/\*/g, '.*'); // Convert * to .*
return new RegExp(regexPattern).test(url);
}
async captureState(): Promise<DebugState> {
if (!this.page) {
throw new Error('No page attached');
}
const [localStorage, sessionStorage, cookies, url] = await Promise.all([
this.captureStorage('localStorage'),
this.captureStorage('sessionStorage'),
this.page.context().cookies(),
this.page.url()
]);
return {
url,
localStorage,
sessionStorage,
cookies,
timestamp: new Date()
};
}
private async captureStorage(type: 'localStorage' | 'sessionStorage'): Promise<Record<string, any>> {
if (!this.page) return {};
try {
return await this.page.evaluate((storageType) => {
const storage = (window as any)[storageType];
const items: Record<string, any> = {};
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key) {
try {
items[key] = JSON.parse(storage.getItem(key) || '');
} catch {
items[key] = storage.getItem(key);
}
}
}
return items;
}, type);
} catch (error) {
return {};
}
}
async captureDOMSnapshot(): Promise<DOMSnapshot> {
if (!this.page) {
throw new Error('No page attached');
}
const snapshot = await this.page.evaluate(() => {
const serializeNode = (node: Element): any => {
const attributes: Record<string, string> = {};
for (const attr of Array.from(node.attributes) as Attr[]) {
attributes[attr.name] = attr.value;
}
return {
tagName: node.tagName.toLowerCase(),
attributes,
children: Array.from(node.children).map(serializeNode),
textContent: node.textContent?.trim() || ''
};
};
return {
html: document.documentElement.outerHTML,
structure: serializeNode(document.documentElement),
timestamp: new Date().toISOString()
};
});
return {
...snapshot,
timestamp: new Date(snapshot.timestamp)
};
}
async injectDebuggingCode(code: string): Promise<any> {
if (!this.page) {
throw new Error('No page attached');
}
return await this.page.evaluate(code);
}
async inspectElement(selector: string): Promise<{
exists: boolean;
visible?: boolean;
properties?: Record<string, any>;
computedStyles?: Record<string, string>;
}> {
if (!this.page) {
throw new Error('No page attached');
}
return await this.page.evaluate((sel) => {
const element = document.querySelector(sel);
if (!element) {
return { exists: false };
}
const rect = element.getBoundingClientRect();
const styles = (window as any).getComputedStyle(element);
const computedStyles: Record<string, string> = {};
// Get key computed styles
['display', 'visibility', 'opacity', 'position', 'top', 'left', 'width', 'height'].forEach(prop => {
computedStyles[prop] = styles.getPropertyValue(prop);
});
return {
exists: true,
visible: rect.width > 0 && rect.height > 0 && styles.display !== 'none' && styles.visibility !== 'hidden',
properties: {
tagName: element.tagName,
id: element.id,
className: element.className,
textContent: element.textContent?.trim(),
innerHTML: element.innerHTML.substring(0, 200),
rect: {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height
}
},
computedStyles
};
}, selector);
}
async simulateUserAction(action: string, selector?: string, value?: any): Promise<void> {
if (!this.page) {
throw new Error('No page attached');
}
switch (action) {
case 'click':
if (selector) {
await this.page.click(selector);
}
break;
case 'type':
if (selector && value) {
await this.page.fill(selector, value);
}
break;
case 'hover':
if (selector) {
await this.page.hover(selector);
}
break;
case 'screenshot':
await this.page.screenshot({
path: value || 'debug-screenshot.png',
fullPage: true
});
break;
case 'reload':
await this.page.reload();
break;
default:
throw new Error(`Unknown action: ${action}`);
}
}
getConsoleMessages(
filter?: { type?: string; since?: Date; maxTokens?: number }
): Array<{ type: string; text: string; timestamp: Date }> {
let messages = [...this.consoleMessages];
if (filter?.type) {
messages = messages.filter(msg => msg.type === filter.type);
}
if (filter?.since) {
messages = messages.filter(msg => msg.timestamp > filter.since!);
}
// Apply intelligent truncation if needed
if (filter?.maxTokens) {
// First try grouping similar messages
messages = LogTruncator.groupSimilarLogs(messages);
// Then truncate to fit token limit
messages = LogTruncator.truncateLogs(messages, {
maxTokens: filter.maxTokens,
preserveLatest: true,
prioritizeErrors: true
});
}
return messages;
}
getNetworkRequests(filter?: { status?: string; pattern?: string }): NetworkRequest[] {
let requests = [...this.networkRequests];
if (filter?.status) {
requests = requests.filter(req => req.status === filter.status);
}
if (filter?.pattern) {
const regex = new RegExp(filter.pattern);
requests = requests.filter(req => regex.test(req.url));
}
return requests;
}
async findReactComponents(): Promise<Array<{ name: string; props: any; state: any }>> {
if (!this.page) {
throw new Error('No page attached');
}
return await this.page.evaluate(() => {
const components: Array<{ name: string; props: any; state: any }> = [];
// Find React fiber nodes
const findReactFiber = (element: Element): any => {
const keys = Object.keys(element);
const fiberKey = keys.find(key => key.startsWith('__reactFiber'));
return fiberKey ? (element as any)[fiberKey] : null;
};
// Walk the DOM and find React components
const walkDOM = (element: Element) => {
const fiber = findReactFiber(element);
if (fiber && fiber.elementType && typeof fiber.elementType === 'function') {
components.push({
name: fiber.elementType.name || 'Anonymous',
props: fiber.memoizedProps || {},
state: fiber.memoizedState || {}
});
}
Array.from(element.children).forEach(walkDOM);
};
walkDOM(document.body);
return components;
}).catch(() => []);
}
async findPhoenixLiveViewState(): Promise<any> {
if (!this.page) {
throw new Error('No page attached');
}
return await this.page.evaluate(() => {
// Look for Phoenix LiveView hooks
const liveSocket = (window as any).liveSocket;
if (!liveSocket) return null;
const views: any[] = [];
// Find all LiveView elements
document.querySelectorAll('[data-phx-view]').forEach((element: any) => {
const phxView = element.getAttribute('data-phx-view');
const phxSession = element.getAttribute('data-phx-session');
views.push({
id: phxView,
session: phxSession,
element: {
id: element.id,
className: element.className,
tagName: element.tagName
}
});
});
return {
connected: liveSocket.isConnected(),
views,
hooks: Object.keys((window as any).Hooks || {})
};
}).catch(() => null);
}
async detectFramework(page: Page): Promise<string> {
this.page = page;
// Check for meta-frameworks first (they might also be React/Vue/etc)
this.metaFrameworkEngine = new MetaFrameworkDebugEngine();
await this.metaFrameworkEngine.attachToPage(page);
const metaFrameworkInfo = this.metaFrameworkEngine.getFrameworkInfo();
if (metaFrameworkInfo?.framework) {
return metaFrameworkInfo.framework;
}
// Check for Next.js (if not detected by meta-framework engine)
this.nextjsEngine = new NextJSDebugEngine();
const isNextJS = await this.nextjsEngine.detectNextJS(page);
if (isNextJS) {
await this.nextjsEngine.attachToPage(page);
return 'nextjs';
}
// Check for server frameworks
this.serverFrameworkEngine = new ServerFrameworkDebugEngine();
const serverFramework = await this.serverFrameworkEngine.detectServerFramework(page);
if (serverFramework) {
await this.serverFrameworkEngine.attachToPage(page);
return serverFramework;
}
// Check for Flutter Web
this.flutterEngine = new FlutterDebugEngine();
const isFlutter = await this.flutterEngine.detectFlutterWeb(page);
if (isFlutter) {
// Try to connect to Flutter DevTools
const port = await this.flutterEngine.findDebugPort(page);
if (port) {
await this.flutterEngine.connect(port);
}
return 'flutter-web';
}
// Check for Phoenix LiveView
const phoenixState = await this.findPhoenixLiveViewState();
if (phoenixState) return 'phoenix';
// Check for React
const reactComponents = await this.findReactComponents();
if (reactComponents.length > 0) return 'react';
// Check for Vue
const hasVue = await page.evaluate(() => {
return !!(window as any).Vue || document.querySelector('[data-v-]');
});
if (hasVue) return 'vue';
// Check for Angular
const hasAngular = await page.evaluate(() => {
return !!(window as any).ng || document.querySelector('[ng-version]');
});
if (hasAngular) return 'angular';
// Check for Svelte
const hasSvelte = await page.evaluate(() => {
return document.querySelector('[data-svelte]') !== null;
});
if (hasSvelte) return 'svelte';
return 'unknown';
}
async injectDebugging(page: Page, framework: string): Promise<void> {
this.page = page;
// Inject framework-agnostic debugging
await page.addInitScript(() => {
(window as any).__AI_DEBUG__ = {
events: [],
recordEvent: function(type: string, data: any) {
this.events.push({
type,
data,
timestamp: new Date().toISOString()
});
}
};
// Monitor console
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
console.log = function(...args) {
(window as any).__AI_DEBUG__.recordEvent('console.log', { args });
return originalLog.apply(console, args);
};
console.error = function(...args) {
(window as any).__AI_DEBUG__.recordEvent('console.error', { args });
return originalError.apply(console, args);
};
console.warn = function(...args) {
(window as any).__AI_DEBUG__.recordEvent('console.warn', { args });
return originalWarn.apply(console, args);
};
// Monitor clicks
document.addEventListener('click', (e: any) => {
const target = e.target as HTMLElement;
(window as any).__AI_DEBUG__.recordEvent('click', {
selector: target.tagName + (target.id ? '#' + target.id : '') + (target.className ? '.' + target.className.split(' ').join('.') : ''),
text: target.textContent?.substring(0, 50)
});
}, true);
});
// Framework-specific instrumentation
if (framework === 'phoenix') {
await page.addInitScript(() => {
const checkLiveSocket = setInterval(() => {
const liveSocket = (window as any).liveSocket;
if (liveSocket) {
clearInterval(checkLiveSocket);
// Monitor LiveView events
const originalPush = liveSocket.push;
liveSocket.push = function(event: any) {
(window as any).__AI_DEBUG__.recordEvent('phoenix.push', { event });
return originalPush.apply(this, arguments);
};
}
}, 100);
});
}
}
async monitorEvents(page: Page, duration: number): Promise<any[]> {
const startTime = Date.now();
const events: any[] = [];
// Collect events for the specified duration
while (Date.now() - startTime < duration * 1000) {
const pageEvents = await page.evaluate(() => {
const aiDebug = (window as any).__AI_DEBUG__;
if (!aiDebug) return [];
const currentEvents = [...aiDebug.events];
aiDebug.events = []; // Clear collected events
return currentEvents;
});
events.push(...pageEvents);
await page.waitForTimeout(1000); // Check every second
}
return events;
}
basicAnalysis(events: any[]): any {
const analysis: any = {
mostActiveElement: null,
performanceScore: 100,
errorRate: 0
};
// Find most clicked element
const clickCounts: Record<string, number> = {};
events.filter(e => e.type === 'click').forEach(e => {
const selector = e.data.selector;
clickCounts[selector] = (clickCounts[selector] || 0) + 1;
});
if (Object.keys(clickCounts).length > 0) {
analysis.mostActiveElement = Object.entries(clickCounts)
.sort(([,a], [,b]) => b - a)[0][0];
}
// Calculate error rate
const errorCount = events.filter(e => e.type === 'console.error' || e.type === 'error').length;
analysis.errorRate = events.length > 0 ? Math.round((errorCount / events.length) * 100) : 0;
// Adjust performance score based on errors
analysis.performanceScore = Math.max(0, 100 - (analysis.errorRate * 2));
return analysis;
}
async simulateAction(page: Page, action: string, selector: string, value?: string): Promise<any> {
const startTime = Date.now();
let success = true;
let error = null;
let description = '';
try {
switch (action) {
case 'click':
await page.click(selector);
description = `Clicked element ${selector}`;
break;
case 'type':
await page.fill(selector, value || '');
description = `Typed "${value}" into ${selector}`;
break;
case 'submit':
await page.press(selector, 'Enter');
description = `Submitted form at ${selector}`;
break;
case 'scroll':
await page.hover(selector);
await page.mouse.wheel(0, parseInt(value || '100'));
description = `Scrolled ${value || 100}px at ${selector}`;
break;
case 'hover':
await page.hover(selector);
description = `Hovered over ${selector}`;
break;
}
} catch (e) {
success = false;
error = e instanceof Error ? e.message : String(e);
description = `Failed to ${action} ${selector}`;
}
return {
success,
error,
description,
duration: Date.now() - startTime
};
}
async captureRecentEvents(page: Page, duration: number): Promise<any[]> {
await page.waitForTimeout(duration);
return await page.evaluate(() => {
const aiDebug = (window as any).__AI_DEBUG__;
if (!aiDebug) return [];
return [...aiDebug.events];
});
}
async extractPageState(page: Page): Promise<any> {
const state = await this.captureState();
const dom = await this.captureDOMSnapshot();
const framework = await this.detectFramework(page);
let frameworkState = null;
if (framework === 'phoenix') {
frameworkState = await this.findPhoenixLiveViewState();
} else if (framework === 'react') {
frameworkState = await this.findReactComponents();
}
return {
...state,
dom,
framework,
frameworkState
};
}
generateBasicReport(session: any): any {
const errorEvents = session.events.filter((e: any) => e.type === 'error' || e.type === 'console.error');
const clickEvents = session.events.filter((e: any) => e.type === 'click');
const networkEvents = session.events.filter((e: any) => e.type === 'networkRequest');
const findings = [];
if (errorEvents.length > 0) {
findings.push(`Found ${errorEvents.length} errors during session`);
}
if (clickEvents.length > 10) {
findings.push(`High user interaction detected (${clickEvents.length} clicks)`);
}
if (networkEvents.length > 50) {
findings.push(`Heavy network activity (${networkEvents.length} requests)`);
}
if (findings.length === 0) {
findings.push('Session completed without significant issues');
}
return { findings };
}
async addNetworkMock(
urlPattern: string,
response: {
status?: number;
body?: any;
headers?: Record<string, string>;
delay?: number;
}
): Promise<void> {
this.mockedResponses.set(urlPattern, {
status: response.status || 200,
body: response.body || {},
headers: response.headers,
delay: response.delay
});
// Re-setup mocking if page is already attached
if (this.page) {
await this.setupNetworkMocking();
}
}
removeNetworkMock(urlPattern: string): boolean {
return this.mockedResponses.delete(urlPattern);
}
clearAllMocks(): void {
this.mockedResponses.clear();
}
getMockedUrls(): string[] {
return Array.from(this.mockedResponses.keys());
}
// Flutter-specific methods
async getFlutterWidgetTree(): Promise<any> {
if (!this.flutterEngine) {
throw new Error('Flutter engine not initialized');
}
return await this.flutterEngine.getWidgetTree();
}
async getFlutterPerformance(): Promise<any> {
if (!this.flutterEngine) {
throw new Error('Flutter engine not initialized');
}
return await this.flutterEngine.getPerformanceInfo();
}
async inspectFlutterWidget(widgetId: string): Promise<any> {
if (!this.flutterEngine) {
throw new Error('Flutter engine not initialized');
}
return await this.flutterEngine.inspectWidget(widgetId);
}
async highlightFlutterWidget(widgetId: string): Promise<void> {
if (!this.flutterEngine) {
throw new Error('Flutter engine not initialized');
}
await this.flutterEngine.highlightWidget(widgetId);
}
async getFlutterMemoryUsage(): Promise<any> {
if (!this.flutterEngine) {
throw new Error('Flutter engine not initialized');
}
return await this.flutterEngine.getMemoryUsage();
}
isFlutterConnected(): boolean {
return this.flutterEngine !== undefined;
}
// Problem-focused debugging methods
async getHydrationIssues(): Promise<any> {
return await this.problemEngine.getHydrationIssues();
}
async getBundleAnalysis(): Promise<any> {
return await this.problemEngine.getBundleAnalysis();
}
async getRouteChanges(): Promise<any> {
return await this.problemEngine.getRouteChanges();
}
async detectCommonProblems(): Promise<any> {
return await this.problemEngine.detectCommonProblems();
}
private async attachFrameworkEngines(page: Page): Promise<void> {
// This is called after framework detection in detectFramework
// Engines are already attached there
}
// Next.js specific methods
async getNextJSPageInfo(): Promise<any> {
if (!this.nextjsEngine) {
throw new Error('Next.js engine not initialized');
}
return await this.nextjsEngine.getPageInfo();
}
async getNextJSBuildInfo(): Promise<any> {
if (!this.nextjsEngine) {
throw new Error('Next.js engine not initialized');
}
return await this.nextjsEngine.getBuildInfo();
}
async getNextJSHydrationErrors(): Promise<any> {
if (!this.nextjsEngine) {
throw new Error('Next.js engine not initialized');
}
return await this.nextjsEngine.getHydrationErrors();
}
async getNextJSImageIssues(): Promise<any> {
if (!this.nextjsEngine) {
throw new Error('Next.js engine not initialized');
}
return await this.nextjsEngine.getImageOptimizationIssues();
}
async detectNextJSProblems(): Promise<any> {
if (!this.nextjsEngine) {
throw new Error('Next.js engine not initialized');
}
return await this.nextjsEngine.detectNextJSProblems();
}
// Server framework specific methods
async getTurboEvents(): Promise<any> {
if (!this.serverFrameworkEngine) {
throw new Error('Server framework engine not initialized');
}
return await this.serverFrameworkEngine.getTurboEvents();
}
async getStimulusControllers(): Promise<any> {
if (!this.serverFrameworkEngine) {
throw new Error('Server framework engine not initialized');
}
return await this.serverFrameworkEngine.getStimulusControllers();
}
async getCSRFIssues(): Promise<any> {
if (!this.serverFrameworkEngine) {
throw new Error('Server framework engine not initialized');
}
return await this.serverFrameworkEngine.getCSRFIssues();
}
async detectServerFrameworkProblems(): Promise<any> {
if (!this.serverFrameworkEngine) {
throw new Error('Server framework engine not initialized');
}
return await this.serverFrameworkEngine.detectServerFrameworkProblems();
}
async getFormSubmissions(): Promise<any> {
if (!this.serverFrameworkEngine) {
throw new Error('Server framework engine not initialized');
}
return await this.serverFrameworkEngine.getFormSubmissions();
}
isNextJSConnected(): boolean {
return this.nextjsEngine !== undefined;
}
isServerFrameworkConnected(): boolean {
return this.serverFrameworkEngine !== undefined;
}
// Meta-framework specific methods
async getMetaFrameworkInfo(): Promise<any> {
return this.metaFrameworkEngine?.getFrameworkInfo();
}
async getRemixLoaderData(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.getRemixLoaderData();
}
async getRemixRouteModules(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.getRemixRouteModules();
}
async getAstroIslands(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.getAstroIslands();
}
async getNuxtPayload(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.getNuxtPayload();
}
async getQwikResumability(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.getQwikResumability();
}
async getViteHMREvents(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.getViteHMREvents();
}
async detectMetaFrameworkProblems(): Promise<any> {
if (!this.metaFrameworkEngine) {
throw new Error('Meta-framework engine not initialized');
}
return await this.metaFrameworkEngine.detectMetaFrameworkProblems();
}
isMetaFrameworkConnected(): boolean {
return this.metaFrameworkEngine !== undefined;
}
async cleanup(): Promise<void> {
if (this.page) {
await this.page.close();
}
if (this.browser) {
await this.browser.close();
}
}
}