UNPKG

supapup

Version:

⚔ Lightning-fast MCP browser dev tool. Navigate → Get instant structured data. No screenshots needed! Puppeteer: šŸ“ø → CSS selectors → JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.

414 lines (413 loc) • 18 kB
export class DebuggingTools { page; pausedParams = null; client = null; pausedHandler = () => { }; scriptParsedHandler = () => { }; constructor(page) { this.page = page; this.initializeCDP(); } async initializeCDP() { try { // Create proper CDP session this.client = await this.page.target().createCDPSession(); // Enable debugger domain await this.client.send('Debugger.enable'); // Store handler for cleanup this.pausedHandler = (params) => { this.pausedParams = params; const callFrame = params.callFrames[0]; // console.error(`šŸ›‘ Breakpoint hit at ${callFrame.url}:${callFrame.location.lineNumber}`); // console.error(`šŸ“ Function: ${callFrame.functionName || 'anonymous'}`); // console.error(`šŸ” Reason: ${params.reason}`); }; // Handle debugger paused events this.client.on('Debugger.paused', this.pausedHandler); } catch (err) { // console.error('[DebuggingTools] CDP initialization error:', err); } } // Cleanup method to remove event listeners and close CDP session async cleanup() { if (this.client) { if (this.pausedHandler) { this.client.off('Debugger.paused', this.pausedHandler); } if (this.scriptParsedHandler) { this.client.off('Debugger.scriptParsed', this.scriptParsedHandler); } try { await this.client.detach(); } catch (err) { // Ignore cleanup errors } this.client = null; } } async setBreakpoint(args) { try { const { lineNumber, url = '', condition } = args; await this.page.evaluate(() => { if (!window.chrome?.runtime) { window.chrome = { runtime: {} }; } }); if (!this.client) throw new Error('CDP session not initialized'); // Debugger already enabled in constructor // Build location object according to CDP spec const location = { lineNumber: lineNumber - 1, columnNumber: 0 }; if (url && url !== 'inline') { // Use setBreakpointByUrl for URL-based breakpoints const result = await this.client.send('Debugger.setBreakpointByUrl', { lineNumber: lineNumber - 1, url: url, columnNumber: 0, condition: condition }); return { content: [ { type: 'text', text: `šŸŽÆ Breakpoint set!\n\n` + `šŸ“ Location: ${url}:${lineNumber}\n` + `šŸ†” ID: ${result.breakpointId}\n` + `⚔ Condition: ${condition || 'none'}\n` }, ], }; } else { // For inline scripts, we need to wait for the script to be parsed and use its scriptId // First, enable script parsing events await this.client.send('Debugger.enable'); // Get all parsed scripts const scripts = []; this.scriptParsedHandler = (params) => { scripts.push(params); }; this.client.on('Debugger.scriptParsed', this.scriptParsedHandler); // Force a page evaluation to ensure scripts are parsed await this.page.evaluate(() => { }); // Try using setBreakpointByUrl with the current page URL // This should work for inline scripts in HTML files const currentUrl = await this.page.url(); try { const result = await this.client.send('Debugger.setBreakpointByUrl', { lineNumber: lineNumber - 1, url: currentUrl, urlRegex: undefined, scriptHash: undefined, columnNumber: 0, condition: condition }); return { content: [ { type: 'text', text: `šŸŽÆ Breakpoint set!\n\n` + `šŸ“ Location: ${currentUrl}:${lineNumber}\n` + `šŸ†” ID: ${result.breakpointId}\n` + `šŸ“Œ Locations: ${result.locations?.length || 0} resolved\n` + `⚔ Condition: ${condition || 'none'}\n\n` + `āš ļø Note: For inline scripts, ensure line numbers are correct relative to the HTML file.` }, ], }; } catch (error) { // If that fails, provide helpful error message throw new Error(`Failed to set breakpoint for inline script at line ${lineNumber}. ` + `This is a Chrome DevTools Protocol limitation. ` + `Consider moving your script to an external file or ensure the line number is correct.`); } } } catch (error) { throw new Error(`Failed to set breakpoint: ${error.message}`); } } async removeBreakpoint(args) { try { const { breakpointId } = args; if (!this.client) throw new Error('CDP session not initialized'); await this.client.send('Debugger.removeBreakpoint', { breakpointId }); return { content: [ { type: 'text', text: `šŸ—‘ļø Breakpoint ${breakpointId} removed` }, ], }; } catch (error) { throw new Error(`Failed to remove breakpoint: ${error.message}`); } } async debugContinue() { try { if (!this.client) throw new Error('CDP session not initialized'); await this.client.send('Debugger.resume'); this.pausedParams = null; return { content: [ { type: 'text', text: 'ā–¶ļø Execution resumed\nāœ… Breakpoints cleaned up' }, ], }; } catch (error) { throw new Error(`Failed to continue execution: ${error.message}`); } } async debugStepOver() { try { if (!this.client) throw new Error('CDP session not initialized'); if (!this.pausedParams) { throw new Error('Debugger is not currently paused at a breakpoint.\n' + '\n' + 'šŸ“‹ To use debug_step_over, you must first:\n' + '1. Set a breakpoint using debug_set_breakpoint\n' + '2. Trigger code execution that hits the breakpoint\n' + '3. Or use debug_function to automate both steps\n' + '\n' + 'Example: debug_function({lineNumber: 10})'); } await this.client.send('Debugger.stepOver'); return { content: [ { type: 'text', text: 'šŸ‘‰ Stepped over to next line' }, ], }; } catch (error) { throw new Error(`Failed to step over: ${error.message}`); } } async debugStepInto() { try { if (!this.client) throw new Error('CDP session not initialized'); if (!this.pausedParams) { throw new Error('Debugger is not currently paused at a breakpoint.\n' + '\n' + 'šŸ“‹ To use debug_step_into, you must first:\n' + '1. Set a breakpoint using debug_set_breakpoint\n' + '2. Trigger code execution that hits the breakpoint\n' + '3. Or use debug_function to automate both steps\n' + '\n' + 'Example: debug_function({lineNumber: 10})'); } await this.client.send('Debugger.stepInto'); return { content: [ { type: 'text', text: 'šŸ” Stepped into function' }, ], }; } catch (error) { throw new Error(`Failed to step into: ${error.message}`); } } async debugEvaluate(args) { try { const { expression } = args; if (!this.client) throw new Error('CDP session not initialized'); if (!this.pausedParams) { throw new Error('Debugger is not currently paused at a breakpoint.\n' + '\n' + 'šŸ“‹ To use debug_evaluate, you must first:\n' + '1. Set a breakpoint using debug_set_breakpoint\n' + '2. Trigger code execution that hits the breakpoint\n' + '3. Or use debug_function to automate both steps\n' + '\n' + 'Example: debug_function({lineNumber: 10})\n' + '\n' + 'šŸ’” If you just want to evaluate JavaScript without debugging,\n' + 'use page_evaluate_script instead.'); } const { result } = await this.client.send('Runtime.evaluate', { expression, contextId: this.pausedParams?.callFrames[0]?.callFrameId }); const value = result.value !== undefined ? result.value : result.description; return { content: [ { type: 'text', text: `šŸ” Evaluation Result:\n\n` + `šŸ“ Expression: ${expression}\n` + `šŸ“Š Result: ${JSON.stringify(value, null, 2)}\n` + `šŸ·ļø Type: ${result.type || 'unknown'}` }, ], }; } catch (error) { throw new Error(`Failed to evaluate expression: ${error.message}`); } } async debugGetVariables() { try { if (!this.pausedParams) { throw new Error('Debugger is not currently paused at a breakpoint.\n' + '\n' + 'šŸ“‹ To use debug_get_variables, you must first:\n' + '1. Set a breakpoint using debug_set_breakpoint\n' + '2. Trigger code execution that hits the breakpoint\n' + '3. Or use debug_function to automate both steps\n' + '\n' + 'Example: debug_function({lineNumber: 10})\n' + '\n' + 'šŸ’” This tool shows local variables at the current breakpoint.\n' + 'For general page inspection, use devtools_inspect_element.'); } if (!this.client) throw new Error('CDP session not initialized'); const callFrame = this.pausedParams.callFrames[0]; const { functionName, location } = callFrame; const variables = {}; // Only get the first scope (local scope) to avoid massive responses const localScope = callFrame.scopeChain.find((scope) => scope.type === 'local') || callFrame.scopeChain[0]; if (localScope) { const { result } = await this.client.send('Runtime.getProperties', { objectId: localScope.object.objectId, ownProperties: true, accessorPropertiesOnly: false, generatePreview: false }); // Only get simple values (not functions or complex objects) result.forEach((prop) => { if (prop.value && prop.value.type !== 'function' && !prop.name.startsWith('__')) { const value = prop.value.value !== undefined ? prop.value.value : prop.value.type === 'object' ? `[${prop.value.className || 'Object'}]` : prop.value.description; variables[prop.name] = value; } }); } return { content: [ { type: 'text', text: `šŸ› Debug Context:\n\n` + `šŸ“ Location: ${callFrame.url}:${location.lineNumber + 1}\n` + `šŸ” Function: ${functionName || 'anonymous'}\n` + `šŸ“ Scope: ${localScope?.type || 'unknown'}\n\n` + `šŸ“Š Local Variables:\n${JSON.stringify(variables, null, 2)}\n\n` }, ], }; } catch (error) { throw new Error(`Failed to get variables: ${error.message}`); } } async debugFunction(args) { try { const { lineNumber, triggerAction } = args; // Get current page URL for breakpoint const url = await this.page.url(); // Set breakpoint with URL const breakpointResult = await this.setBreakpoint({ lineNumber, url }); const breakpointId = breakpointResult.content[0].text.match(/šŸ†” ID: ([^\n]+)/)?.[1]; // Try to trigger the action if provided let actionToTrigger = triggerAction; if (!actionToTrigger) { const actions = await this.page.evaluate(() => { const agentPage = window.__AGENT_PAGE__; return agentPage?.manifest?.elements?.map((el) => el.id) || []; }); actionToTrigger = actions[0] || 'click-first-button'; } // Execute action and wait for breakpoint const actionPromise = this.page.evaluate((actionId) => { const agentPage = window.__AGENT_PAGE__; if (agentPage?.execute) { return agentPage.execute(actionId, {}); } return null; }, actionToTrigger); // Wait for debugger to pause or action to complete const pausePromise = new Promise((resolve) => { const checkPause = () => { if (this.pausedParams) { resolve(this.pausedParams); } else { setTimeout(checkPause, 100); } }; checkPause(); }); const result = await Promise.race([ pausePromise, actionPromise.then(() => ({ actionCompleted: true })) ]); if (result.actionCompleted) { return { content: [ { type: 'text', text: `āš ļø Breakpoint at line ${lineNumber} was not triggered by action ${actionToTrigger}.\n` + `The action completed without hitting the breakpoint.\n` }, ], }; } // Get current variables const variablesResult = await this.debugGetVariables(); const variables = JSON.parse(variablesResult.content[0].text.split('šŸ“Š Local Variables:\n')[1].split('\n\n')[0]); const callFrame = this.pausedParams.callFrames[0]; return { content: [ { type: 'text', text: `šŸ› DEBUG SESSION ACTIVE\n\n` + `šŸ“ Breakpoint hit at line ${lineNumber}\n` + `šŸ” Function: ${callFrame.functionName || 'anonymous'}\n` + `šŸŽÆ Triggered by: ${actionToTrigger}\n\n` + `šŸ“Š Current Variables:\n${JSON.stringify(variables, null, 2)}\n\n` + `šŸ”§ NEXT STEPS:\n` + `1. Use debug_evaluate to inspect specific expressions\n` + ` Example: debug_evaluate --expression "variableName"\n\n` + `2. Use debug_step_over to execute next line\n` + ` Example: debug_step_over\n\n` + `3. Use debug_continue to resume normal execution\n` + ` Example: debug_continue\n\n` + `šŸ’” TIP: After stepping, use debug_get_variables to see updated values\n\n` }, ], }; } catch (error) { return { content: [ { type: 'text', text: `āŒ Debug setup failed: ${error.message}` }, ], }; } } }