Claude Cursor opencode Skill

chrome-devtools

Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download vodailocz-kilo-kit-mcp-skills_operations_chrome-devtools-0448e6c.zip · 57 KB
Part of vodailocz/kilo-kit-mcp — 142 skills

Install

skills CLI npx skills add https://github.com/VoDaiLocz/kilo-kit-mcp/tree/main/skills/operations/chrome-devtools
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vodailocz-kilo-kit-mcp@llmmart
Git git clone https://github.com/VoDaiLocz/kilo-kit-mcp.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vodailocz/kilo-kit-mcp collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Chrome DevTools Agent Skill

Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing.

Quick Start

CRITICAL: Always check pwd before running scripts.

Installation

Step 1: Install System Dependencies (Linux/WSL only)

On Linux/WSL, Chrome requires system libraries. Install them first:

pwd  # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
./install-deps.sh  # Auto-detects OS and installs required libs

Supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro

macOS/Windows: Skip this step (dependencies bundled with Chrome)

Step 2: Install Node Dependencies

npm install  # Installs puppeteer, debug, yargs

Step 3: Install ImageMagick (Optional, Recommended)

ImageMagick enables automatic screenshot compression to keep files under 5MB:

macOS:

brew install imagemagick

Ubuntu/Debian/WSL:

sudo apt-get install imagemagick

Verify:

magick -version  # or: convert -version

Without ImageMagick, screenshots >5MB will not be compressed (may fail to load in Gemini/Claude).

Test

node navigate.js --url https://example.com
# Output: {"success": true, "url": "https://example.com", "title": "Example Domain"}

Available Scripts

All scripts are in .claude/skills/chrome-devtools/scripts/

CRITICAL: Always check pwd before running scripts.

Script Usage

  • ./scripts/README.md

Core Automation

  • navigate.js - Navigate to URLs
  • screenshot.js - Capture screenshots (full page or element)
  • click.js - Click elements
  • fill.js - Fill form fields
  • evaluate.js - Execute JavaScript in page context

Analysis & Monitoring

  • snapshot.js - Extract interactive elements with metadata
  • console.js - Monitor console messages/errors
  • network.js - Track HTTP requests/responses
  • performance.js - Measure Core Web Vitals + record traces

Usage Patterns

Single Command

pwd  # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
node screenshot.js --url https://example.com --output ./docs/screenshots/page.png

Important: Always save screenshots to ./docs/screenshots directory.

Automatic Image Compression

Screenshots are automatically compressed if they exceed 5MB to ensure compatibility with Gemini API and Claude Code (which have 5MB limits). This uses ImageMagick internally:

# Default: auto-compress if >5MB
node screenshot.js --url https://example.com --output page.png

# Custom size threshold (e.g., 3MB)
node screenshot.js --url https://example.com --output page.png --max-size 3

# Disable compression
node screenshot.js --url https://example.com --output page.png --no-compress

Compression behavior:

  • PNG: Resizes to 90% + quality 85 (or 75% + quality 70 if still too large)
  • JPEG: Quality 80 + progressive encoding (or quality 60 if still too large)
  • Other formats: Converted to JPEG with compression
  • Requires ImageMagick installed (see imagemagick skill)

Output includes compression info:

{
  "success": true,
  "output": "/path/to/page.png",
  "compressed": true,
  "originalSize": 8388608,
  "size": 3145728,
  "compressionRatio": "62.50%",
  "url": "https://example.com"
}

Chain Commands (reuse browser)

# Keep browser open with --close false
node navigate.js --url https://example.com/login --close false
node fill.js --selector "#email" --value "user@example.com" --close false
node fill.js --selector "#password" --value "secret" --close false
node click.js --selector "button[type=submit]"

Parse JSON Output

# Extract specific fields with jq
node performance.js --url https://example.com | jq '.vitals.LCP'

# Save to file
node network.js --url https://example.com --output /tmp/requests.json

Execution Protocol

Working Directory Verification

BEFORE executing any script:

  1. Check current working directory with pwd
  2. Verify in .claude/skills/chrome-devtools/scripts/ directory
  3. If wrong directory, cd to correct location
  4. Use absolute paths for all output files

Example:

pwd  # Should show: .../chrome-devtools/scripts
# If wrong:
cd .claude/skills/chrome-devtools/scripts

Output Validation

AFTER screenshot/capture operations:

  1. Verify file created with ls -lh <output-path>
  2. Read screenshot using Read tool to confirm content
  3. Check JSON output for success:true
  4. Report file size and compression status

Example:

node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
ls -lh ./docs/screenshots/page.png  # Verify file exists
# Then use Read tool to visually inspect
  1. Restart working directory to the project root.

Error Recovery

If script fails:

  1. Check error message for selector issues
  2. Use snapshot.js to discover correct selectors
  3. Try XPath selector if CSS selector fails
  4. Verify element is visible and interactive

Example:

# CSS selector fails
node click.js --url https://example.com --selector ".btn-submit"
# Error: waiting for selector ".btn-submit" failed

# Discover correct selector
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'

# Try XPath
node click.js --url https://example.com --selector "//button[contains(text(),'Submit')]"

Common Mistakes

❌ Wrong working directory → output files go to wrong location ❌ Skipping output validation → silent failures ❌ Using complex CSS selectors without testing → selector errors ❌ Not checking element visibility → timeout errors

✅ Always verify pwd before running scripts ✅ Always validate output after screenshots ✅ Use snapshot.js to discover selectors ✅ Test selectors with simple commands first

Common Workflows

Web Scraping

node evaluate.js --url https://example.com --script "
  Array.from(document.querySelectorAll('.item')).map(el => ({
    title: el.querySelector('h2')?.textContent,
    link: el.querySelector('a')?.href
  }))
" | jq '.result'

Performance Testing

PERF=$(node performance.js --url https://example.com)
LCP=$(echo $PERF | jq '.vitals.LCP')
if (( $(echo "$LCP < 2500" | bc -l) )); then
  echo "✓ LCP passed: ${LCP}ms"
else
  echo "✗ LCP failed: ${LCP}ms"
fi

Form Automation

node fill.js --url https://example.com --selector "#search" --value "query" --close false
node click.js --selector "button[type=submit]"

Error Monitoring

node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount'

Script Options

All scripts support:

  • --headless false - Show browser window
  • --close false - Keep browser open for chaining
  • --timeout 30000 - Set timeout (milliseconds)
  • --wait-until networkidle2 - Wait strategy

See ./scripts/README.md for complete options.

Output Format

All scripts output JSON to stdout:

{
  "success": true,
  "url": "https://example.com",
  ... // script-specific data
}

Errors go to stderr:

{
  "success": false,
  "error": "Error message"
}

Finding Elements

Use snapshot.js to discover selectors:

node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'

Troubleshooting

Common Errors

"Cannot find package 'puppeteer'"

  • Run: npm install in the scripts directory

"error while loading shared libraries: libnss3.so" (Linux/WSL)

  • Missing system dependencies
  • Fix: Run ./install-deps.sh in scripts directory
  • Manual install: sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1

"Failed to launch the browser process"

  • Check system dependencies installed (Linux/WSL)
  • Verify Chrome downloaded: ls ~/.cache/puppeteer
  • Try: npm rebuild then npm install

Chrome not found

  • Puppeteer auto-downloads Chrome during npm install
  • If failed, manually trigger: npx puppeteer browsers install chrome

Script Issues

Element not found

  • Get snapshot first to find correct selector: node snapshot.js --url <url>

Script hangs

  • Increase timeout: --timeout 60000
  • Change wait strategy: --wait-until load or --wait-until domcontentloaded

Blank screenshot

  • Wait for page load: --wait-until networkidle2
  • Increase timeout: --timeout 30000

Permission denied on scripts

  • Make executable: chmod +x *.sh

Screenshot too large (>5MB)

  • Install ImageMagick for automatic compression
  • Manually set lower threshold: --max-size 3
  • Use JPEG format instead of PNG: --format jpeg --quality 80
  • Capture specific element instead of full page: --selector .main-content

Compression not working

  • Verify ImageMagick installed: magick -version or convert -version
  • Check file was actually compressed in output JSON: "compressed": true
  • For very large pages, use --selector to capture only needed area

Resilience and Subagent Stability

When using the browser subagent or Puppeteer scripts, you may encounter connectivity issues (e.g., "connection is reset") or tab loss. Follow these guardrails for professional-grade reliability:

1. State Verification First

Rule: Always call list_pages (or a similar verification script) at the start of a new subagent task or if an error occurs.

  • Do not assume the tab you just used is still active.
  • If a "Connection Reset" error occurs, it usually means the browser instance crashed or closed. Immediately attempt to re-launch or re-initiate the session.

2. Handling Authenticated Sessions

Pattern: Use Persistent Browser Mode for sites requiring login or complex state.

  • Launch with node launch-persistent.js --headless false.
  • This ensures the browser process stays alive even if the subagent session times out.
  • Subsequent subagents can reconnect via the WebSocket endpoint in .browser-endpoint.

3. Progressive Handoff

Rule: Use --close false consistently if the workflow requires multiple steps.

  • When finishing a subagent task, explicitly check if the browser should remain open for the next phase.
  • Provide the user with the URL and a screenshot of the final state to confirm continuity.

4. Error Recovery Strategy

If a "Page not found" or "Target closed" error occurs:

  1. List all open pages to see if the tab was accidentally closed or navigated away.
  2. Check for session tokens (cookies/local storage) if the site allows session persistence.
  3. Re-navigate to the last known URL if the tab is truly lost.
  4. Notify the user clearly about the reset instead of retrying silently 10 times.

5. Interaction Heartbeats

Before a complex click or fill sequence, take a small snapshot or evaluate a simple script (e.g., () => document.readyState) to "ping" the page. If it fails, trigger the recovery strategy early.


Reference Documentation

Detailed guides available in ./references/:

Advanced Usage

Custom Scripts

Create custom scripts using shared library:

import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js';
// Your automation logic

Direct CDP Access

const client = await page.createCDPSession();
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });

See reference documentation for advanced patterns and complete API coverage.

External Resources

Files (kilo-kit-mcp)
  • references
    • cdp-domains.md 17.4 KB
      # Chrome DevTools Protocol (CDP) Domains Reference
      
      Complete reference of CDP domains and their capabilities for browser automation and debugging.
      
      ## Overview
      
      CDP is organized into **47 domains**, each providing specific browser capabilities. Domains are grouped by functionality:
      
      - **Core** - Fundamental browser control
      - **DOM & Styling** - Page structure and styling
      - **Network & Fetch** - HTTP traffic management
      - **Page & Navigation** - Page lifecycle control
      - **Storage & Data** - Browser storage APIs
      - **Performance & Profiling** - Metrics and analysis
      - **Emulation & Simulation** - Device and network emulation
      - **Worker & Service** - Background tasks
      - **Developer Tools** - Debugging support
      
      ---
      
      ## Core Domains
      
      ### Runtime
      **Purpose:** Execute JavaScript, manage objects, handle promises
      
      **Key Commands:**
      - `Runtime.evaluate(expression)` - Execute JavaScript
      - `Runtime.callFunctionOn(functionDeclaration, objectId)` - Call function on object
      - `Runtime.getProperties(objectId)` - Get object properties
      - `Runtime.awaitPromise(promiseObjectId)` - Wait for promise resolution
      
      **Key Events:**
      - `Runtime.consoleAPICalled` - Console message logged
      - `Runtime.exceptionThrown` - Uncaught exception
      
      **Use Cases:**
      - Execute custom JavaScript
      - Access page data
      - Monitor console output
      - Handle exceptions
      
      ---
      
      ### Debugger
      **Purpose:** JavaScript debugging, breakpoints, stack traces
      
      **Key Commands:**
      - `Debugger.enable()` - Enable debugger
      - `Debugger.setBreakpoint(location)` - Set breakpoint
      - `Debugger.pause()` - Pause execution
      - `Debugger.resume()` - Resume execution
      - `Debugger.stepOver/stepInto/stepOut()` - Step through code
      
      **Key Events:**
      - `Debugger.paused` - Execution paused
      - `Debugger.resumed` - Execution resumed
      - `Debugger.scriptParsed` - Script loaded
      
      **Use Cases:**
      - Debug JavaScript errors
      - Inspect call stacks
      - Set conditional breakpoints
      - Source map support
      
      ---
      
      ### Console (Deprecated - Use Runtime/Log)
      **Purpose:** Legacy console message access
      
      **Note:** Use `Runtime.consoleAPICalled` event instead for new implementations.
      
      ---
      
      ## DOM & Styling Domains
      
      ### DOM
      **Purpose:** Access and manipulate DOM tree
      
      **Key Commands:**
      - `DOM.getDocument()` - Get root document node
      - `DOM.querySelector(nodeId, selector)` - Query selector
      - `DOM.querySelectorAll(nodeId, selector)` - Query all
      - `DOM.getAttributes(nodeId)` - Get element attributes
      - `DOM.setOuterHTML(nodeId, outerHTML)` - Replace element
      - `DOM.getBoxModel(nodeId)` - Get element layout box
      - `DOM.focus(nodeId)` - Focus element
      
      **Key Events:**
      - `DOM.documentUpdated` - Document changed
      - `DOM.setChildNodes` - Child nodes updated
      
      **Use Cases:**
      - Navigate DOM tree
      - Query elements
      - Modify DOM structure
      - Get element positions
      
      ---
      
      ### CSS
      **Purpose:** Inspect and modify CSS styles
      
      **Key Commands:**
      - `CSS.enable()` - Enable CSS domain
      - `CSS.getComputedStyleForNode(nodeId)` - Get computed styles
      - `CSS.getInlineStylesForNode(nodeId)` - Get inline styles
      - `CSS.getMatchedStylesForNode(nodeId)` - Get matched CSS rules
      - `CSS.setStyleTexts(edits)` - Modify styles
      
      **Key Events:**
      - `CSS.styleSheetAdded` - Stylesheet added
      - `CSS.styleSheetChanged` - Stylesheet modified
      
      **Use Cases:**
      - Inspect element styles
      - Debug CSS issues
      - Modify styles dynamically
      - Extract stylesheet data
      
      ---
      
      ### Accessibility
      **Purpose:** Access accessibility tree
      
      **Key Commands:**
      - `Accessibility.enable()` - Enable accessibility
      - `Accessibility.getFullAXTree()` - Get complete AX tree
      - `Accessibility.getPartialAXTree(nodeId)` - Get node subtree
      - `Accessibility.queryAXTree(nodeId, role, name)` - Query AX tree
      
      **Use Cases:**
      - Accessibility testing
      - Screen reader simulation
      - ARIA attribute inspection
      - AX tree analysis
      
      ---
      
      ## Network & Fetch Domains
      
      ### Network
      **Purpose:** Monitor and control HTTP traffic
      
      **Key Commands:**
      - `Network.enable()` - Enable network tracking
      - `Network.setCacheDisabled(cacheDisabled)` - Disable cache
      - `Network.setExtraHTTPHeaders(headers)` - Add custom headers
      - `Network.getCookies(urls)` - Get cookies
      - `Network.setCookie(name, value, domain)` - Set cookie
      - `Network.getResponseBody(requestId)` - Get response body
      - `Network.emulateNetworkConditions(offline, latency, downloadThroughput, uploadThroughput)` - Throttle network
      
      **Key Events:**
      - `Network.requestWillBeSent` - Request starting
      - `Network.responseReceived` - Response received
      - `Network.loadingFinished` - Request completed
      - `Network.loadingFailed` - Request failed
      
      **Use Cases:**
      - Monitor API calls
      - Intercept requests
      - Analyze response data
      - Simulate slow networks
      - Manage cookies
      
      ---
      
      ### Fetch
      **Purpose:** Intercept and modify network requests
      
      **Key Commands:**
      - `Fetch.enable(patterns)` - Enable request interception
      - `Fetch.continueRequest(requestId, url, method, headers)` - Continue/modify request
      - `Fetch.fulfillRequest(requestId, responseCode, headers, body)` - Mock response
      - `Fetch.failRequest(requestId, errorReason)` - Fail request
      
      **Key Events:**
      - `Fetch.requestPaused` - Request intercepted
      
      **Use Cases:**
      - Mock API responses
      - Block requests
      - Modify request/response
      - Test error scenarios
      
      ---
      
      ## Page & Navigation Domains
      
      ### Page
      **Purpose:** Control page lifecycle and navigation
      
      **Key Commands:**
      - `Page.enable()` - Enable page domain
      - `Page.navigate(url)` - Navigate to URL
      - `Page.reload(ignoreCache)` - Reload page
      - `Page.goBack()/goForward()` - Navigate history
      - `Page.captureScreenshot(format, quality)` - Take screenshot
      - `Page.printToPDF(landscape, displayHeaderFooter)` - Generate PDF
      - `Page.getLayoutMetrics()` - Get page dimensions
      - `Page.createIsolatedWorld(frameId)` - Create isolated context
      - `Page.handleJavaScriptDialog(accept, promptText)` - Handle alerts/confirms
      
      **Key Events:**
      - `Page.loadEventFired` - Page loaded
      - `Page.domContentEventFired` - DOM ready
      - `Page.frameNavigated` - Frame navigated
      - `Page.javascriptDialogOpening` - Alert/confirm shown
      
      **Use Cases:**
      - Navigate pages
      - Capture screenshots
      - Generate PDFs
      - Handle popups
      - Monitor page lifecycle
      
      ---
      
      ### Target
      **Purpose:** Manage browser targets (tabs, workers, frames)
      
      **Key Commands:**
      - `Target.getTargets()` - List all targets
      - `Target.createTarget(url)` - Open new tab
      - `Target.closeTarget(targetId)` - Close tab
      - `Target.attachToTarget(targetId)` - Attach debugger
      - `Target.detachFromTarget(sessionId)` - Detach debugger
      - `Target.setDiscoverTargets(discover)` - Auto-discover targets
      
      **Key Events:**
      - `Target.targetCreated` - New target created
      - `Target.targetDestroyed` - Target closed
      - `Target.targetInfoChanged` - Target updated
      
      **Use Cases:**
      - Multi-tab automation
      - Service worker debugging
      - Frame inspection
      - Extension debugging
      
      ---
      
      ### Input
      **Purpose:** Simulate user input
      
      **Key Commands:**
      - `Input.dispatchKeyEvent(type, key, code)` - Keyboard input
      - `Input.dispatchMouseEvent(type, x, y, button)` - Mouse input
      - `Input.dispatchTouchEvent(type, touchPoints)` - Touch input
      - `Input.synthesizePinchGesture(x, y, scaleFactor)` - Pinch gesture
      - `Input.synthesizeScrollGesture(x, y, xDistance, yDistance)` - Scroll
      
      **Use Cases:**
      - Simulate clicks
      - Type text
      - Drag and drop
      - Touch gestures
      - Scroll pages
      
      ---
      
      ## Storage & Data Domains
      
      ### Storage
      **Purpose:** Manage browser storage
      
      **Key Commands:**
      - `Storage.getCookies(browserContextId)` - Get cookies
      - `Storage.setCookies(cookies)` - Set cookies
      - `Storage.clearCookies(browserContextId)` - Clear cookies
      - `Storage.clearDataForOrigin(origin, storageTypes)` - Clear storage
      - `Storage.getUsageAndQuota(origin)` - Get storage usage
      
      **Storage Types:**
      - appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage
      
      **Use Cases:**
      - Cookie management
      - Clear browser data
      - Inspect storage usage
      - Test quota limits
      
      ---
      
      ### DOMStorage
      **Purpose:** Access localStorage/sessionStorage
      
      **Key Commands:**
      - `DOMStorage.enable()` - Enable storage tracking
      - `DOMStorage.getDOMStorageItems(storageId)` - Get items
      - `DOMStorage.setDOMStorageItem(storageId, key, value)` - Set item
      - `DOMStorage.removeDOMStorageItem(storageId, key)` - Remove item
      
      **Key Events:**
      - `DOMStorage.domStorageItemsCleared` - Storage cleared
      - `DOMStorage.domStorageItemAdded/Updated/Removed` - Item changed
      
      ---
      
      ### IndexedDB
      **Purpose:** Query IndexedDB databases
      
      **Key Commands:**
      - `IndexedDB.requestDatabaseNames(securityOrigin)` - List databases
      - `IndexedDB.requestDatabase(securityOrigin, databaseName)` - Get DB structure
      - `IndexedDB.requestData(securityOrigin, databaseName, objectStoreName)` - Query data
      
      **Use Cases:**
      - Inspect IndexedDB data
      - Debug database issues
      - Extract stored data
      
      ---
      
      ### CacheStorage
      **Purpose:** Manage Cache API
      
      **Key Commands:**
      - `CacheStorage.requestCacheNames(securityOrigin)` - List caches
      - `CacheStorage.requestCachedResponses(cacheId, securityOrigin)` - List cached responses
      - `CacheStorage.deleteCache(cacheId)` - Delete cache
      
      **Use Cases:**
      - Service worker cache inspection
      - Offline functionality testing
      
      ---
      
      ## Performance & Profiling Domains
      
      ### Performance
      **Purpose:** Collect performance metrics
      
      **Key Commands:**
      - `Performance.enable()` - Enable performance tracking
      - `Performance.disable()` - Disable tracking
      - `Performance.getMetrics()` - Get current metrics
      
      **Metrics:**
      - Timestamp, Documents, Frames, JSEventListeners, Nodes, LayoutCount, RecalcStyleCount, LayoutDuration, RecalcStyleDuration, ScriptDuration, TaskDuration, JSHeapUsedSize, JSHeapTotalSize
      
      **Use Cases:**
      - Monitor page metrics
      - Track memory usage
      - Measure render times
      
      ---
      
      ### PerformanceTimeline
      **Purpose:** Access Performance Timeline API
      
      **Key Commands:**
      - `PerformanceTimeline.enable(eventTypes)` - Subscribe to events
      
      **Event Types:**
      - mark, measure, navigation, resource, longtask, paint, layout-shift
      
      **Key Events:**
      - `PerformanceTimeline.timelineEventAdded` - New performance entry
      
      ---
      
      ### Tracing
      **Purpose:** Record Chrome trace
      
      **Key Commands:**
      - `Tracing.start(categories, options)` - Start recording
      - `Tracing.end()` - Stop recording
      - `Tracing.requestMemoryDump()` - Capture memory snapshot
      
      **Trace Categories:**
      - blink, cc, devtools, gpu, loading, navigation, rendering, v8, disabled-by-default-*
      
      **Key Events:**
      - `Tracing.dataCollected` - Trace chunk received
      - `Tracing.tracingComplete` - Recording finished
      
      **Use Cases:**
      - Deep performance analysis
      - Frame rendering profiling
      - CPU flame graphs
      - Memory profiling
      
      ---
      
      ### Profiler
      **Purpose:** CPU profiling
      
      **Key Commands:**
      - `Profiler.enable()` - Enable profiler
      - `Profiler.start()` - Start CPU profiling
      - `Profiler.stop()` - Stop and get profile
      
      **Use Cases:**
      - Find CPU bottlenecks
      - Optimize JavaScript
      - Generate flame graphs
      
      ---
      
      ### HeapProfiler (via Memory domain)
      **Purpose:** Memory profiling
      
      **Key Commands:**
      - `Memory.getDOMCounters()` - Get DOM object counts
      - `Memory.prepareForLeakDetection()` - Prepare leak detection
      - `Memory.forciblyPurgeJavaScriptMemory()` - Force GC
      - `Memory.setPressureNotificationsSuppressed(suppressed)` - Control memory warnings
      - `Memory.simulatePressureNotification(level)` - Simulate memory pressure
      
      **Use Cases:**
      - Detect memory leaks
      - Analyze heap snapshots
      - Monitor object counts
      
      ---
      
      ## Emulation & Simulation Domains
      
      ### Emulation
      **Purpose:** Emulate device conditions
      
      **Key Commands:**
      - `Emulation.setDeviceMetricsOverride(width, height, deviceScaleFactor, mobile)` - Emulate device
      - `Emulation.setGeolocationOverride(latitude, longitude, accuracy)` - Fake location
      - `Emulation.setEmulatedMedia(media, features)` - Emulate media type
      - `Emulation.setTimezoneOverride(timezoneId)` - Override timezone
      - `Emulation.setLocaleOverride(locale)` - Override language
      - `Emulation.setUserAgentOverride(userAgent)` - Change user agent
      
      **Use Cases:**
      - Mobile device testing
      - Geolocation testing
      - Print media emulation
      - Timezone/locale testing
      
      ---
      
      ### DeviceOrientation
      **Purpose:** Simulate device orientation
      
      **Key Commands:**
      - `DeviceOrientation.setDeviceOrientationOverride(alpha, beta, gamma)` - Set orientation
      
      **Use Cases:**
      - Test accelerometer features
      - Orientation-dependent layouts
      
      ---
      
      ## Worker & Service Domains
      
      ### ServiceWorker
      **Purpose:** Manage service workers
      
      **Key Commands:**
      - `ServiceWorker.enable()` - Enable tracking
      - `ServiceWorker.unregister(scopeURL)` - Unregister worker
      - `ServiceWorker.startWorker(scopeURL)` - Start worker
      - `ServiceWorker.stopWorker(versionId)` - Stop worker
      - `ServiceWorker.inspectWorker(versionId)` - Debug worker
      
      **Key Events:**
      - `ServiceWorker.workerRegistrationUpdated` - Registration changed
      - `ServiceWorker.workerVersionUpdated` - Version updated
      
      ---
      
      ### WebAuthn
      **Purpose:** Simulate WebAuthn/FIDO2
      
      **Key Commands:**
      - `WebAuthn.enable()` - Enable virtual authenticators
      - `WebAuthn.addVirtualAuthenticator(options)` - Add virtual device
      - `WebAuthn.removeVirtualAuthenticator(authenticatorId)` - Remove device
      - `WebAuthn.addCredential(authenticatorId, credential)` - Add credential
      
      **Use Cases:**
      - Test WebAuthn flows
      - Simulate biometric auth
      - Test security keys
      
      ---
      
      ## Developer Tools Support
      
      ### Inspector
      **Purpose:** Protocol-level debugging
      
      **Key Events:**
      - `Inspector.detached` - Debugger disconnected
      - `Inspector.targetCrashed` - Target crashed
      
      ---
      
      ### Log
      **Purpose:** Collect browser logs
      
      **Key Commands:**
      - `Log.enable()` - Enable log collection
      - `Log.clear()` - Clear logs
      
      **Key Events:**
      - `Log.entryAdded` - New log entry
      
      **Use Cases:**
      - Collect console logs
      - Monitor violations
      - Track deprecations
      
      ---
      
      ### DOMDebugger
      **Purpose:** DOM-level debugging
      
      **Key Commands:**
      - `DOMDebugger.setDOMBreakpoint(nodeId, type)` - Break on DOM changes
      - `DOMDebugger.setEventListenerBreakpoint(eventName)` - Break on event
      - `DOMDebugger.setXHRBreakpoint(url)` - Break on XHR
      
      **Breakpoint Types:**
      - subtree-modified, attribute-modified, node-removed
      
      ---
      
      ### DOMSnapshot
      **Purpose:** Capture complete DOM snapshot
      
      **Key Commands:**
      - `DOMSnapshot.captureSnapshot(computedStyles)` - Capture full DOM
      
      **Use Cases:**
      - Export page structure
      - Offline analysis
      - DOM diffing
      
      ---
      
      ### Audits (Lighthouse Integration)
      **Purpose:** Run automated audits
      
      **Key Commands:**
      - `Audits.enable()` - Enable audits
      - `Audits.getEncodingIssues()` - Check encoding issues
      
      ---
      
      ### LayerTree
      **Purpose:** Inspect rendering layers
      
      **Key Commands:**
      - `LayerTree.enable()` - Enable layer tracking
      - `LayerTree.compositingReasons(layerId)` - Get why layer created
      
      **Key Events:**
      - `LayerTree.layerTreeDidChange` - Layers changed
      
      **Use Cases:**
      - Debug rendering performance
      - Identify layer creation
      - Optimize compositing
      
      ---
      
      ## Other Domains
      
      ### Browser
      **Purpose:** Browser-level control
      
      **Key Commands:**
      - `Browser.getVersion()` - Get browser info
      - `Browser.getBrowserCommandLine()` - Get launch args
      - `Browser.setPermission(permission, setting, origin)` - Set permissions
      - `Browser.grantPermissions(permissions, origin)` - Grant permissions
      
      **Permissions:**
      - geolocation, midi, notifications, push, camera, microphone, background-sync, sensors, accessibility-events, clipboard-read, clipboard-write, payment-handler
      
      ---
      
      ### IO
      **Purpose:** File I/O operations
      
      **Key Commands:**
      - `IO.read(handle, offset, size)` - Read stream
      - `IO.close(handle)` - Close stream
      
      **Use Cases:**
      - Read large response bodies
      - Process binary data
      
      ---
      
      ### Media
      **Purpose:** Inspect media players
      
      **Key Commands:**
      - `Media.enable()` - Track media players
      
      **Key Events:**
      - `Media.playerPropertiesChanged` - Player state changed
      - `Media.playerEventsAdded` - Player events
      
      ---
      
      ### BackgroundService
      **Purpose:** Track background services
      
      **Key Commands:**
      - `BackgroundService.startObserving(service)` - Track service
      
      **Services:**
      - backgroundFetch, backgroundSync, pushMessaging, notifications, paymentHandler, periodicBackgroundSync
      
      ---
      
      ## Domain Dependencies
      
      Some domains depend on others and must be enabled in order:
      
      ```
      Runtime (no dependencies)
        ↓
      DOM (depends on Runtime)
        ↓
      CSS (depends on DOM)
      
      Network (no dependencies)
      
      Page (depends on Runtime)
        ↓
      Target (depends on Page)
      
      Debugger (depends on Runtime)
      ```
      
      ## Quick Command Reference
      
      ### Most Common Commands
      
      ```javascript
      // Navigation
      Page.navigate(url)
      Page.reload()
      
      // JavaScript Execution
      Runtime.evaluate(expression)
      
      // DOM Access
      DOM.getDocument()
      DOM.querySelector(nodeId, selector)
      
      // Screenshots
      Page.captureScreenshot(format, quality)
      
      // Network Monitoring
      Network.enable()
      // Listen for Network.requestWillBeSent events
      
      // Console Messages
      // Listen for Runtime.consoleAPICalled events
      
      // Cookies
      Network.getCookies(urls)
      Network.setCookie(...)
      
      // Device Emulation
      Emulation.setDeviceMetricsOverride(width, height, ...)
      
      // Performance
      Performance.getMetrics()
      Tracing.start(categories)
      Tracing.end()
      ```
      
      ---
      
      ## Best Practices
      
      1. **Enable domains before use:** Always call `.enable()` for stateful domains
      2. **Handle events:** Subscribe to events for real-time updates
      3. **Clean up:** Disable domains when done to reduce overhead
      4. **Use sessions:** Attach to specific targets for isolated debugging
      5. **Handle errors:** Implement proper error handling for command failures
      6. **Version awareness:** Check browser version for experimental API support
      
      ---
      
      ## Additional Resources
      
      - [Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/) - Interactive domain browser
      - [Protocol JSON](https://chromedevtools.github.io/devtools-protocol/tot/json) - Machine-readable specification
      - [Getting Started with CDP](https://github.com/aslushnikov/getting-started-with-cdp)
      - [devtools-protocol NPM](https://www.npmjs.com/package/devtools-protocol) - TypeScript definitions
      
    • performance-guide.md 21 KB
      # Performance Analysis Guide
      
      Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools skill.
      
      ## Table of Contents
      
      - [Core Web Vitals](#core-web-vitals)
      - [Performance Tracing](#performance-tracing)
      - [Network Analysis](#network-analysis)
      - [JavaScript Performance](#javascript-performance)
      - [Rendering Performance](#rendering-performance)
      - [Memory Analysis](#memory-analysis)
      - [Optimization Strategies](#optimization-strategies)
      
      ---
      
      ## Core Web Vitals
      
      ### Overview
      
      Core Web Vitals are Google's standardized metrics for measuring user experience:
      
      - **LCP (Largest Contentful Paint)** - Loading performance (< 2.5s good)
      - **FID (First Input Delay)** - Interactivity (< 100ms good)
      - **CLS (Cumulative Layout Shift)** - Visual stability (< 0.1 good)
      
      ### Measuring with chrome-devtools-mcp
      
      ```javascript
      // Start performance trace
      await useTool('performance_start_trace', {
        categories: ['loading', 'rendering', 'scripting']
      });
      
      // Navigate to page
      await useTool('navigate_page', {
        url: 'https://example.com'
      });
      
      // Wait for complete load
      await useTool('wait_for', {
        waitUntil: 'networkidle'
      });
      
      // Stop trace and get data
      await useTool('performance_stop_trace');
      
      // Get AI-powered insights
      const insights = await useTool('performance_analyze_insight');
      
      // insights will include:
      // - LCP timing
      // - FID analysis
      // - CLS score
      // - Performance recommendations
      ```
      
      ### Measuring with Puppeteer
      
      ```javascript
      import puppeteer from 'puppeteer';
      
      const browser = await puppeteer.launch();
      const page = await browser.newPage();
      
      // Measure Core Web Vitals
      await page.goto('https://example.com', {
        waitUntil: 'networkidle2'
      });
      
      const vitals = await page.evaluate(() => {
        return new Promise((resolve) => {
          const vitals = {
            LCP: null,
            FID: null,
            CLS: 0
          };
      
          // LCP
          new PerformanceObserver((list) => {
            const entries = list.getEntries();
            vitals.LCP = entries[entries.length - 1].renderTime ||
                         entries[entries.length - 1].loadTime;
          }).observe({ entryTypes: ['largest-contentful-paint'] });
      
          // FID
          new PerformanceObserver((list) => {
            vitals.FID = list.getEntries()[0].processingStart -
                         list.getEntries()[0].startTime;
          }).observe({ entryTypes: ['first-input'] });
      
          // CLS
          new PerformanceObserver((list) => {
            list.getEntries().forEach((entry) => {
              if (!entry.hadRecentInput) {
                vitals.CLS += entry.value;
              }
            });
          }).observe({ entryTypes: ['layout-shift'] });
      
          // Wait 5 seconds for metrics
          setTimeout(() => resolve(vitals), 5000);
        });
      });
      
      console.log('Core Web Vitals:', vitals);
      ```
      
      ### Other Important Metrics
      
      **TTFB (Time to First Byte)**
      ```javascript
      const ttfb = await page.evaluate(() => {
        const [navigationEntry] = performance.getEntriesByType('navigation');
        return navigationEntry.responseStart - navigationEntry.requestStart;
      });
      ```
      
      **FCP (First Contentful Paint)**
      ```javascript
      const fcp = await page.evaluate(() => {
        const paintEntries = performance.getEntriesByType('paint');
        const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
        return fcpEntry ? fcpEntry.startTime : null;
      });
      ```
      
      **TTI (Time to Interactive)**
      ```javascript
      // Requires lighthouse or manual calculation
      const tti = await page.evaluate(() => {
        // Complex calculation based on network idle and long tasks
        // Best to use Lighthouse for accurate TTI
      });
      ```
      
      ---
      
      ## Performance Tracing
      
      ### Chrome Trace Categories
      
      **Loading:**
      - Page load events
      - Resource loading
      - Parser activity
      
      **Rendering:**
      - Layout calculations
      - Paint operations
      - Compositing
      
      **Scripting:**
      - JavaScript execution
      - V8 compilation
      - Garbage collection
      
      **Network:**
      - HTTP requests
      - WebSocket traffic
      - Resource fetching
      
      **Input:**
      - User input processing
      - Touch/scroll events
      
      **GPU:**
      - GPU operations
      - Compositing work
      
      ### Record Performance Trace
      
      **Using chrome-devtools-mcp:**
      ```javascript
      // Start trace with specific categories
      await useTool('performance_start_trace', {
        categories: ['loading', 'rendering', 'scripting', 'network']
      });
      
      // Perform actions
      await useTool('navigate_page', { url: 'https://example.com' });
      await useTool('wait_for', { waitUntil: 'networkidle' });
      
      // Optional: Interact with page
      await useTool('click', { uid: 'button-uid' });
      
      // Stop trace
      const traceData = await useTool('performance_stop_trace');
      
      // Analyze trace
      const insights = await useTool('performance_analyze_insight');
      ```
      
      **Using Puppeteer:**
      ```javascript
      // Start tracing
      await page.tracing.start({
        path: 'trace.json',
        categories: [
          'devtools.timeline',
          'disabled-by-default-devtools.timeline',
          'disabled-by-default-v8.cpu_profiler'
        ]
      });
      
      // Navigate
      await page.goto('https://example.com', {
        waitUntil: 'networkidle2'
      });
      
      // Stop tracing
      await page.tracing.stop();
      
      // Analyze in Chrome DevTools (chrome://tracing)
      ```
      
      ### Analyze Trace Data
      
      **Key Metrics from Trace:**
      
      1. **Main Thread Activity**
         - JavaScript execution time
         - Layout/reflow time
         - Paint time
         - Long tasks (> 50ms)
      
      2. **Network Waterfall**
         - Request start times
         - DNS lookup
         - Connection time
         - Download time
      
      3. **Rendering Pipeline**
         - DOM construction
         - Style calculation
         - Layout
         - Paint
         - Composite
      
      **Common Issues to Look For:**
      - Long tasks blocking main thread
      - Excessive JavaScript execution
      - Layout thrashing
      - Unnecessary repaints
      - Slow network requests
      - Large bundle sizes
      
      ---
      
      ## Network Analysis
      
      ### Monitor Network Requests
      
      **Using chrome-devtools-mcp:**
      ```javascript
      // Navigate to page
      await useTool('navigate_page', { url: 'https://example.com' });
      
      // Wait for all requests
      await useTool('wait_for', { waitUntil: 'networkidle' });
      
      // List all requests
      const requests = await useTool('list_network_requests', {
        resourceTypes: ['Document', 'Script', 'Stylesheet', 'Image', 'XHR', 'Fetch'],
        pageSize: 100
      });
      
      // Analyze specific request
      for (const req of requests.requests) {
        const details = await useTool('get_network_request', {
          requestId: req.id
        });
      
        console.log({
          url: details.url,
          method: details.method,
          status: details.status,
          size: details.encodedDataLength,
          time: details.timing.receiveHeadersEnd - details.timing.requestTime,
          cached: details.fromCache
        });
      }
      ```
      
      **Using Puppeteer:**
      ```javascript
      const requests = [];
      
      // Capture all requests
      page.on('request', (request) => {
        requests.push({
          url: request.url(),
          method: request.method(),
          resourceType: request.resourceType(),
          headers: request.headers()
        });
      });
      
      // Capture responses
      page.on('response', (response) => {
        const request = response.request();
        console.log({
          url: response.url(),
          status: response.status(),
          size: response.headers()['content-length'],
          cached: response.fromCache(),
          timing: response.timing()
        });
      });
      
      await page.goto('https://example.com');
      ```
      
      ### Network Performance Metrics
      
      **Calculate Total Page Weight:**
      ```javascript
      let totalBytes = 0;
      let resourceCounts = {};
      
      page.on('response', async (response) => {
        const type = response.request().resourceType();
        const buffer = await response.buffer();
      
        totalBytes += buffer.length;
        resourceCounts[type] = (resourceCounts[type] || 0) + 1;
      });
      
      await page.goto('https://example.com');
      
      console.log('Total size:', (totalBytes / 1024 / 1024).toFixed(2), 'MB');
      console.log('Resources:', resourceCounts);
      ```
      
      **Identify Slow Requests:**
      ```javascript
      page.on('response', (response) => {
        const timing = response.timing();
        const totalTime = timing.receiveHeadersEnd - timing.requestTime;
      
        if (totalTime > 1000) { // Slower than 1 second
          console.log('Slow request:', {
            url: response.url(),
            time: totalTime.toFixed(2) + 'ms',
            size: response.headers()['content-length']
          });
        }
      });
      ```
      
      ### Network Throttling
      
      **Simulate Slow Connection:**
      ```javascript
      // Using chrome-devtools-mcp
      await useTool('emulate_network', {
        throttlingOption: 'Slow 3G'  // or 'Fast 3G', 'Slow 4G'
      });
      
      // Using Puppeteer
      const client = await page.createCDPSession();
      await client.send('Network.emulateNetworkConditions', {
        offline: false,
        downloadThroughput: 400 * 1024 / 8,  // 400 Kbps
        uploadThroughput: 400 * 1024 / 8,
        latency: 2000  // 2000ms RTT
      });
      ```
      
      ---
      
      ## JavaScript Performance
      
      ### Identify Long Tasks
      
      **Using Performance Observer:**
      ```javascript
      await page.evaluate(() => {
        return new Promise((resolve) => {
          const longTasks = [];
      
          const observer = new PerformanceObserver((list) => {
            list.getEntries().forEach((entry) => {
              longTasks.push({
                name: entry.name,
                duration: entry.duration,
                startTime: entry.startTime
              });
            });
          });
      
          observer.observe({ entryTypes: ['longtask'] });
      
          // Collect for 10 seconds
          setTimeout(() => {
            observer.disconnect();
            resolve(longTasks);
          }, 10000);
        });
      });
      ```
      
      ### CPU Profiling
      
      **Using Puppeteer:**
      ```javascript
      // Start CPU profiling
      const client = await page.createCDPSession();
      await client.send('Profiler.enable');
      await client.send('Profiler.start');
      
      // Navigate and interact
      await page.goto('https://example.com');
      await page.click('.button');
      
      // Stop profiling
      const { profile } = await client.send('Profiler.stop');
      
      // Analyze profile (flame graph data)
      // Import into Chrome DevTools for visualization
      ```
      
      ### JavaScript Coverage
      
      **Identify Unused Code:**
      ```javascript
      // Start coverage
      await Promise.all([
        page.coverage.startJSCoverage(),
        page.coverage.startCSSCoverage()
      ]);
      
      // Navigate
      await page.goto('https://example.com');
      
      // Stop coverage
      const [jsCoverage, cssCoverage] = await Promise.all([
        page.coverage.stopJSCoverage(),
        page.coverage.stopCSSCoverage()
      ]);
      
      // Calculate unused bytes
      function calculateUnusedBytes(coverage) {
        let usedBytes = 0;
        let totalBytes = 0;
      
        for (const entry of coverage) {
          totalBytes += entry.text.length;
          for (const range of entry.ranges) {
            usedBytes += range.end - range.start - 1;
          }
        }
      
        return {
          usedBytes,
          totalBytes,
          unusedBytes: totalBytes - usedBytes,
          unusedPercentage: ((totalBytes - usedBytes) / totalBytes * 100).toFixed(2)
        };
      }
      
      console.log('JS Coverage:', calculateUnusedBytes(jsCoverage));
      console.log('CSS Coverage:', calculateUnusedBytes(cssCoverage));
      ```
      
      ### Bundle Size Analysis
      
      **Analyze JavaScript Bundles:**
      ```javascript
      page.on('response', async (response) => {
        const url = response.url();
        const type = response.request().resourceType();
      
        if (type === 'script') {
          const buffer = await response.buffer();
          const size = buffer.length;
      
          console.log({
            url: url.split('/').pop(),
            size: (size / 1024).toFixed(2) + ' KB',
            gzipped: response.headers()['content-encoding'] === 'gzip'
          });
        }
      });
      ```
      
      ---
      
      ## Rendering Performance
      
      ### Layout Thrashing Detection
      
      **Monitor Layout Recalculations:**
      ```javascript
      // Using Performance Observer
      await page.evaluate(() => {
        return new Promise((resolve) => {
          const measurements = [];
      
          const observer = new PerformanceObserver((list) => {
            list.getEntries().forEach((entry) => {
              if (entry.entryType === 'measure' &&
                  entry.name.includes('layout')) {
                measurements.push({
                  name: entry.name,
                  duration: entry.duration,
                  startTime: entry.startTime
                });
              }
            });
          });
      
          observer.observe({ entryTypes: ['measure'] });
      
          setTimeout(() => {
            observer.disconnect();
            resolve(measurements);
          }, 5000);
        });
      });
      ```
      
      ### Paint and Composite Metrics
      
      **Get Paint Metrics:**
      ```javascript
      const paintMetrics = await page.evaluate(() => {
        const paints = performance.getEntriesByType('paint');
        return {
          firstPaint: paints.find(p => p.name === 'first-paint')?.startTime,
          firstContentfulPaint: paints.find(p => p.name === 'first-contentful-paint')?.startTime
        };
      });
      ```
      
      ### Frame Rate Analysis
      
      **Monitor FPS:**
      ```javascript
      await page.evaluate(() => {
        return new Promise((resolve) => {
          let frames = 0;
          let lastTime = performance.now();
      
          function countFrames() {
            frames++;
            requestAnimationFrame(countFrames);
          }
      
          countFrames();
      
          setTimeout(() => {
            const now = performance.now();
            const elapsed = (now - lastTime) / 1000;
            const fps = frames / elapsed;
            resolve(fps);
          }, 5000);
        });
      });
      ```
      
      ### Layout Shifts (CLS)
      
      **Track Individual Shifts:**
      ```javascript
      await page.evaluate(() => {
        return new Promise((resolve) => {
          const shifts = [];
          let totalCLS = 0;
      
          const observer = new PerformanceObserver((list) => {
            list.getEntries().forEach((entry) => {
              if (!entry.hadRecentInput) {
                totalCLS += entry.value;
                shifts.push({
                  value: entry.value,
                  time: entry.startTime,
                  elements: entry.sources?.map(s => s.node)
                });
              }
            });
          });
      
          observer.observe({ entryTypes: ['layout-shift'] });
      
          setTimeout(() => {
            observer.disconnect();
            resolve({ totalCLS, shifts });
          }, 10000);
        });
      });
      ```
      
      ---
      
      ## Memory Analysis
      
      ### Memory Metrics
      
      **Get Memory Usage:**
      ```javascript
      // Using chrome-devtools-mcp
      await useTool('evaluate_script', {
        expression: `
          ({
            usedJSHeapSize: performance.memory?.usedJSHeapSize,
            totalJSHeapSize: performance.memory?.totalJSHeapSize,
            jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit
          })
        `,
        returnByValue: true
      });
      
      // Using Puppeteer
      const metrics = await page.metrics();
      console.log({
        jsHeapUsed: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2) + ' MB',
        jsHeapTotal: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) + ' MB',
        domNodes: metrics.Nodes,
        documents: metrics.Documents,
        jsEventListeners: metrics.JSEventListeners
      });
      ```
      
      ### Memory Leak Detection
      
      **Monitor Memory Over Time:**
      ```javascript
      async function detectMemoryLeak(page, duration = 30000) {
        const samples = [];
        const interval = 1000; // Sample every second
        const samples_count = duration / interval;
      
        for (let i = 0; i < samples_count; i++) {
          const metrics = await page.metrics();
          samples.push({
            time: i,
            heapUsed: metrics.JSHeapUsedSize
          });
      
          await page.waitForTimeout(interval);
        }
      
        // Analyze trend
        const firstSample = samples[0].heapUsed;
        const lastSample = samples[samples.length - 1].heapUsed;
        const increase = ((lastSample - firstSample) / firstSample * 100).toFixed(2);
      
        return {
          samples,
          memoryIncrease: increase + '%',
          possibleLeak: increase > 50 // > 50% increase indicates possible leak
        };
      }
      
      const leakAnalysis = await detectMemoryLeak(page, 30000);
      console.log('Memory Analysis:', leakAnalysis);
      ```
      
      ### Heap Snapshot
      
      **Capture Heap Snapshot:**
      ```javascript
      const client = await page.createCDPSession();
      
      // Take snapshot
      await client.send('HeapProfiler.enable');
      const { result } = await client.send('HeapProfiler.takeHeapSnapshot');
      
      // Snapshot is streamed in chunks
      // Save to file or analyze programmatically
      ```
      
      ---
      
      ## Optimization Strategies
      
      ### Image Optimization
      
      **Detect Unoptimized Images:**
      ```javascript
      const images = await page.evaluate(() => {
        const images = Array.from(document.querySelectorAll('img'));
        return images.map(img => ({
          src: img.src,
          naturalWidth: img.naturalWidth,
          naturalHeight: img.naturalHeight,
          displayWidth: img.width,
          displayHeight: img.height,
          oversized: img.naturalWidth > img.width * 1.5 ||
                     img.naturalHeight > img.height * 1.5
        }));
      });
      
      const oversizedImages = images.filter(img => img.oversized);
      console.log('Oversized images:', oversizedImages);
      ```
      
      ### Font Loading
      
      **Detect Render-Blocking Fonts:**
      ```javascript
      const fonts = await page.evaluate(() => {
        return Array.from(document.fonts).map(font => ({
          family: font.family,
          weight: font.weight,
          style: font.style,
          status: font.status,
          loaded: font.status === 'loaded'
        }));
      });
      
      console.log('Fonts:', fonts);
      ```
      
      ### Third-Party Scripts
      
      **Measure Third-Party Impact:**
      ```javascript
      const thirdPartyDomains = ['googletagmanager.com', 'facebook.net', 'doubleclick.net'];
      
      page.on('response', async (response) => {
        const url = response.url();
        const isThirdParty = thirdPartyDomains.some(domain => url.includes(domain));
      
        if (isThirdParty) {
          const buffer = await response.buffer();
          console.log({
            url: url,
            size: (buffer.length / 1024).toFixed(2) + ' KB',
            type: response.request().resourceType()
          });
        }
      });
      ```
      
      ### Critical Rendering Path
      
      **Identify Render-Blocking Resources:**
      ```javascript
      await page.goto('https://example.com');
      
      const renderBlockingResources = await page.evaluate(() => {
        const resources = performance.getEntriesByType('resource');
        return resources.filter(resource => {
          return (resource.initiatorType === 'link' &&
                  resource.name.includes('.css')) ||
                 (resource.initiatorType === 'script' &&
                  !resource.name.includes('async'));
        }).map(r => ({
          url: r.name,
          duration: r.duration,
          startTime: r.startTime
        }));
      });
      
      console.log('Render-blocking resources:', renderBlockingResources);
      ```
      
      ### Lighthouse Integration
      
      **Run Lighthouse Audit:**
      ```javascript
      import lighthouse from 'lighthouse';
      import { launch } from 'chrome-launcher';
      
      // Launch Chrome
      const chrome = await launch({ chromeFlags: ['--headless'] });
      
      // Run Lighthouse
      const { lhr } = await lighthouse('https://example.com', {
        port: chrome.port,
        onlyCategories: ['performance']
      });
      
      // Get scores
      console.log({
        performanceScore: lhr.categories.performance.score * 100,
        metrics: {
          FCP: lhr.audits['first-contentful-paint'].displayValue,
          LCP: lhr.audits['largest-contentful-paint'].displayValue,
          TBT: lhr.audits['total-blocking-time'].displayValue,
          CLS: lhr.audits['cumulative-layout-shift'].displayValue,
          SI: lhr.audits['speed-index'].displayValue
        },
        opportunities: lhr.audits['opportunities']
      });
      
      await chrome.kill();
      ```
      
      ---
      
      ## Performance Budgets
      
      ### Set Performance Budgets
      
      ```javascript
      const budgets = {
        // Core Web Vitals
        LCP: 2500,        // ms
        FID: 100,         // ms
        CLS: 0.1,         // score
      
        // Other metrics
        FCP: 1800,        // ms
        TTI: 3800,        // ms
        TBT: 300,         // ms
      
        // Resource budgets
        totalPageSize: 2 * 1024 * 1024,  // 2 MB
        jsSize: 500 * 1024,               // 500 KB
        cssSize: 100 * 1024,              // 100 KB
        imageSize: 1 * 1024 * 1024,       // 1 MB
      
        // Request counts
        totalRequests: 50,
        jsRequests: 10,
        cssRequests: 5
      };
      
      async function checkBudgets(page, budgets) {
        // Measure actual values
        const vitals = await measureCoreWebVitals(page);
        const resources = await analyzeResources(page);
      
        // Compare against budgets
        const violations = [];
      
        if (vitals.LCP > budgets.LCP) {
          violations.push(`LCP: ${vitals.LCP}ms exceeds budget of ${budgets.LCP}ms`);
        }
      
        if (resources.totalSize > budgets.totalPageSize) {
          violations.push(`Page size: ${resources.totalSize} exceeds budget of ${budgets.totalPageSize}`);
        }
      
        // ... check other budgets
      
        return {
          passed: violations.length === 0,
          violations
        };
      }
      ```
      
      ---
      
      ## Automated Performance Testing
      
      ### CI/CD Integration
      
      ```javascript
      // performance-test.js
      import puppeteer from 'puppeteer';
      
      async function performanceTest(url) {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();
      
        // Measure metrics
        await page.goto(url, { waitUntil: 'networkidle2' });
        const metrics = await page.metrics();
        const vitals = await measureCoreWebVitals(page);
      
        await browser.close();
      
        // Check against thresholds
        const thresholds = {
          LCP: 2500,
          FID: 100,
          CLS: 0.1,
          jsHeapSize: 50 * 1024 * 1024  // 50 MB
        };
      
        const failed = [];
        if (vitals.LCP > thresholds.LCP) failed.push('LCP');
        if (vitals.FID > thresholds.FID) failed.push('FID');
        if (vitals.CLS > thresholds.CLS) failed.push('CLS');
        if (metrics.JSHeapUsedSize > thresholds.jsHeapSize) failed.push('Memory');
      
        if (failed.length > 0) {
          console.error('Performance test failed:', failed);
          process.exit(1);
        }
      
        console.log('Performance test passed');
      }
      
      performanceTest(process.env.TEST_URL);
      ```
      
      ---
      
      ## Best Practices
      
      ### Performance Testing Checklist
      
      1. **Measure Multiple Times**
         - Run tests 3-5 times
         - Use median values
         - Account for variance
      
      2. **Test Different Conditions**
         - Fast 3G
         - Slow 3G
         - Offline
         - CPU throttling
      
      3. **Test Different Devices**
         - Mobile (low-end)
         - Mobile (high-end)
         - Desktop
         - Tablet
      
      4. **Monitor Over Time**
         - Track metrics in CI/CD
         - Set up alerts for regressions
         - Create performance dashboards
      
      5. **Focus on User Experience**
         - Prioritize Core Web Vitals
         - Test real user journeys
         - Consider perceived performance
      
      6. **Optimize Critical Path**
         - Minimize render-blocking resources
         - Defer non-critical JavaScript
         - Optimize font loading
         - Lazy load images
      
      ---
      
      ## Resources
      
      - [Web.dev Performance](https://web.dev/performance/)
      - [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/)
      - [Core Web Vitals](https://web.dev/vitals/)
      - [Lighthouse](https://developer.chrome.com/docs/lighthouse/)
      - [WebPageTest](https://www.webpagetest.org/)
      
    • puppeteer-reference.md 18.6 KB
      # Puppeteer Quick Reference
      
      Complete guide to browser automation with Puppeteer - a high-level API over Chrome DevTools Protocol.
      
      ## Table of Contents
      
      - [Setup](#setup)
      - [Browser & Page Management](#browser--page-management)
      - [Navigation](#navigation)
      - [Element Interaction](#element-interaction)
      - [JavaScript Execution](#javascript-execution)
      - [Screenshots & PDFs](#screenshots--pdfs)
      - [Network Interception](#network-interception)
      - [Device Emulation](#device-emulation)
      - [Performance](#performance)
      - [Common Patterns](#common-patterns)
      
      ---
      
      ## Setup
      
      ### Installation
      
      ```bash
      # Install Puppeteer
      npm install puppeteer
      
      # Install core only (bring your own Chrome)
      npm install puppeteer-core
      ```
      
      ### Basic Usage
      
      ```javascript
      import puppeteer from 'puppeteer';
      
      // Launch browser
      const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox']
      });
      
      // Open page
      const page = await browser.newPage();
      
      // Navigate
      await page.goto('https://example.com');
      
      // Do work...
      
      // Cleanup
      await browser.close();
      ```
      
      ---
      
      ## Browser & Page Management
      
      ### Launch Browser
      
      ```javascript
      const browser = await puppeteer.launch({
        // Visibility
        headless: false,              // Show browser UI
        headless: 'new',              // New headless mode (Chrome 112+)
      
        // Chrome location
        executablePath: '/path/to/chrome',
        channel: 'chrome',            // or 'chrome-canary', 'chrome-beta'
      
        // Browser context
        userDataDir: './user-data',   // Persistent profile
      
        // Window size
        defaultViewport: {
          width: 1920,
          height: 1080,
          deviceScaleFactor: 1,
          isMobile: false
        },
      
        // Advanced options
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox',
          '--disable-dev-shm-usage',
          '--disable-web-security',
          '--disable-features=IsolateOrigins',
          '--disable-site-isolation-trials',
          '--start-maximized'
        ],
      
        // Debugging
        devtools: true,               // Open DevTools automatically
        slowMo: 250,                  // Slow down by 250ms per action
      
        // Network
        proxy: {
          server: 'http://proxy.com:8080'
        }
      });
      ```
      
      ### Connect to Running Browser
      
      ```javascript
      // Launch Chrome with debugging
      // google-chrome --remote-debugging-port=9222
      
      const browser = await puppeteer.connect({
        browserURL: 'http://localhost:9222',
        // or browserWSEndpoint: 'ws://localhost:9222/devtools/browser/...'
      });
      ```
      
      ### Page Management
      
      ```javascript
      // Create new page
      const page = await browser.newPage();
      
      // Get all pages
      const pages = await browser.pages();
      
      // Close page
      await page.close();
      
      // Multiple pages
      const page1 = await browser.newPage();
      const page2 = await browser.newPage();
      
      // Switch between pages
      await page1.bringToFront();
      ```
      
      ### Browser Context (Incognito)
      
      ```javascript
      // Create isolated context
      const context = await browser.createBrowserContext();
      const page = await context.newPage();
      
      // Cleanup context
      await context.close();
      ```
      
      ---
      
      ## Navigation
      
      ### Basic Navigation
      
      ```javascript
      // Navigate to URL
      await page.goto('https://example.com');
      
      // Navigate with options
      await page.goto('https://example.com', {
        waitUntil: 'networkidle2',    // or 'load', 'domcontentloaded', 'networkidle0'
        timeout: 30000                 // Max wait time (ms)
      });
      
      // Reload page
      await page.reload({ waitUntil: 'networkidle2' });
      
      // Navigation history
      await page.goBack();
      await page.goForward();
      
      // Wait for navigation
      await page.waitForNavigation({
        waitUntil: 'networkidle2'
      });
      ```
      
      ### Wait Until Options
      
      - `load` - Wait for load event
      - `domcontentloaded` - Wait for DOMContentLoaded event
      - `networkidle0` - Wait until no network connections for 500ms
      - `networkidle2` - Wait until max 2 network connections for 500ms
      
      ---
      
      ## Element Interaction
      
      ### Selectors
      
      ```javascript
      // CSS selectors
      await page.$('#id');
      await page.$('.class');
      await page.$('div > p');
      
      // XPath
      await page.$x('//button[text()="Submit"]');
      
      // Get all matching elements
      await page.$$('.item');
      await page.$$x('//div[@class="item"]');
      ```
      
      ### Click Elements
      
      ```javascript
      // Click by selector
      await page.click('.button');
      
      // Click with options
      await page.click('.button', {
        button: 'left',           // or 'right', 'middle'
        clickCount: 1,            // 2 for double-click
        delay: 100                // Delay between mousedown and mouseup
      });
      
      // ElementHandle click
      const button = await page.$('.button');
      await button.click();
      ```
      
      ### Type Text
      
      ```javascript
      // Type into input
      await page.type('#search', 'query text');
      
      // Type with delay
      await page.type('#search', 'slow typing', { delay: 100 });
      
      // Clear and type
      await page.$eval('#search', el => el.value = '');
      await page.type('#search', 'new text');
      ```
      
      ### Form Interaction
      
      ```javascript
      // Fill input
      await page.type('#username', 'john@example.com');
      await page.type('#password', 'secret123');
      
      // Select dropdown option
      await page.select('#country', 'US');           // By value
      await page.select('#country', 'USA', 'UK');    // Multiple
      
      // Check/uncheck checkbox
      await page.click('input[type="checkbox"]');
      
      // Choose radio button
      await page.click('input[value="option2"]');
      
      // Upload file
      const input = await page.$('input[type="file"]');
      await input.uploadFile('/path/to/file.pdf');
      
      // Submit form
      await page.click('button[type="submit"]');
      await page.waitForNavigation();
      ```
      
      ### Hover & Focus
      
      ```javascript
      // Hover over element
      await page.hover('.menu-item');
      
      // Focus element
      await page.focus('#input');
      
      // Blur
      await page.$eval('#input', el => el.blur());
      ```
      
      ### Drag & Drop
      
      ```javascript
      const source = await page.$('.draggable');
      const target = await page.$('.drop-zone');
      
      await source.drag(target);
      await source.drop(target);
      ```
      
      ---
      
      ## JavaScript Execution
      
      ### Evaluate in Page Context
      
      ```javascript
      // Execute JavaScript
      const title = await page.evaluate(() => document.title);
      
      // With arguments
      const text = await page.evaluate(
        (selector) => document.querySelector(selector).textContent,
        '.heading'
      );
      
      // Return complex data
      const data = await page.evaluate(() => ({
        title: document.title,
        url: location.href,
        cookies: document.cookie
      }));
      
      // With ElementHandle
      const element = await page.$('.button');
      const text = await page.evaluate(el => el.textContent, element);
      ```
      
      ### Query & Modify DOM
      
      ```javascript
      // Get element property
      const value = await page.$eval('#input', el => el.value);
      
      // Get multiple elements
      const items = await page.$$eval('.item', elements =>
        elements.map(el => el.textContent)
      );
      
      // Modify element
      await page.$eval('#input', (el, value) => {
        el.value = value;
      }, 'new value');
      
      // Add class
      await page.$eval('.element', el => el.classList.add('active'));
      ```
      
      ### Expose Functions
      
      ```javascript
      // Expose Node.js function to page
      await page.exposeFunction('md5', (text) =>
        crypto.createHash('md5').update(text).digest('hex')
      );
      
      // Call from page context
      const hash = await page.evaluate(async () => {
        return await window.md5('hello world');
      });
      ```
      
      ---
      
      ## Screenshots & PDFs
      
      ### Screenshots
      
      ```javascript
      // Full page screenshot
      await page.screenshot({
        path: 'screenshot.png',
        fullPage: true
      });
      
      // Viewport screenshot
      await page.screenshot({
        path: 'viewport.png',
        fullPage: false
      });
      
      // Element screenshot
      const element = await page.$('.chart');
      await element.screenshot({
        path: 'chart.png'
      });
      
      // Screenshot options
      await page.screenshot({
        path: 'page.png',
        type: 'png',              // or 'jpeg', 'webp'
        quality: 80,              // JPEG quality (0-100)
        clip: {                   // Crop region
          x: 0,
          y: 0,
          width: 500,
          height: 500
        },
        omitBackground: true      // Transparent background
      });
      
      // Screenshot to buffer
      const buffer = await page.screenshot();
      ```
      
      ### PDF Generation
      
      ```javascript
      // Generate PDF
      await page.pdf({
        path: 'page.pdf',
        format: 'A4',             // or 'Letter', 'Legal', etc.
        printBackground: true,
        margin: {
          top: '1cm',
          right: '1cm',
          bottom: '1cm',
          left: '1cm'
        }
      });
      
      // Custom page size
      await page.pdf({
        path: 'custom.pdf',
        width: '8.5in',
        height: '11in',
        landscape: true
      });
      
      // Header and footer
      await page.pdf({
        path: 'report.pdf',
        displayHeaderFooter: true,
        headerTemplate: '<div style="font-size:10px;">Header</div>',
        footerTemplate: '<div style="font-size:10px;">Page <span class="pageNumber"></span></div>'
      });
      ```
      
      ---
      
      ## Network Interception
      
      ### Request Interception
      
      ```javascript
      // Enable request interception
      await page.setRequestInterception(true);
      
      // Intercept requests
      page.on('request', (request) => {
        // Block specific resource types
        if (request.resourceType() === 'image') {
          request.abort();
        }
        // Block URLs
        else if (request.url().includes('ads')) {
          request.abort();
        }
        // Modify request
        else if (request.url().includes('api')) {
          request.continue({
            headers: {
              ...request.headers(),
              'Authorization': 'Bearer token'
            }
          });
        }
        // Continue normally
        else {
          request.continue();
        }
      });
      ```
      
      ### Mock Responses
      
      ```javascript
      await page.setRequestInterception(true);
      
      page.on('request', (request) => {
        if (request.url().includes('/api/user')) {
          request.respond({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
              id: 1,
              name: 'Mock User'
            })
          });
        } else {
          request.continue();
        }
      });
      ```
      
      ### Monitor Network
      
      ```javascript
      // Track requests
      page.on('request', (request) => {
        console.log('Request:', request.method(), request.url());
      });
      
      // Track responses
      page.on('response', (response) => {
        console.log('Response:', response.status(), response.url());
      });
      
      // Track failed requests
      page.on('requestfailed', (request) => {
        console.log('Failed:', request.failure().errorText, request.url());
      });
      
      // Get response body
      page.on('response', async (response) => {
        if (response.url().includes('/api/data')) {
          const json = await response.json();
          console.log('API Data:', json);
        }
      });
      ```
      
      ---
      
      ## Device Emulation
      
      ### Predefined Devices
      
      ```javascript
      import { devices } from 'puppeteer';
      
      // Emulate iPhone
      const iPhone = devices['iPhone 13 Pro'];
      await page.emulate(iPhone);
      
      // Common devices
      const iPad = devices['iPad Pro'];
      const pixel = devices['Pixel 5'];
      const galaxy = devices['Galaxy S9+'];
      
      // Navigate after emulation
      await page.goto('https://example.com');
      ```
      
      ### Custom Device
      
      ```javascript
      await page.emulate({
        viewport: {
          width: 375,
          height: 812,
          deviceScaleFactor: 3,
          isMobile: true,
          hasTouch: true,
          isLandscape: false
        },
        userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)...'
      });
      ```
      
      ### Viewport Only
      
      ```javascript
      await page.setViewport({
        width: 1920,
        height: 1080,
        deviceScaleFactor: 1
      });
      ```
      
      ### Geolocation
      
      ```javascript
      // Set geolocation
      await page.setGeolocation({
        latitude: 37.7749,
        longitude: -122.4194,
        accuracy: 100
      });
      
      // Grant permissions
      const context = browser.defaultBrowserContext();
      await context.overridePermissions('https://example.com', ['geolocation']);
      ```
      
      ### Timezone & Locale
      
      ```javascript
      // Set timezone
      await page.emulateTimezone('America/New_York');
      
      // Set locale
      await page.emulateMediaType('screen');
      await page.evaluateOnNewDocument(() => {
        Object.defineProperty(navigator, 'language', {
          get: () => 'en-US'
        });
      });
      ```
      
      ---
      
      ## Performance
      
      ### CPU & Network Throttling
      
      ```javascript
      // CPU throttling
      const client = await page.createCDPSession();
      await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
      
      // Network throttling
      await page.emulateNetworkConditions({
        offline: false,
        downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
        uploadThroughput: 750 * 1024 / 8,           // 750 Kbps
        latency: 40                                  // 40ms RTT
      });
      
      // Predefined profiles
      await page.emulateNetworkConditions(
        puppeteer.networkConditions['Fast 3G']
      );
      
      // Disable throttling
      await page.emulateNetworkConditions({
        offline: false,
        downloadThroughput: -1,
        uploadThroughput: -1,
        latency: 0
      });
      ```
      
      ### Performance Metrics
      
      ```javascript
      // Get metrics
      const metrics = await page.metrics();
      console.log(metrics);
      // {
      //   Timestamp, Documents, Frames, JSEventListeners,
      //   Nodes, LayoutCount, RecalcStyleCount,
      //   LayoutDuration, RecalcStyleDuration,
      //   ScriptDuration, TaskDuration,
      //   JSHeapUsedSize, JSHeapTotalSize
      // }
      ```
      
      ### Performance Tracing
      
      ```javascript
      // Start tracing
      await page.tracing.start({
        path: 'trace.json',
        categories: [
          'devtools.timeline',
          'disabled-by-default-devtools.timeline'
        ]
      });
      
      // Navigate
      await page.goto('https://example.com');
      
      // Stop tracing
      await page.tracing.stop();
      
      // Analyze trace in chrome://tracing
      ```
      
      ### Coverage (Code Usage)
      
      ```javascript
      // Start JS coverage
      await page.coverage.startJSCoverage();
      
      // Start CSS coverage
      await page.coverage.startCSSCoverage();
      
      // Navigate
      await page.goto('https://example.com');
      
      // Stop and get coverage
      const jsCoverage = await page.coverage.stopJSCoverage();
      const cssCoverage = await page.coverage.stopCSSCoverage();
      
      // Calculate unused bytes
      let totalBytes = 0;
      let usedBytes = 0;
      for (const entry of [...jsCoverage, ...cssCoverage]) {
        totalBytes += entry.text.length;
        for (const range of entry.ranges) {
          usedBytes += range.end - range.start - 1;
        }
      }
      
      console.log(`Used: ${usedBytes / totalBytes * 100}%`);
      ```
      
      ---
      
      ## Common Patterns
      
      ### Wait for Elements
      
      ```javascript
      // Wait for selector
      await page.waitForSelector('.element', {
        visible: true,
        timeout: 5000
      });
      
      // Wait for XPath
      await page.waitForXPath('//button[text()="Submit"]');
      
      // Wait for function
      await page.waitForFunction(
        () => document.querySelector('.loading') === null,
        { timeout: 10000 }
      );
      
      // Wait for timeout
      await page.waitForTimeout(2000);
      ```
      
      ### Handle Dialogs
      
      ```javascript
      // Alert, confirm, prompt
      page.on('dialog', async (dialog) => {
        console.log(dialog.type(), dialog.message());
      
        // Accept
        await dialog.accept();
        // or reject
        // await dialog.dismiss();
        // or provide input for prompt
        // await dialog.accept('input text');
      });
      ```
      
      ### Handle Downloads
      
      ```javascript
      // Set download path
      const client = await page.createCDPSession();
      await client.send('Page.setDownloadBehavior', {
        behavior: 'allow',
        downloadPath: '/path/to/downloads'
      });
      
      // Trigger download
      await page.click('a[download]');
      ```
      
      ### Multiple Pages (Tabs)
      
      ```javascript
      // Listen for new pages
      browser.on('targetcreated', async (target) => {
        if (target.type() === 'page') {
          const newPage = await target.page();
          console.log('New page opened:', newPage.url());
        }
      });
      
      // Click link that opens new tab
      const [newPage] = await Promise.all([
        new Promise(resolve => browser.once('targetcreated', target => resolve(target.page()))),
        page.click('a[target="_blank"]')
      ]);
      
      console.log('New page URL:', newPage.url());
      ```
      
      ### Frames (iframes)
      
      ```javascript
      // Get all frames
      const frames = page.frames();
      
      // Find frame by name
      const frame = page.frames().find(f => f.name() === 'myframe');
      
      // Find frame by URL
      const frame = page.frames().find(f => f.url().includes('example.com'));
      
      // Main frame
      const mainFrame = page.mainFrame();
      
      // Interact with frame
      await frame.click('.button');
      await frame.type('#input', 'text');
      ```
      
      ### Infinite Scroll
      
      ```javascript
      async function autoScroll(page) {
        await page.evaluate(async () => {
          await new Promise((resolve) => {
            let totalHeight = 0;
            const distance = 100;
            const timer = setInterval(() => {
              const scrollHeight = document.body.scrollHeight;
              window.scrollBy(0, distance);
              totalHeight += distance;
      
              if (totalHeight >= scrollHeight) {
                clearInterval(timer);
                resolve();
              }
            }, 100);
          });
        });
      }
      
      await autoScroll(page);
      ```
      
      ### Cookies
      
      ```javascript
      // Get cookies
      const cookies = await page.cookies();
      
      // Set cookies
      await page.setCookie({
        name: 'session',
        value: 'abc123',
        domain: 'example.com',
        path: '/',
        httpOnly: true,
        secure: true,
        sameSite: 'Strict'
      });
      
      // Delete cookies
      await page.deleteCookie({ name: 'session' });
      ```
      
      ### Local Storage
      
      ```javascript
      // Set localStorage
      await page.evaluate(() => {
        localStorage.setItem('key', 'value');
      });
      
      // Get localStorage
      const value = await page.evaluate(() => {
        return localStorage.getItem('key');
      });
      
      // Clear localStorage
      await page.evaluate(() => localStorage.clear());
      ```
      
      ### Error Handling
      
      ```javascript
      try {
        await page.goto('https://example.com', {
          waitUntil: 'networkidle2',
          timeout: 30000
        });
      } catch (error) {
        if (error.name === 'TimeoutError') {
          console.error('Page load timeout');
        } else {
          console.error('Navigation failed:', error);
        }
      
        // Take screenshot on error
        await page.screenshot({ path: 'error.png' });
      }
      ```
      
      ### Stealth Mode (Avoid Detection)
      
      ```javascript
      // Hide automation indicators
      await page.evaluateOnNewDocument(() => {
        // Override navigator.webdriver
        Object.defineProperty(navigator, 'webdriver', {
          get: () => false
        });
      
        // Mock chrome object
        window.chrome = {
          runtime: {}
        };
      
        // Mock permissions
        const originalQuery = window.navigator.permissions.query;
        window.navigator.permissions.query = (parameters) => (
          parameters.name === 'notifications' ?
            Promise.resolve({ state: 'granted' }) :
            originalQuery(parameters)
        );
      });
      
      // Set realistic user agent
      await page.setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
      );
      ```
      
      ---
      
      ## Debugging Tips
      
      ### Take Screenshots on Error
      
      ```javascript
      page.on('pageerror', async (error) => {
        console.error('Page error:', error);
        await page.screenshot({ path: `error-${Date.now()}.png` });
      });
      ```
      
      ### Console Logging
      
      ```javascript
      // Forward console to Node
      page.on('console', (msg) => {
        console.log('PAGE LOG:', msg.text());
      });
      ```
      
      ### Slow Down Execution
      
      ```javascript
      const browser = await puppeteer.launch({
        slowMo: 250  // 250ms delay between actions
      });
      ```
      
      ### Keep Browser Open
      
      ```javascript
      const browser = await puppeteer.launch({
        headless: false,
        devtools: true
      });
      
      // Prevent auto-close
      await page.evaluate(() => debugger);
      ```
      
      ---
      
      ## Best Practices
      
      1. **Always close browser:** Use try/finally or process cleanup
      2. **Wait appropriately:** Use waitForSelector, not setTimeout
      3. **Handle errors:** Wrap navigation in try/catch
      4. **Optimize selectors:** Use specific selectors for reliability
      5. **Avoid race conditions:** Wait for navigation after clicks
      6. **Reuse pages:** Don't create new pages unnecessarily
      7. **Set timeouts:** Always specify reasonable timeouts
      8. **Clean up:** Close unused pages and contexts
      
      ---
      
      ## Resources
      
      - [Puppeteer Documentation](https://pptr.dev/)
      - [Puppeteer API](https://pptr.dev/api)
      - [Puppeteer Examples](https://github.com/puppeteer/puppeteer/tree/main/examples)
      - [Awesome Puppeteer](https://github.com/transitive-bullshit/awesome-puppeteer)
      
  • scripts
    • lib
      • browser.js 3.2 KB
        /**
         * Shared browser utilities for Chrome DevTools scripts
         */
        import puppeteer from 'puppeteer';
        import debug from 'debug';
        import fs from 'fs';
        import path from 'path';
        import { fileURLToPath } from 'url';
        
        const __dirname = path.dirname(fileURLToPath(import.meta.url));
        const ENDPOINT_FILE = path.join(__dirname, '..', '.browser-endpoint');
        
        const log = debug('chrome-devtools:browser');
        
        let browserInstance = null;
        let pageInstance = null;
        
        /**
         * Launch or connect to browser
         */
        export async function getBrowser(options = {}) {
          if (browserInstance && browserInstance.isConnected()) {
            log('Reusing existing browser instance');
            return browserInstance;
          }
        
          // Check for persistent browser endpoint
          if (!options.browserUrl && !options.wsEndpoint && fs.existsSync(ENDPOINT_FILE)) {
            try {
              const wsEndpoint = fs.readFileSync(ENDPOINT_FILE, 'utf8').trim();
              log('Found persistent browser endpoint, connecting...');
              browserInstance = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
              return browserInstance;
            } catch (error) {
              log('Failed to connect to persistent browser, launching new one:', error.message);
              // Clean up stale endpoint file
              if (fs.existsSync(ENDPOINT_FILE)) {
                fs.unlinkSync(ENDPOINT_FILE);
              }
            }
          }
        
          const launchOptions = {
            headless: options.headless !== false,
            args: [
              '--no-sandbox',
              '--disable-setuid-sandbox',
              '--disable-dev-shm-usage',
              ...(options.args || [])
            ],
            defaultViewport: options.viewport || {
              width: 1920,
              height: 1080
            },
            ...options
          };
        
          if (options.browserUrl || options.wsEndpoint) {
            log('Connecting to existing browser');
            browserInstance = await puppeteer.connect({
              browserURL: options.browserUrl,
              browserWSEndpoint: options.wsEndpoint
            });
          } else {
            log('Launching new browser');
            browserInstance = await puppeteer.launch(launchOptions);
          }
        
          return browserInstance;
        }
        
        /**
         * Get current page or create new one
         */
        export async function getPage(browser) {
          if (pageInstance && !pageInstance.isClosed()) {
            log('Reusing existing page');
            return pageInstance;
          }
        
          const pages = await browser.pages();
          if (pages.length > 0) {
            pageInstance = pages[0];
          } else {
            pageInstance = await browser.newPage();
          }
        
          return pageInstance;
        }
        
        /**
         * Close browser
         */
        export async function closeBrowser() {
          if (browserInstance) {
            await browserInstance.close();
            browserInstance = null;
            pageInstance = null;
          }
        }
        
        /**
         * Parse command line arguments
         */
        export function parseArgs(argv, options = {}) {
          const args = {};
        
          for (let i = 0; i < argv.length; i++) {
            const arg = argv[i];
        
            if (arg.startsWith('--')) {
              const key = arg.slice(2);
              const nextArg = argv[i + 1];
        
              if (nextArg && !nextArg.startsWith('--')) {
                args[key] = nextArg;
                i++;
              } else {
                args[key] = true;
              }
            }
          }
        
          return args;
        }
        
        /**
         * Output JSON result
         */
        export function outputJSON(data) {
          console.log(JSON.stringify(data, null, 2));
        }
        
        /**
         * Output error
         */
        export function outputError(error) {
          console.error(JSON.stringify({
            success: false,
            error: error.message,
            stack: error.stack
          }, null, 2));
          process.exit(1);
        }
        
      • selector.js 5.3 KB
        /**
         * Shared selector parsing and validation library
         * Supports CSS and XPath selectors with security validation
         */
        
        /**
         * Parse and validate selector
         * @param {string} selector - CSS or XPath selector
         * @returns {{type: 'css'|'xpath', selector: string}}
         * @throws {Error} If XPath contains injection patterns
         */
        export function parseSelector(selector) {
          if (!selector || typeof selector !== 'string') {
            throw new Error('Selector must be a non-empty string');
          }
        
          // Detect XPath selectors
          if (selector.startsWith('/') || selector.startsWith('(//')) {
            // XPath injection prevention
            validateXPath(selector);
            return { type: 'xpath', selector };
          }
        
          // CSS selector
          return { type: 'css', selector };
        }
        
        /**
         * Validate XPath selector for security
         * @param {string} xpath - XPath expression to validate
         * @throws {Error} If XPath contains dangerous patterns
         */
        function validateXPath(xpath) {
          const dangerous = [
            'javascript:',
            '<script',
            'onerror=',
            'onload=',
            'onclick=',
            'onmouseover=',
            'eval(',
            'Function(',
            'constructor(',
          ];
        
          const lower = xpath.toLowerCase();
          for (const pattern of dangerous) {
            if (lower.includes(pattern.toLowerCase())) {
              throw new Error(`Potential XPath injection detected: ${pattern}`);
            }
          }
        
          // Additional validation: check for extremely long selectors (potential DoS)
          if (xpath.length > 1000) {
            throw new Error('XPath selector too long (max 1000 characters)');
          }
        }
        
        /**
         * Wait for element based on selector type
         * @param {Object} page - Puppeteer page instance
         * @param {{type: string, selector: string}} parsed - Parsed selector
         * @param {Object} options - Wait options (visible, timeout)
         * @returns {Promise<void>}
         */
        export async function waitForElement(page, parsed, options = {}) {
          const defaultOptions = {
            visible: true,
            timeout: 5000,
            ...options
          };
        
          if (parsed.type === 'xpath') {
            // Use locator API for XPath (Puppeteer v24+)
            const locator = page.locator(`::-p-xpath(${parsed.selector})`);
            // setVisibility and setTimeout are the locator options
            await locator
              .setVisibility(defaultOptions.visible ? 'visible' : null)
              .setTimeout(defaultOptions.timeout)
              .wait();
          } else {
            await page.waitForSelector(parsed.selector, defaultOptions);
          }
        }
        
        /**
         * Click element based on selector type
         * @param {Object} page - Puppeteer page instance
         * @param {{type: string, selector: string}} parsed - Parsed selector
         * @returns {Promise<void>}
         */
        export async function clickElement(page, parsed) {
          if (parsed.type === 'xpath') {
            // Use locator API for XPath (Puppeteer v24+)
            const locator = page.locator(`::-p-xpath(${parsed.selector})`);
            await locator.click();
          } else {
            await page.click(parsed.selector);
          }
        }
        
        /**
         * Type into element based on selector type
         * @param {Object} page - Puppeteer page instance
         * @param {{type: string, selector: string}} parsed - Parsed selector
         * @param {string} value - Text to type
         * @param {Object} options - Type options (delay, clear)
         * @returns {Promise<void>}
         */
        export async function typeIntoElement(page, parsed, value, options = {}) {
          if (parsed.type === 'xpath') {
            // Use locator API for XPath (Puppeteer v24+)
            const locator = page.locator(`::-p-xpath(${parsed.selector})`);
        
            // Clear if requested
            if (options.clear) {
              await locator.fill('');
            }
        
            await locator.fill(value);
          } else {
            // CSS selector
            if (options.clear) {
              await page.$eval(parsed.selector, el => el.value = '');
            }
        
            await page.type(parsed.selector, value, { delay: options.delay || 0 });
          }
        }
        
        /**
         * Get element handle based on selector type
         * @param {Object} page - Puppeteer page instance
         * @param {{type: string, selector: string}} parsed - Parsed selector
         * @returns {Promise<ElementHandle|null>}
         */
        export async function getElement(page, parsed) {
          if (parsed.type === 'xpath') {
            // For XPath, use page.evaluate with XPath evaluation
            // This returns the first matching element
            const element = await page.evaluateHandle((xpath) => {
              const result = document.evaluate(
                xpath,
                document,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
              );
              return result.singleNodeValue;
            }, parsed.selector);
        
            // Convert JSHandle to ElementHandle
            const elementHandle = element.asElement();
            return elementHandle;
          } else {
            return await page.$(parsed.selector);
          }
        }
        
        /**
         * Get enhanced error message for selector failures
         * @param {Error} error - Original error
         * @param {string} selector - Selector that failed
         * @returns {Error} Enhanced error with troubleshooting tips
         */
        export function enhanceError(error, selector) {
          if (error.message.includes('waiting for selector') ||
              error.message.includes('waiting for XPath') ||
              error.message.includes('No node found')) {
            error.message += '\n\nTroubleshooting:\n' +
              '1. Use snapshot.js to find correct selector: node snapshot.js --url <url>\n' +
              '2. Try XPath selector: //button[text()="Click"] or //button[contains(text(),"Click")]\n' +
              '3. Check element is visible on page (not display:none or hidden)\n' +
              '4. Increase --timeout value: --timeout 10000\n' +
              '5. Change wait strategy: --wait-until load or --wait-until domcontentloaded';
          }
          return error;
        }
        
    • __tests__
      • selector.test.js 6.4 KB
        /**
         * Tests for selector parsing library
         * Run with: node --test __tests__/selector.test.js
         */
        import { describe, it } from 'node:test';
        import assert from 'node:assert';
        import { parseSelector } from '../lib/selector.js';
        
        describe('parseSelector', () => {
          describe('CSS Selectors', () => {
            it('should detect simple CSS selectors', () => {
              const result = parseSelector('button');
              assert.strictEqual(result.type, 'css');
              assert.strictEqual(result.selector, 'button');
            });
        
            it('should detect class selectors', () => {
              const result = parseSelector('.btn-submit');
              assert.strictEqual(result.type, 'css');
              assert.strictEqual(result.selector, '.btn-submit');
            });
        
            it('should detect ID selectors', () => {
              const result = parseSelector('#email-input');
              assert.strictEqual(result.type, 'css');
              assert.strictEqual(result.selector, '#email-input');
            });
        
            it('should detect attribute selectors', () => {
              const result = parseSelector('button[type="submit"]');
              assert.strictEqual(result.type, 'css');
              assert.strictEqual(result.selector, 'button[type="submit"]');
            });
        
            it('should detect complex CSS selectors', () => {
              const result = parseSelector('div.container > button.btn-primary:hover');
              assert.strictEqual(result.type, 'css');
            });
          });
        
          describe('XPath Selectors', () => {
            it('should detect absolute XPath', () => {
              const result = parseSelector('/html/body/button');
              assert.strictEqual(result.type, 'xpath');
              assert.strictEqual(result.selector, '/html/body/button');
            });
        
            it('should detect relative XPath', () => {
              const result = parseSelector('//button');
              assert.strictEqual(result.type, 'xpath');
              assert.strictEqual(result.selector, '//button');
            });
        
            it('should detect XPath with text matching', () => {
              const result = parseSelector('//button[text()="Click Me"]');
              assert.strictEqual(result.type, 'xpath');
            });
        
            it('should detect XPath with contains', () => {
              const result = parseSelector('//button[contains(text(),"Submit")]');
              assert.strictEqual(result.type, 'xpath');
            });
        
            it('should detect XPath with attributes', () => {
              const result = parseSelector('//input[@type="email"]');
              assert.strictEqual(result.type, 'xpath');
            });
        
            it('should detect grouped XPath', () => {
              const result = parseSelector('(//button)[1]');
              assert.strictEqual(result.type, 'xpath');
            });
          });
        
          describe('Security Validation', () => {
            it('should block javascript: injection', () => {
              assert.throws(
                () => parseSelector('//button[@onclick="javascript:alert(1)"]'),
                /XPath injection detected.*javascript:/i
              );
            });
        
            it('should block <script tag injection', () => {
              assert.throws(
                () => parseSelector('//div[contains(text(),"<script>alert(1)</script>")]'),
                /XPath injection detected.*<script/i
              );
            });
        
            it('should block onerror= injection', () => {
              assert.throws(
                () => parseSelector('//img[@onerror="alert(1)"]'),
                /XPath injection detected.*onerror=/i
              );
            });
        
            it('should block onload= injection', () => {
              assert.throws(
                () => parseSelector('//body[@onload="malicious()"]'),
                /XPath injection detected.*onload=/i
              );
            });
        
            it('should block onclick= injection', () => {
              assert.throws(
                () => parseSelector('//a[@onclick="steal()"]'),
                /XPath injection detected.*onclick=/i
              );
            });
        
            it('should block eval( injection', () => {
              assert.throws(
                () => parseSelector('//div[eval("malicious")]'),
                /XPath injection detected.*eval\(/i
              );
            });
        
            it('should block Function( injection', () => {
              assert.throws(
                () => parseSelector('//div[Function("return 1")()]'),
                /XPath injection detected.*Function\(/i
              );
            });
        
            it('should block constructor( injection', () => {
              assert.throws(
                () => parseSelector('//div[constructor("alert(1)")()]'),
                /XPath injection detected.*constructor\(/i
              );
            });
        
            it('should be case-insensitive for security checks', () => {
              assert.throws(
                () => parseSelector('//div[@ONERROR="alert(1)"]'),
                /XPath injection detected/i
              );
            });
        
            it('should block extremely long selectors (DoS prevention)', () => {
              const longSelector = '//' + 'a'.repeat(1001);
              assert.throws(
                () => parseSelector(longSelector),
                /XPath selector too long/i
              );
            });
          });
        
          describe('Edge Cases', () => {
            it('should throw on empty string', () => {
              assert.throws(
                () => parseSelector(''),
                /Selector must be a non-empty string/
              );
            });
        
            it('should throw on null', () => {
              assert.throws(
                () => parseSelector(null),
                /Selector must be a non-empty string/
              );
            });
        
            it('should throw on undefined', () => {
              assert.throws(
                () => parseSelector(undefined),
                /Selector must be a non-empty string/
              );
            });
        
            it('should throw on non-string input', () => {
              assert.throws(
                () => parseSelector(123),
                /Selector must be a non-empty string/
              );
            });
        
            it('should handle selectors with special characters', () => {
              const result = parseSelector('button[data-test="submit-form"]');
              assert.strictEqual(result.type, 'css');
            });
        
            it('should allow safe XPath with parentheses', () => {
              const result = parseSelector('//button[contains(text(),"Save")]');
              assert.strictEqual(result.type, 'xpath');
              // Should not throw
            });
          });
        
          describe('Real-World Examples', () => {
            it('should handle common button selector', () => {
              const result = parseSelector('//button[contains(text(),"Submit")]');
              assert.strictEqual(result.type, 'xpath');
            });
        
            it('should handle complex form selector', () => {
              const result = parseSelector('//form[@id="login-form"]//input[@type="email"]');
              assert.strictEqual(result.type, 'xpath');
            });
        
            it('should handle descendant selector', () => {
              const result = parseSelector('//div[@class="modal"]//button[@class="close"]');
              assert.strictEqual(result.type, 'xpath');
            });
        
            it('should handle nth-child equivalent', () => {
              const result = parseSelector('(//li)[3]');
              assert.strictEqual(result.type, 'xpath');
            });
          });
        });
        
    • .gitignore 12 B · in bundle
    • click.js 2.2 KB
      #!/usr/bin/env node
      /**
       * Click an element
       * Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
       * Supports both CSS and XPath selectors:
       *   - CSS: node click.js --selector "button.submit"
       *   - XPath: node click.js --selector "//button[contains(text(),'Submit')]"
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      import { parseSelector, waitForElement, clickElement, enhanceError } from './lib/selector.js';
      
      async function click() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.selector) {
          outputError(new Error('--selector is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          // Navigate if URL provided
          if (args.url) {
            await page.goto(args.url, {
              waitUntil: args['wait-until'] || 'networkidle2'
            });
          }
      
          // Parse and validate selector
          const parsed = parseSelector(args.selector);
      
          // Wait for element based on selector type
          await waitForElement(page, parsed, {
            visible: true,
            timeout: parseInt(args.timeout || '5000')
          });
      
          // Set up navigation promise BEFORE clicking (in case click triggers immediate navigation)
          const navigationPromise = page.waitForNavigation({
            waitUntil: 'load',
            timeout: 5000
          }).catch(() => null); // Catch timeout - navigation may not occur
      
          // Click element
          await clickElement(page, parsed);
      
          // Wait for optional selector after click
          if (args['wait-for']) {
            await page.waitForSelector(args['wait-for'], {
              timeout: parseInt(args.timeout || '5000')
            });
          } else {
            // Wait for navigation to complete (or timeout if no navigation)
            await navigationPromise;
          }
      
          outputJSON({
            success: true,
            url: page.url(),
            title: await page.title()
          });
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          // Enhance error message with troubleshooting tips
          const enhanced = enhanceError(error, args.selector);
          outputError(enhanced);
          process.exit(1);
        }
      }
      
      click();
      
    • close-persistent.js 974 B
      #!/usr/bin/env node
      /**
       * Close the persistent Chrome browser
       */
      import puppeteer from 'puppeteer';
      import fs from 'fs';
      import path from 'path';
      import { fileURLToPath } from 'url';
      
      const __dirname = path.dirname(fileURLToPath(import.meta.url));
      const ENDPOINT_FILE = path.join(__dirname, '.browser-endpoint');
      
      async function main() {
        if (!fs.existsSync(ENDPOINT_FILE)) {
          console.log('No persistent browser found.');
          process.exit(0);
        }
      
        const wsEndpoint = fs.readFileSync(ENDPOINT_FILE, 'utf8');
        console.log('Connecting to browser...');
      
        try {
          const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
          await browser.close();
          fs.unlinkSync(ENDPOINT_FILE);
          console.log('✓ Browser closed successfully.');
        } catch (error) {
          console.error('Error closing browser:', error.message);
          // Clean up stale endpoint file
          if (fs.existsSync(ENDPOINT_FILE)) {
            fs.unlinkSync(ENDPOINT_FILE);
          }
        }
      }
      
      main();
      
    • console.js 1.7 KB
      #!/usr/bin/env node
      /**
       * Monitor console messages
       * Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000]
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      
      async function monitorConsole() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.url) {
          outputError(new Error('--url is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          const messages = [];
          const filterTypes = args.types ? args.types.split(',') : null;
      
          // Listen for console messages
          page.on('console', (msg) => {
            const type = msg.type();
      
            if (!filterTypes || filterTypes.includes(type)) {
              messages.push({
                type: type,
                text: msg.text(),
                location: msg.location(),
                timestamp: Date.now()
              });
            }
          });
      
          // Listen for page errors
          page.on('pageerror', (error) => {
            messages.push({
              type: 'pageerror',
              text: error.message,
              stack: error.stack,
              timestamp: Date.now()
            });
          });
      
          // Navigate
          await page.goto(args.url, {
            waitUntil: args['wait-until'] || 'networkidle2'
          });
      
          // Wait for additional time if specified
          if (args.duration) {
            await new Promise(resolve => setTimeout(resolve, parseInt(args.duration)));
          }
      
          outputJSON({
            success: true,
            url: page.url(),
            messageCount: messages.length,
            messages: messages
          });
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          outputError(error);
        }
      }
      
      monitorConsole();
      
    • evaluate.js 1.1 KB
      #!/usr/bin/env node
      /**
       * Execute JavaScript in page context
       * Usage: node evaluate.js --script "document.title" [--url https://example.com]
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      
      async function evaluate() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.script) {
          outputError(new Error('--script is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          // Navigate if URL provided
          if (args.url) {
            await page.goto(args.url, {
              waitUntil: args['wait-until'] || 'networkidle2'
            });
          }
      
          const result = await page.evaluate((script) => {
            // eslint-disable-next-line no-eval
            return eval(script);
          }, args.script);
      
          outputJSON({
            success: true,
            result: result,
            url: page.url()
          });
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          outputError(error);
        }
      }
      
      evaluate();
      
    • fill.js 1.9 KB
      #!/usr/bin/env node
      /**
       * Fill form fields
       * Usage: node fill.js --selector "#input" --value "text" [--url https://example.com]
       * Supports both CSS and XPath selectors:
       *   - CSS: node fill.js --selector "#email" --value "user@example.com"
       *   - XPath: node fill.js --selector "//input[@type='email']" --value "user@example.com"
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      import { parseSelector, waitForElement, typeIntoElement, enhanceError } from './lib/selector.js';
      
      async function fill() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.selector) {
          outputError(new Error('--selector is required'));
          return;
        }
      
        if (!args.value) {
          outputError(new Error('--value is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          // Navigate if URL provided
          if (args.url) {
            await page.goto(args.url, {
              waitUntil: args['wait-until'] || 'networkidle2'
            });
          }
      
          // Parse and validate selector
          const parsed = parseSelector(args.selector);
      
          // Wait for element based on selector type
          await waitForElement(page, parsed, {
            visible: true,
            timeout: parseInt(args.timeout || '5000')
          });
      
          // Type into element
          await typeIntoElement(page, parsed, args.value, {
            clear: args.clear === 'true',
            delay: parseInt(args.delay || '0')
          });
      
          outputJSON({
            success: true,
            selector: args.selector,
            value: args.value,
            url: page.url()
          });
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          // Enhance error message with troubleshooting tips
          const enhanced = enhanceError(error, args.selector);
          outputError(enhanced);
          process.exit(1);
        }
      }
      
      fill();
      
    • install-deps.sh 4.3 KB
      #!/bin/bash
      # System dependencies installation script for Chrome DevTools Agent Skill
      # This script installs required system libraries for running Chrome/Chromium
      
      set -e
      
      echo "🚀 Installing system dependencies for Chrome/Chromium..."
      echo ""
      
      # Detect OS
      if [ -f /etc/os-release ]; then
          . /etc/os-release
          OS=$ID
      else
          echo "❌ Cannot detect OS. This script supports Debian/Ubuntu-based systems."
          exit 1
      fi
      
      # Check if running as root
      if [ "$EUID" -ne 0 ]; then
          SUDO="sudo"
          echo "⚠️  This script requires root privileges to install system packages."
          echo "   You may be prompted for your password."
          echo ""
      else
          SUDO=""
      fi
      
      # Install dependencies based on OS
      case $OS in
          ubuntu|debian|pop)
              echo "Detected: $PRETTY_NAME"
              echo "Installing dependencies with apt..."
              echo ""
      
              $SUDO apt-get update
      
              # Install Chrome dependencies
              $SUDO apt-get install -y \
                  ca-certificates \
                  fonts-liberation \
                  libasound2t64 \
                  libatk-bridge2.0-0 \
                  libatk1.0-0 \
                  libc6 \
                  libcairo2 \
                  libcups2 \
                  libdbus-1-3 \
                  libexpat1 \
                  libfontconfig1 \
                  libgbm1 \
                  libgcc1 \
                  libglib2.0-0 \
                  libgtk-3-0 \
                  libnspr4 \
                  libnss3 \
                  libpango-1.0-0 \
                  libpangocairo-1.0-0 \
                  libstdc++6 \
                  libx11-6 \
                  libx11-xcb1 \
                  libxcb1 \
                  libxcomposite1 \
                  libxcursor1 \
                  libxdamage1 \
                  libxext6 \
                  libxfixes3 \
                  libxi6 \
                  libxrandr2 \
                  libxrender1 \
                  libxss1 \
                  libxtst6 \
                  lsb-release \
                  wget \
                  xdg-utils
      
              echo ""
              echo "✅ System dependencies installed successfully!"
              ;;
      
          fedora|rhel|centos)
              echo "Detected: $PRETTY_NAME"
              echo "Installing dependencies with dnf/yum..."
              echo ""
      
              # Try dnf first, fallback to yum
              if command -v dnf &> /dev/null; then
                  PKG_MGR="dnf"
              else
                  PKG_MGR="yum"
              fi
      
              $SUDO $PKG_MGR install -y \
                  alsa-lib \
                  atk \
                  at-spi2-atk \
                  cairo \
                  cups-libs \
                  dbus-libs \
                  expat \
                  fontconfig \
                  glib2 \
                  gtk3 \
                  libdrm \
                  libgbm \
                  libX11 \
                  libxcb \
                  libXcomposite \
                  libXcursor \
                  libXdamage \
                  libXext \
                  libXfixes \
                  libXi \
                  libxkbcommon \
                  libXrandr \
                  libXrender \
                  libXScrnSaver \
                  libXtst \
                  mesa-libgbm \
                  nspr \
                  nss \
                  pango
      
              echo ""
              echo "✅ System dependencies installed successfully!"
              ;;
      
          arch|manjaro)
              echo "Detected: $PRETTY_NAME"
              echo "Installing dependencies with pacman..."
              echo ""
      
              $SUDO pacman -Sy --noconfirm \
                  alsa-lib \
                  at-spi2-core \
                  cairo \
                  cups \
                  dbus \
                  expat \
                  glib2 \
                  gtk3 \
                  libdrm \
                  libx11 \
                  libxcb \
                  libxcomposite \
                  libxcursor \
                  libxdamage \
                  libxext \
                  libxfixes \
                  libxi \
                  libxkbcommon \
                  libxrandr \
                  libxrender \
                  libxshmfence \
                  libxss \
                  libxtst \
                  mesa \
                  nspr \
                  nss \
                  pango
      
              echo ""
              echo "✅ System dependencies installed successfully!"
              ;;
      
          *)
              echo "❌ Unsupported OS: $OS"
              echo "   This script supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro"
              echo ""
              echo "   Please install Chrome/Chromium dependencies manually for your OS."
              echo "   See: https://pptr.dev/troubleshooting"
              exit 1
              ;;
      esac
      
      echo ""
      echo "📝 Next steps:"
      echo "   1. Run: cd $(dirname "$0")"
      echo "   2. Run: npm install"
      echo "   3. Test: node navigate.js --url https://example.com"
      echo ""
      
    • install.sh 2.1 KB
      #!/bin/bash
      # Installation script for Chrome DevTools Agent Skill
      
      set -e
      
      echo "🚀 Installing Chrome DevTools Agent Skill..."
      echo ""
      
      # Check Node.js version
      echo "Checking Node.js version..."
      NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
      
      if [ "$NODE_VERSION" -lt 18 ]; then
        echo "❌ Error: Node.js 18+ is required. Current version: $(node --version)"
        echo "   Please upgrade Node.js: https://nodejs.org/"
        exit 1
      fi
      
      echo "✓ Node.js version: $(node --version)"
      echo ""
      
      # Check for system dependencies (Linux only)
      if [[ "$OSTYPE" == "linux-gnu"* ]]; then
        echo "Checking system dependencies (Linux)..."
      
        # Check for critical Chrome dependencies
        MISSING_DEPS=()
      
        if ! ldconfig -p | grep -q libnss3.so; then
          MISSING_DEPS+=("libnss3")
        fi
      
        if ! ldconfig -p | grep -q libnspr4.so; then
          MISSING_DEPS+=("libnspr4")
        fi
      
        if ! ldconfig -p | grep -q libgbm.so; then
          MISSING_DEPS+=("libgbm1")
        fi
      
        if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
          echo "⚠️  Missing system dependencies: ${MISSING_DEPS[*]}"
          echo ""
          echo "   Chrome/Chromium requires system libraries to run."
          echo "   Install them with:"
          echo ""
          echo "   ./install-deps.sh"
          echo ""
          echo "   Or manually:"
          echo "   sudo apt-get install -y libnss3 libnspr4 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2"
          echo ""
      
          read -p "   Continue anyway? (y/N) " -n 1 -r
          echo ""
          if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            echo "Installation cancelled."
            exit 1
          fi
        else
          echo "✓ System dependencies found"
        fi
        echo ""
      elif [[ "$OSTYPE" == "darwin"* ]]; then
        echo "Platform: macOS (no system dependencies needed)"
        echo ""
      elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
        echo "Platform: Windows (no system dependencies needed)"
        echo ""
      fi
      
      # Install Node.js dependencies
      echo "Installing Node.js dependencies..."
      npm install
      
      echo ""
      echo "✅ Installation complete!"
      echo ""
      echo "Test the installation:"
      echo "  node navigate.js --url https://example.com"
      echo ""
      echo "For more information:"
      echo "  cat README.md"
      echo ""
      
    • launch-persistent.js 2.1 KB
      #!/usr/bin/env node
      /**
       * Launch a persistent Chrome browser that can be reused across multiple commands
       * Saves the WebSocket endpoint to a file for other scripts to connect to
       */
      import puppeteer from 'puppeteer';
      import fs from 'fs';
      import path from 'path';
      import { fileURLToPath } from 'url';
      
      const __dirname = path.dirname(fileURLToPath(import.meta.url));
      const ENDPOINT_FILE = path.join(__dirname, '.browser-endpoint');
      
      async function main() {
        // Parse command line arguments
        const args = process.argv.slice(2);
        const headless = !args.includes('--headless=false') && !args.includes('--no-headless');
        const url = args.find(arg => arg.startsWith('--url='))?.split('=')[1] || 'about:blank';
      
        console.log('Launching persistent Chrome browser...');
      
        const browser = await puppeteer.launch({
          headless,
          args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage',
            '--remote-debugging-port=9222'
          ],
          defaultViewport: {
            width: 1920,
            height: 1080
          }
        });
      
        const wsEndpoint = browser.wsEndpoint();
        
        // Save endpoint to file
        fs.writeFileSync(ENDPOINT_FILE, wsEndpoint);
        console.log(`Browser launched. WebSocket endpoint saved to: ${ENDPOINT_FILE}`);
        console.log(`WebSocket: ${wsEndpoint}`);
        
        // Navigate to initial URL if provided
        if (url !== 'about:blank') {
          const page = (await browser.pages())[0];
          console.log(`Navigating to: ${url}`);
          await page.goto(url, { waitUntil: 'networkidle2' });
        }
      
        console.log('\n✓ Browser is ready for commands!');
        console.log('  Use other scripts normally - they will connect to this browser.');
        console.log('  Run "node close-persistent.js" or press Ctrl+C to close.\n');
      
        // Keep process alive
        process.on('SIGINT', async () => {
          console.log('\nClosing browser...');
          await browser.close();
          if (fs.existsSync(ENDPOINT_FILE)) {
            fs.unlinkSync(ENDPOINT_FILE);
          }
          process.exit(0);
        });
      
        // Keep alive indefinitely
        await new Promise(() => {});
      }
      
      main().catch(error => {
        console.error('Error launching browser:', error);
        process.exit(1);
      });
      
    • navigate.js 994 B
      #!/usr/bin/env node
      /**
       * Navigate to a URL
       * Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      
      async function navigate() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.url) {
          outputError(new Error('--url is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          const options = {
            waitUntil: args['wait-until'] || 'networkidle2',
            timeout: parseInt(args.timeout || '30000')
          };
      
          await page.goto(args.url, options);
      
          const result = {
            success: true,
            url: page.url(),
            title: await page.title()
          };
      
          outputJSON(result);
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          outputError(error);
        }
      }
      
      navigate();
      
    • network.js 2.7 KB
      #!/usr/bin/env node
      /**
       * Monitor network requests
       * Usage: node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      import fs from 'fs/promises';
      
      async function monitorNetwork() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.url) {
          outputError(new Error('--url is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          const requests = [];
          const filterTypes = args.types ? args.types.split(',').map(t => t.toLowerCase()) : null;
      
          // Monitor requests
          page.on('request', (request) => {
            const resourceType = request.resourceType().toLowerCase();
      
            if (!filterTypes || filterTypes.includes(resourceType)) {
              requests.push({
                id: request._requestId || requests.length,
                url: request.url(),
                method: request.method(),
                resourceType: resourceType,
                headers: request.headers(),
                postData: request.postData(),
                timestamp: Date.now()
              });
            }
          });
      
          // Monitor responses
          const responses = new Map();
          page.on('response', async (response) => {
            const request = response.request();
            const resourceType = request.resourceType().toLowerCase();
      
            if (!filterTypes || filterTypes.includes(resourceType)) {
              try {
                responses.set(request._requestId || request.url(), {
                  status: response.status(),
                  statusText: response.statusText(),
                  headers: response.headers(),
                  fromCache: response.fromCache(),
                  timing: response.timing()
                });
              } catch (e) {
                // Ignore errors for some response types
              }
            }
          });
      
          // Navigate
          await page.goto(args.url, {
            waitUntil: args['wait-until'] || 'networkidle2'
          });
      
          // Merge requests with responses
          const combined = requests.map(req => ({
            ...req,
            response: responses.get(req.id) || responses.get(req.url) || null
          }));
      
          const result = {
            success: true,
            url: page.url(),
            requestCount: combined.length,
            requests: combined
          };
      
          if (args.output) {
            await fs.writeFile(args.output, JSON.stringify(result, null, 2));
            outputJSON({
              success: true,
              output: args.output,
              requestCount: combined.length
            });
          } else {
            outputJSON(result);
          }
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          outputError(error);
        }
      }
      
      monitorNetwork();
      
    • package-lock.json 42.1 KB
      {
        "name": "chrome-devtools-scripts",
        "version": "1.0.0",
        "lockfileVersion": 3,
        "requires": true,
        "packages": {
          "": {
            "name": "chrome-devtools-scripts",
            "version": "1.0.0",
            "dependencies": {
              "debug": "^4.4.0",
              "puppeteer": "^23.11.1",
              "yargs": "^17.7.2"
            },
            "engines": {
              "node": ">=18.0.0"
            }
          },
          "node_modules/@babel/code-frame": {
            "version": "7.27.1",
            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
            "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
            "license": "MIT",
            "dependencies": {
              "@babel/helper-validator-identifier": "^7.27.1",
              "js-tokens": "^4.0.0",
              "picocolors": "^1.1.1"
            },
            "engines": {
              "node": ">=6.9.0"
            }
          },
          "node_modules/@babel/helper-validator-identifier": {
            "version": "7.28.5",
            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
            "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
            "license": "MIT",
            "engines": {
              "node": ">=6.9.0"
            }
          },
          "node_modules/@puppeteer/browsers": {
            "version": "2.6.1",
            "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz",
            "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==",
            "license": "Apache-2.0",
            "dependencies": {
              "debug": "^4.4.0",
              "extract-zip": "^2.0.1",
              "progress": "^2.0.3",
              "proxy-agent": "^6.5.0",
              "semver": "^7.6.3",
              "tar-fs": "^3.0.6",
              "unbzip2-stream": "^1.4.3",
              "yargs": "^17.7.2"
            },
            "bin": {
              "browsers": "lib/cjs/main-cli.js"
            },
            "engines": {
              "node": ">=18"
            }
          },
          "node_modules/@tootallnate/quickjs-emscripten": {
            "version": "0.23.0",
            "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
            "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
            "license": "MIT"
          },
          "node_modules/@types/node": {
            "version": "24.9.1",
            "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
            "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
            "license": "MIT",
            "optional": true,
            "dependencies": {
              "undici-types": "~7.16.0"
            }
          },
          "node_modules/@types/yauzl": {
            "version": "2.10.3",
            "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
            "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
            "license": "MIT",
            "optional": true,
            "dependencies": {
              "@types/node": "*"
            }
          },
          "node_modules/agent-base": {
            "version": "7.1.4",
            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
            "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
            "license": "MIT",
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/ansi-regex": {
            "version": "5.0.1",
            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
            "license": "MIT",
            "engines": {
              "node": ">=8"
            }
          },
          "node_modules/ansi-styles": {
            "version": "4.3.0",
            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
            "license": "MIT",
            "dependencies": {
              "color-convert": "^2.0.1"
            },
            "engines": {
              "node": ">=8"
            },
            "funding": {
              "url": "https://github.com/chalk/ansi-styles?sponsor=1"
            }
          },
          "node_modules/argparse": {
            "version": "2.0.1",
            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
            "license": "Python-2.0"
          },
          "node_modules/ast-types": {
            "version": "0.13.4",
            "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
            "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
            "license": "MIT",
            "dependencies": {
              "tslib": "^2.0.1"
            },
            "engines": {
              "node": ">=4"
            }
          },
          "node_modules/b4a": {
            "version": "1.7.3",
            "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
            "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
            "license": "Apache-2.0",
            "peerDependencies": {
              "react-native-b4a": "*"
            },
            "peerDependenciesMeta": {
              "react-native-b4a": {
                "optional": true
              }
            }
          },
          "node_modules/bare-events": {
            "version": "2.8.1",
            "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz",
            "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==",
            "license": "Apache-2.0",
            "peerDependencies": {
              "bare-abort-controller": "*"
            },
            "peerDependenciesMeta": {
              "bare-abort-controller": {
                "optional": true
              }
            }
          },
          "node_modules/bare-fs": {
            "version": "4.5.0",
            "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.0.tgz",
            "integrity": "sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ==",
            "license": "Apache-2.0",
            "optional": true,
            "dependencies": {
              "bare-events": "^2.5.4",
              "bare-path": "^3.0.0",
              "bare-stream": "^2.6.4",
              "bare-url": "^2.2.2",
              "fast-fifo": "^1.3.2"
            },
            "engines": {
              "bare": ">=1.16.0"
            },
            "peerDependencies": {
              "bare-buffer": "*"
            },
            "peerDependenciesMeta": {
              "bare-buffer": {
                "optional": true
              }
            }
          },
          "node_modules/bare-os": {
            "version": "3.6.2",
            "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
            "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
            "license": "Apache-2.0",
            "optional": true,
            "engines": {
              "bare": ">=1.14.0"
            }
          },
          "node_modules/bare-path": {
            "version": "3.0.0",
            "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
            "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
            "license": "Apache-2.0",
            "optional": true,
            "dependencies": {
              "bare-os": "^3.0.1"
            }
          },
          "node_modules/bare-stream": {
            "version": "2.7.0",
            "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
            "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
            "license": "Apache-2.0",
            "optional": true,
            "dependencies": {
              "streamx": "^2.21.0"
            },
            "peerDependencies": {
              "bare-buffer": "*",
              "bare-events": "*"
            },
            "peerDependenciesMeta": {
              "bare-buffer": {
                "optional": true
              },
              "bare-events": {
                "optional": true
              }
            }
          },
          "node_modules/bare-url": {
            "version": "2.3.1",
            "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz",
            "integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==",
            "license": "Apache-2.0",
            "optional": true,
            "dependencies": {
              "bare-path": "^3.0.0"
            }
          },
          "node_modules/base64-js": {
            "version": "1.5.1",
            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
            "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
            "funding": [
              {
                "type": "github",
                "url": "https://github.com/sponsors/feross"
              },
              {
                "type": "patreon",
                "url": "https://www.patreon.com/feross"
              },
              {
                "type": "consulting",
                "url": "https://feross.org/support"
              }
            ],
            "license": "MIT"
          },
          "node_modules/basic-ftp": {
            "version": "5.0.5",
            "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
            "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
            "license": "MIT",
            "engines": {
              "node": ">=10.0.0"
            }
          },
          "node_modules/buffer": {
            "version": "5.7.1",
            "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
            "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
            "funding": [
              {
                "type": "github",
                "url": "https://github.com/sponsors/feross"
              },
              {
                "type": "patreon",
                "url": "https://www.patreon.com/feross"
              },
              {
                "type": "consulting",
                "url": "https://feross.org/support"
              }
            ],
            "license": "MIT",
            "dependencies": {
              "base64-js": "^1.3.1",
              "ieee754": "^1.1.13"
            }
          },
          "node_modules/buffer-crc32": {
            "version": "0.2.13",
            "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
            "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
            "license": "MIT",
            "engines": {
              "node": "*"
            }
          },
          "node_modules/callsites": {
            "version": "3.1.0",
            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
            "license": "MIT",
            "engines": {
              "node": ">=6"
            }
          },
          "node_modules/chromium-bidi": {
            "version": "0.11.0",
            "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz",
            "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==",
            "license": "Apache-2.0",
            "dependencies": {
              "mitt": "3.0.1",
              "zod": "3.23.8"
            },
            "peerDependencies": {
              "devtools-protocol": "*"
            }
          },
          "node_modules/cliui": {
            "version": "8.0.1",
            "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
            "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
            "license": "ISC",
            "dependencies": {
              "string-width": "^4.2.0",
              "strip-ansi": "^6.0.1",
              "wrap-ansi": "^7.0.0"
            },
            "engines": {
              "node": ">=12"
            }
          },
          "node_modules/color-convert": {
            "version": "2.0.1",
            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
            "license": "MIT",
            "dependencies": {
              "color-name": "~1.1.4"
            },
            "engines": {
              "node": ">=7.0.0"
            }
          },
          "node_modules/color-name": {
            "version": "1.1.4",
            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
            "license": "MIT"
          },
          "node_modules/cosmiconfig": {
            "version": "9.0.0",
            "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
            "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
            "license": "MIT",
            "dependencies": {
              "env-paths": "^2.2.1",
              "import-fresh": "^3.3.0",
              "js-yaml": "^4.1.0",
              "parse-json": "^5.2.0"
            },
            "engines": {
              "node": ">=14"
            },
            "funding": {
              "url": "https://github.com/sponsors/d-fischer"
            },
            "peerDependencies": {
              "typescript": ">=4.9.5"
            },
            "peerDependenciesMeta": {
              "typescript": {
                "optional": true
              }
            }
          },
          "node_modules/data-uri-to-buffer": {
            "version": "6.0.2",
            "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
            "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
            "license": "MIT",
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/debug": {
            "version": "4.4.3",
            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
            "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
            "license": "MIT",
            "dependencies": {
              "ms": "^2.1.3"
            },
            "engines": {
              "node": ">=6.0"
            },
            "peerDependenciesMeta": {
              "supports-color": {
                "optional": true
              }
            }
          },
          "node_modules/degenerator": {
            "version": "5.0.1",
            "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
            "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
            "license": "MIT",
            "dependencies": {
              "ast-types": "^0.13.4",
              "escodegen": "^2.1.0",
              "esprima": "^4.0.1"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/devtools-protocol": {
            "version": "0.0.1367902",
            "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
            "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
            "license": "BSD-3-Clause"
          },
          "node_modules/emoji-regex": {
            "version": "8.0.0",
            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
            "license": "MIT"
          },
          "node_modules/end-of-stream": {
            "version": "1.4.5",
            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
            "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
            "license": "MIT",
            "dependencies": {
              "once": "^1.4.0"
            }
          },
          "node_modules/env-paths": {
            "version": "2.2.1",
            "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
            "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
            "license": "MIT",
            "engines": {
              "node": ">=6"
            }
          },
          "node_modules/error-ex": {
            "version": "1.3.4",
            "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
            "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
            "license": "MIT",
            "dependencies": {
              "is-arrayish": "^0.2.1"
            }
          },
          "node_modules/escalade": {
            "version": "3.2.0",
            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
            "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
            "license": "MIT",
            "engines": {
              "node": ">=6"
            }
          },
          "node_modules/escodegen": {
            "version": "2.1.0",
            "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
            "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
            "license": "BSD-2-Clause",
            "dependencies": {
              "esprima": "^4.0.1",
              "estraverse": "^5.2.0",
              "esutils": "^2.0.2"
            },
            "bin": {
              "escodegen": "bin/escodegen.js",
              "esgenerate": "bin/esgenerate.js"
            },
            "engines": {
              "node": ">=6.0"
            },
            "optionalDependencies": {
              "source-map": "~0.6.1"
            }
          },
          "node_modules/esprima": {
            "version": "4.0.1",
            "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
            "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
            "license": "BSD-2-Clause",
            "bin": {
              "esparse": "bin/esparse.js",
              "esvalidate": "bin/esvalidate.js"
            },
            "engines": {
              "node": ">=4"
            }
          },
          "node_modules/estraverse": {
            "version": "5.3.0",
            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
            "license": "BSD-2-Clause",
            "engines": {
              "node": ">=4.0"
            }
          },
          "node_modules/esutils": {
            "version": "2.0.3",
            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
            "license": "BSD-2-Clause",
            "engines": {
              "node": ">=0.10.0"
            }
          },
          "node_modules/events-universal": {
            "version": "1.0.1",
            "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
            "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
            "license": "Apache-2.0",
            "dependencies": {
              "bare-events": "^2.7.0"
            }
          },
          "node_modules/extract-zip": {
            "version": "2.0.1",
            "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
            "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
            "license": "BSD-2-Clause",
            "dependencies": {
              "debug": "^4.1.1",
              "get-stream": "^5.1.0",
              "yauzl": "^2.10.0"
            },
            "bin": {
              "extract-zip": "cli.js"
            },
            "engines": {
              "node": ">= 10.17.0"
            },
            "optionalDependencies": {
              "@types/yauzl": "^2.9.1"
            }
          },
          "node_modules/fast-fifo": {
            "version": "1.3.2",
            "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
            "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
            "license": "MIT"
          },
          "node_modules/fd-slicer": {
            "version": "1.1.0",
            "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
            "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
            "license": "MIT",
            "dependencies": {
              "pend": "~1.2.0"
            }
          },
          "node_modules/get-caller-file": {
            "version": "2.0.5",
            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
            "license": "ISC",
            "engines": {
              "node": "6.* || 8.* || >= 10.*"
            }
          },
          "node_modules/get-stream": {
            "version": "5.2.0",
            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
            "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
            "license": "MIT",
            "dependencies": {
              "pump": "^3.0.0"
            },
            "engines": {
              "node": ">=8"
            },
            "funding": {
              "url": "https://github.com/sponsors/sindresorhus"
            }
          },
          "node_modules/get-uri": {
            "version": "6.0.5",
            "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
            "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
            "license": "MIT",
            "dependencies": {
              "basic-ftp": "^5.0.2",
              "data-uri-to-buffer": "^6.0.2",
              "debug": "^4.3.4"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/http-proxy-agent": {
            "version": "7.0.2",
            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
            "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
            "license": "MIT",
            "dependencies": {
              "agent-base": "^7.1.0",
              "debug": "^4.3.4"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/https-proxy-agent": {
            "version": "7.0.6",
            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
            "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
            "license": "MIT",
            "dependencies": {
              "agent-base": "^7.1.2",
              "debug": "4"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/ieee754": {
            "version": "1.2.1",
            "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
            "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
            "funding": [
              {
                "type": "github",
                "url": "https://github.com/sponsors/feross"
              },
              {
                "type": "patreon",
                "url": "https://www.patreon.com/feross"
              },
              {
                "type": "consulting",
                "url": "https://feross.org/support"
              }
            ],
            "license": "BSD-3-Clause"
          },
          "node_modules/import-fresh": {
            "version": "3.3.1",
            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
            "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
            "license": "MIT",
            "dependencies": {
              "parent-module": "^1.0.0",
              "resolve-from": "^4.0.0"
            },
            "engines": {
              "node": ">=6"
            },
            "funding": {
              "url": "https://github.com/sponsors/sindresorhus"
            }
          },
          "node_modules/ip-address": {
            "version": "10.0.1",
            "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
            "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
            "license": "MIT",
            "engines": {
              "node": ">= 12"
            }
          },
          "node_modules/is-arrayish": {
            "version": "0.2.1",
            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
            "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
            "license": "MIT"
          },
          "node_modules/is-fullwidth-code-point": {
            "version": "3.0.0",
            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
            "license": "MIT",
            "engines": {
              "node": ">=8"
            }
          },
          "node_modules/js-tokens": {
            "version": "4.0.0",
            "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
            "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
            "license": "MIT"
          },
          "node_modules/js-yaml": {
            "version": "4.1.0",
            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
            "license": "MIT",
            "dependencies": {
              "argparse": "^2.0.1"
            },
            "bin": {
              "js-yaml": "bin/js-yaml.js"
            }
          },
          "node_modules/json-parse-even-better-errors": {
            "version": "2.3.1",
            "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
            "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
            "license": "MIT"
          },
          "node_modules/lines-and-columns": {
            "version": "1.2.4",
            "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
            "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
            "license": "MIT"
          },
          "node_modules/lru-cache": {
            "version": "7.18.3",
            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
            "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
            "license": "ISC",
            "engines": {
              "node": ">=12"
            }
          },
          "node_modules/mitt": {
            "version": "3.0.1",
            "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
            "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
            "license": "MIT"
          },
          "node_modules/ms": {
            "version": "2.1.3",
            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
            "license": "MIT"
          },
          "node_modules/netmask": {
            "version": "2.0.2",
            "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
            "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
            "license": "MIT",
            "engines": {
              "node": ">= 0.4.0"
            }
          },
          "node_modules/once": {
            "version": "1.4.0",
            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
            "license": "ISC",
            "dependencies": {
              "wrappy": "1"
            }
          },
          "node_modules/pac-proxy-agent": {
            "version": "7.2.0",
            "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
            "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
            "license": "MIT",
            "dependencies": {
              "@tootallnate/quickjs-emscripten": "^0.23.0",
              "agent-base": "^7.1.2",
              "debug": "^4.3.4",
              "get-uri": "^6.0.1",
              "http-proxy-agent": "^7.0.0",
              "https-proxy-agent": "^7.0.6",
              "pac-resolver": "^7.0.1",
              "socks-proxy-agent": "^8.0.5"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/pac-resolver": {
            "version": "7.0.1",
            "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
            "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
            "license": "MIT",
            "dependencies": {
              "degenerator": "^5.0.0",
              "netmask": "^2.0.2"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/parent-module": {
            "version": "1.0.1",
            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
            "license": "MIT",
            "dependencies": {
              "callsites": "^3.0.0"
            },
            "engines": {
              "node": ">=6"
            }
          },
          "node_modules/parse-json": {
            "version": "5.2.0",
            "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
            "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
            "license": "MIT",
            "dependencies": {
              "@babel/code-frame": "^7.0.0",
              "error-ex": "^1.3.1",
              "json-parse-even-better-errors": "^2.3.0",
              "lines-and-columns": "^1.1.6"
            },
            "engines": {
              "node": ">=8"
            },
            "funding": {
              "url": "https://github.com/sponsors/sindresorhus"
            }
          },
          "node_modules/pend": {
            "version": "1.2.0",
            "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
            "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
            "license": "MIT"
          },
          "node_modules/picocolors": {
            "version": "1.1.1",
            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
            "license": "ISC"
          },
          "node_modules/progress": {
            "version": "2.0.3",
            "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
            "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
            "license": "MIT",
            "engines": {
              "node": ">=0.4.0"
            }
          },
          "node_modules/proxy-agent": {
            "version": "6.5.0",
            "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
            "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
            "license": "MIT",
            "dependencies": {
              "agent-base": "^7.1.2",
              "debug": "^4.3.4",
              "http-proxy-agent": "^7.0.1",
              "https-proxy-agent": "^7.0.6",
              "lru-cache": "^7.14.1",
              "pac-proxy-agent": "^7.1.0",
              "proxy-from-env": "^1.1.0",
              "socks-proxy-agent": "^8.0.5"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/proxy-from-env": {
            "version": "1.1.0",
            "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
            "license": "MIT"
          },
          "node_modules/pump": {
            "version": "3.0.3",
            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
            "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
            "license": "MIT",
            "dependencies": {
              "end-of-stream": "^1.1.0",
              "once": "^1.3.1"
            }
          },
          "node_modules/puppeteer": {
            "version": "23.11.1",
            "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz",
            "integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==",
            "deprecated": "< 24.15.0 is no longer supported",
            "hasInstallScript": true,
            "license": "Apache-2.0",
            "dependencies": {
              "@puppeteer/browsers": "2.6.1",
              "chromium-bidi": "0.11.0",
              "cosmiconfig": "^9.0.0",
              "devtools-protocol": "0.0.1367902",
              "puppeteer-core": "23.11.1",
              "typed-query-selector": "^2.12.0"
            },
            "bin": {
              "puppeteer": "lib/cjs/puppeteer/node/cli.js"
            },
            "engines": {
              "node": ">=18"
            }
          },
          "node_modules/puppeteer-core": {
            "version": "23.11.1",
            "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz",
            "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==",
            "license": "Apache-2.0",
            "dependencies": {
              "@puppeteer/browsers": "2.6.1",
              "chromium-bidi": "0.11.0",
              "debug": "^4.4.0",
              "devtools-protocol": "0.0.1367902",
              "typed-query-selector": "^2.12.0",
              "ws": "^8.18.0"
            },
            "engines": {
              "node": ">=18"
            }
          },
          "node_modules/require-directory": {
            "version": "2.1.1",
            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
            "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
            "license": "MIT",
            "engines": {
              "node": ">=0.10.0"
            }
          },
          "node_modules/resolve-from": {
            "version": "4.0.0",
            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
            "license": "MIT",
            "engines": {
              "node": ">=4"
            }
          },
          "node_modules/semver": {
            "version": "7.7.3",
            "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
            "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
            "license": "ISC",
            "bin": {
              "semver": "bin/semver.js"
            },
            "engines": {
              "node": ">=10"
            }
          },
          "node_modules/smart-buffer": {
            "version": "4.2.0",
            "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
            "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
            "license": "MIT",
            "engines": {
              "node": ">= 6.0.0",
              "npm": ">= 3.0.0"
            }
          },
          "node_modules/socks": {
            "version": "2.8.7",
            "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
            "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
            "license": "MIT",
            "dependencies": {
              "ip-address": "^10.0.1",
              "smart-buffer": "^4.2.0"
            },
            "engines": {
              "node": ">= 10.0.0",
              "npm": ">= 3.0.0"
            }
          },
          "node_modules/socks-proxy-agent": {
            "version": "8.0.5",
            "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
            "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
            "license": "MIT",
            "dependencies": {
              "agent-base": "^7.1.2",
              "debug": "^4.3.4",
              "socks": "^2.8.3"
            },
            "engines": {
              "node": ">= 14"
            }
          },
          "node_modules/source-map": {
            "version": "0.6.1",
            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
            "license": "BSD-3-Clause",
            "optional": true,
            "engines": {
              "node": ">=0.10.0"
            }
          },
          "node_modules/streamx": {
            "version": "2.23.0",
            "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
            "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
            "license": "MIT",
            "dependencies": {
              "events-universal": "^1.0.0",
              "fast-fifo": "^1.3.2",
              "text-decoder": "^1.1.0"
            }
          },
          "node_modules/string-width": {
            "version": "4.2.3",
            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
            "license": "MIT",
            "dependencies": {
              "emoji-regex": "^8.0.0",
              "is-fullwidth-code-point": "^3.0.0",
              "strip-ansi": "^6.0.1"
            },
            "engines": {
              "node": ">=8"
            }
          },
          "node_modules/strip-ansi": {
            "version": "6.0.1",
            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
            "license": "MIT",
            "dependencies": {
              "ansi-regex": "^5.0.1"
            },
            "engines": {
              "node": ">=8"
            }
          },
          "node_modules/tar-fs": {
            "version": "3.1.1",
            "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
            "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
            "license": "MIT",
            "dependencies": {
              "pump": "^3.0.0",
              "tar-stream": "^3.1.5"
            },
            "optionalDependencies": {
              "bare-fs": "^4.0.1",
              "bare-path": "^3.0.0"
            }
          },
          "node_modules/tar-stream": {
            "version": "3.1.7",
            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
            "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
            "license": "MIT",
            "dependencies": {
              "b4a": "^1.6.4",
              "fast-fifo": "^1.2.0",
              "streamx": "^2.15.0"
            }
          },
          "node_modules/text-decoder": {
            "version": "1.2.3",
            "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
            "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
            "license": "Apache-2.0",
            "dependencies": {
              "b4a": "^1.6.4"
            }
          },
          "node_modules/through": {
            "version": "2.3.8",
            "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
            "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
            "license": "MIT"
          },
          "node_modules/tslib": {
            "version": "2.8.1",
            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
            "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
            "license": "0BSD"
          },
          "node_modules/typed-query-selector": {
            "version": "2.12.0",
            "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
            "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
            "license": "MIT"
          },
          "node_modules/unbzip2-stream": {
            "version": "1.4.3",
            "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
            "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
            "license": "MIT",
            "dependencies": {
              "buffer": "^5.2.1",
              "through": "^2.3.8"
            }
          },
          "node_modules/undici-types": {
            "version": "7.16.0",
            "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
            "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
            "license": "MIT",
            "optional": true
          },
          "node_modules/wrap-ansi": {
            "version": "7.0.0",
            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
            "license": "MIT",
            "dependencies": {
              "ansi-styles": "^4.0.0",
              "string-width": "^4.1.0",
              "strip-ansi": "^6.0.0"
            },
            "engines": {
              "node": ">=10"
            },
            "funding": {
              "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
            }
          },
          "node_modules/wrappy": {
            "version": "1.0.2",
            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
            "license": "ISC"
          },
          "node_modules/ws": {
            "version": "8.18.3",
            "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
            "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
            "license": "MIT",
            "engines": {
              "node": ">=10.0.0"
            },
            "peerDependencies": {
              "bufferutil": "^4.0.1",
              "utf-8-validate": ">=5.0.2"
            },
            "peerDependenciesMeta": {
              "bufferutil": {
                "optional": true
              },
              "utf-8-validate": {
                "optional": true
              }
            }
          },
          "node_modules/y18n": {
            "version": "5.0.8",
            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
            "license": "ISC",
            "engines": {
              "node": ">=10"
            }
          },
          "node_modules/yargs": {
            "version": "17.7.2",
            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
            "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
            "license": "MIT",
            "dependencies": {
              "cliui": "^8.0.1",
              "escalade": "^3.1.1",
              "get-caller-file": "^2.0.5",
              "require-directory": "^2.1.1",
              "string-width": "^4.2.3",
              "y18n": "^5.0.5",
              "yargs-parser": "^21.1.1"
            },
            "engines": {
              "node": ">=12"
            }
          },
          "node_modules/yargs-parser": {
            "version": "21.1.1",
            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
            "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
            "license": "ISC",
            "engines": {
              "node": ">=12"
            }
          },
          "node_modules/yauzl": {
            "version": "2.10.0",
            "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
            "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
            "license": "MIT",
            "dependencies": {
              "buffer-crc32": "~0.2.3",
              "fd-slicer": "~1.1.0"
            }
          },
          "node_modules/zod": {
            "version": "3.23.8",
            "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
            "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
            "license": "MIT",
            "funding": {
              "url": "https://github.com/sponsors/colinhacks"
            }
          }
        }
      }
      
    • package.json 321 B
      {
        "name": "chrome-devtools-scripts",
        "version": "1.0.0",
        "description": "Browser automation scripts for Chrome DevTools Agent Skill",
        "type": "module",
        "scripts": {},
        "dependencies": {
          "puppeteer": "^24.15.0",
          "debug": "^4.4.0",
          "yargs": "^17.7.2"
        },
        "engines": {
          "node": ">=18.0.0"
        }
      }
      
    • performance.js 3.8 KB
      #!/usr/bin/env node
      /**
       * Measure performance metrics and record trace
       * Usage: node performance.js --url https://example.com [--trace trace.json] [--metrics]
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      import fs from 'fs/promises';
      
      async function measurePerformance() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.url) {
          outputError(new Error('--url is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          // Start tracing if requested
          if (args.trace) {
            await page.tracing.start({
              path: args.trace,
              categories: [
                'devtools.timeline',
                'disabled-by-default-devtools.timeline',
                'disabled-by-default-devtools.timeline.frame'
              ]
            });
          }
      
          // Navigate
          await page.goto(args.url, {
            waitUntil: 'networkidle2'
          });
      
          // Stop tracing
          if (args.trace) {
            await page.tracing.stop();
          }
      
          // Get performance metrics
          const metrics = await page.metrics();
      
          // Get Core Web Vitals
          const vitals = await page.evaluate(() => {
            return new Promise((resolve) => {
              const vitals = {
                LCP: null,
                FID: null,
                CLS: 0,
                FCP: null,
                TTFB: null
              };
      
              // LCP
              try {
                new PerformanceObserver((list) => {
                  const entries = list.getEntries();
                  if (entries.length > 0) {
                    const lastEntry = entries[entries.length - 1];
                    vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
                  }
                }).observe({ entryTypes: ['largest-contentful-paint'], buffered: true });
              } catch (e) {}
      
              // CLS
              try {
                new PerformanceObserver((list) => {
                  list.getEntries().forEach((entry) => {
                    if (!entry.hadRecentInput) {
                      vitals.CLS += entry.value;
                    }
                  });
                }).observe({ entryTypes: ['layout-shift'], buffered: true });
              } catch (e) {}
      
              // FCP
              try {
                const paintEntries = performance.getEntriesByType('paint');
                const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
                if (fcpEntry) {
                  vitals.FCP = fcpEntry.startTime;
                }
              } catch (e) {}
      
              // TTFB
              try {
                const [navigationEntry] = performance.getEntriesByType('navigation');
                if (navigationEntry) {
                  vitals.TTFB = navigationEntry.responseStart - navigationEntry.requestStart;
                }
              } catch (e) {}
      
              // Wait a bit for metrics to stabilize
              setTimeout(() => resolve(vitals), 1000);
            });
          });
      
          // Get resource timing
          const resources = await page.evaluate(() => {
            return performance.getEntriesByType('resource').map(r => ({
              name: r.name,
              type: r.initiatorType,
              duration: r.duration,
              size: r.transferSize,
              startTime: r.startTime
            }));
          });
      
          const result = {
            success: true,
            url: page.url(),
            metrics: {
              ...metrics,
              JSHeapUsedSizeMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
              JSHeapTotalSizeMB: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2)
            },
            vitals: vitals,
            resources: {
              count: resources.length,
              totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
              items: args.resources === 'true' ? resources : undefined
            }
          };
      
          if (args.trace) {
            result.trace = args.trace;
          }
      
          outputJSON(result);
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          outputError(error);
        }
      }
      
      measurePerformance();
      
    • PERSISTENT-BROWSER.md 3.1 KB
      # Persistent Browser Mode
      
      These scripts enable you to launch Chrome once and keep it open for multiple commands, making it easier to interact with authenticated sessions and multi-step workflows.
      
      ## Quick Start
      
      ### 1. Launch Persistent Browser
      
      ```bash
      # Launch browser (visible window)
      node launch-persistent.js --headless=false
      
      # Launch with initial URL
      node launch-persistent.js --headless=false --url=https://example.com/login
      
      # Launch headless
      node launch-persistent.js
      ```
      
      The browser will stay open and print a message confirming it's ready.
      
      ### 2. Run Commands
      
      All existing scripts will automatically connect to the persistent browser:
      
      ```bash
      # Navigate to a page
      node navigate.js --url https://example.com/dashboard
      
      # Take a screenshot
      node screenshot.js --output ./screenshot.png
      
      # Run JavaScript
      node evaluate.js --script "document.title"
      
      # Get page snapshot
      node snapshot.js
      
      # Fill forms
      node fill.js --selector "#username" --value "user@example.com"
      node click.js --selector "button[type=submit]"
      ```
      
      ### 3. Close Browser
      
      ```bash
      # Close the persistent browser
      node close-persistent.js
      
      # Or press Ctrl+C in the launch-persistent.js terminal
      ```
      
      ## How It Works
      
      1. **launch-persistent.js** starts Chrome with remote debugging enabled and saves the WebSocket endpoint to `.browser-endpoint`
      2. All other scripts check for this file first and connect to the existing browser if found
      3. If no persistent browser exists, scripts fall back to launching their own temporary browser (original behavior)
      
      ## Use Cases
      
      ### Authenticated Sessions
      ```bash
      # Launch browser
      node launch-persistent.js --headless=false --url=https://app.example.com/login
      
      # Manually log in through the visible browser window
      # Then run commands on authenticated pages
      
      node navigate.js --url=https://app.example.com/dashboard
      node screenshot.js --output ./dashboard.png
      node evaluate.js --script "document.querySelector('.user-name').textContent"
      ```
      
      ### Multi-Step Workflows
      ```bash
      # Launch browser
      node launch-persistent.js --headless=false
      
      # Fill out multi-page form
      node navigate.js --url=https://example.com/signup
      node fill.js --selector "#email" --value "user@example.com"
      node click.js --selector ".next-button"
      # Wait for page to load, then continue...
      node fill.js --selector "#password" --value "secret123"
      node click.js --selector ".submit-button"
      ```
      
      ### Accessibility Audits on Live Pages
      ```bash
      # Launch and navigate manually to complex authenticated state
      node launch-persistent.js --headless=false
      
      # After manual navigation/interaction, run audits
      node evaluate.js --script "/* accessibility check script */"
      node screenshot.js --output ./audit.png
      ```
      
      ## Advantages
      
      - **Manual interaction**: Log in, navigate, or manipulate the page manually
      - **Persistent state**: Cookies, localStorage, and session state preserved
      - **Faster iterations**: No browser startup delay for each command
      - **Real-world testing**: Test on actual authenticated or complex application states
      
      ## Backwards Compatibility
      
      All existing scripts work exactly as before if no persistent browser is running. The enhancement is completely opt-in.
      
    • README.md 5.2 KB
      # Chrome DevTools Scripts
      
      CLI scripts for browser automation using Puppeteer.
      
      **CRITICAL**: Always check `pwd` before running scripts.
      
      ## Installation
      
      ### Quick Install
      
      ```bash
      pwd  # Should show current working directory
      cd .claude/skills/chrome-devtools/scripts
      ./install.sh  # Auto-checks dependencies and installs
      ```
      
      ### Manual Installation
      
      **Linux/WSL** - Install system dependencies first:
      ```bash
      ./install-deps.sh  # Auto-detects OS (Ubuntu, Debian, Fedora, etc.)
      ```
      
      Or manually:
      ```bash
      sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1
      ```
      
      **All platforms** - Install Node dependencies:
      ```bash
      npm install
      ```
      
      ## Scripts
      
      **CRITICAL**: Always check `pwd` before running scripts.
      
      ### navigate.js
      Navigate to a URL.
      
      ```bash
      node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
      ```
      
      ### screenshot.js
      Take a screenshot with automatic compression.
      
      **Important**: Always save screenshots to `./docs/screenshots` directory.
      
      ```bash
      node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
      ```
      
      **Automatic Compression**: Screenshots >5MB are automatically compressed using ImageMagick to ensure compatibility with Gemini API and Claude Code. Install ImageMagick for this feature:
      - macOS: `brew install imagemagick`
      - Linux: `sudo apt-get install imagemagick`
      
      Options:
      - `--max-size N` - Custom size threshold in MB (default: 5)
      - `--no-compress` - Disable automatic compression
      - `--format png|jpeg` - Output format (default: png)
      - `--quality N` - JPEG quality 0-100 (default: auto)
      
      ### click.js
      Click an element.
      
      ```bash
      node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
      ```
      
      ### fill.js
      Fill form fields.
      
      ```bash
      node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]
      ```
      
      ### evaluate.js
      Execute JavaScript in page context.
      
      ```bash
      node evaluate.js --script "document.title" [--url https://example.com]
      ```
      
      ### snapshot.js
      Get DOM snapshot with interactive elements.
      
      ```bash
      node snapshot.js [--url https://example.com] [--output snapshot.json]
      ```
      
      ### console.js
      Monitor console messages.
      
      ```bash
      node console.js --url https://example.com [--types error,warn] [--duration 5000]
      ```
      
      ### network.js
      Monitor network requests.
      
      ```bash
      node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
      ```
      
      ### performance.js
      Measure performance metrics and record trace.
      
      ```bash
      node performance.js --url https://example.com [--trace trace.json] [--metrics] [--resources true]
      ```
      
      ## Common Options
      
      - `--headless false` - Show browser window
      - `--close false` - Keep browser open
      - `--timeout 30000` - Set timeout in milliseconds
      - `--wait-until networkidle2` - Wait strategy (load, domcontentloaded, networkidle0, networkidle2)
      
      ## Selector Support
      
      Scripts that accept `--selector` (click.js, fill.js, screenshot.js) support both **CSS** and **XPath** selectors.
      
      ### CSS Selectors (Default)
      
      ```bash
      # Element tag
      node click.js --selector "button" --url https://example.com
      
      # Class selector
      node click.js --selector ".btn-submit" --url https://example.com
      
      # ID selector
      node fill.js --selector "#email" --value "user@example.com" --url https://example.com
      
      # Attribute selector
      node click.js --selector 'button[type="submit"]' --url https://example.com
      
      # Complex selector
      node screenshot.js --selector "div.container > button.btn-primary" --output btn.png
      ```
      
      ### XPath Selectors
      
      XPath selectors start with `/` or `(//` and are automatically detected:
      
      ```bash
      # Text matching - exact
      node click.js --selector '//button[text()="Submit"]' --url https://example.com
      
      # Text matching - contains
      node click.js --selector '//button[contains(text(),"Submit")]' --url https://example.com
      
      # Attribute matching
      node fill.js --selector '//input[@type="email"]' --value "user@example.com"
      
      # Multiple conditions
      node click.js --selector '//button[@type="submit" and contains(text(),"Save")]'
      
      # Descendant selection
      node screenshot.js --selector '//div[@class="modal"]//button[@class="close"]' --output modal.png
      
      # Nth element
      node click.js --selector '(//button)[2]'  # Second button on page
      ```
      
      ### Discovering Selectors
      
      Use `snapshot.js` to discover correct selectors:
      
      ```bash
      # Get all interactive elements
      node snapshot.js --url https://example.com | jq '.elements[]'
      
      # Find buttons
      node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
      
      # Find inputs
      node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="INPUT")'
      ```
      
      ### Security
      
      XPath selectors are validated to prevent injection attacks. The following patterns are blocked:
      - `javascript:`
      - `<script`
      - `onerror=`, `onload=`, `onclick=`
      - `eval(`, `Function(`, `constructor(`
      
      Selectors exceeding 1000 characters are rejected (DoS prevention).
      
      ## Output Format
      
      All scripts output JSON to stdout:
      
      ```json
      {
        "success": true,
        "url": "https://example.com",
        "title": "Example Domain",
        ...
      }
      ```
      
      Errors are output to stderr:
      
      ```json
      {
        "success": false,
        "error": "Error message",
        "stack": "..."
      }
      ```
      
    • screenshot.js 5.9 KB
      #!/usr/bin/env node
      /**
       * Take a screenshot
       * Usage: node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
       * Supports both CSS and XPath selectors:
       *   - CSS: node screenshot.js --selector ".main-content" --output page.png
       *   - XPath: node screenshot.js --selector "//div[@class='main-content']" --output page.png
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      import { parseSelector, getElement, enhanceError } from './lib/selector.js';
      import fs from 'fs/promises';
      import path from 'path';
      import { execSync } from 'child_process';
      
      /**
       * Compress image using ImageMagick if it exceeds max size
       * @param {string} filePath - Path to the image file
       * @param {number} maxSizeMB - Maximum file size in MB (default: 5)
       * @returns {Promise<{compressed: boolean, originalSize: number, finalSize: number}>}
       */
      async function compressImageIfNeeded(filePath, maxSizeMB = 5) {
        const stats = await fs.stat(filePath);
        const originalSize = stats.size;
        const maxSizeBytes = maxSizeMB * 1024 * 1024;
      
        if (originalSize <= maxSizeBytes) {
          return { compressed: false, originalSize, finalSize: originalSize };
        }
      
        try {
          // Check if ImageMagick is available
          try {
            execSync('magick -version', { stdio: 'pipe' });
          } catch {
            try {
              execSync('convert -version', { stdio: 'pipe' });
            } catch {
              console.error('Warning: ImageMagick not found. Install it to enable automatic compression.');
              return { compressed: false, originalSize, finalSize: originalSize };
            }
          }
      
          const ext = path.extname(filePath).toLowerCase();
          const tempPath = filePath.replace(ext, `.temp${ext}`);
      
          // Determine compression strategy based on file type
          let compressionCmd;
          if (ext === '.png') {
            // For PNG: resize and compress with quality
            compressionCmd = `magick "${filePath}" -strip -resize 90% -quality 85 "${tempPath}"`;
          } else if (ext === '.jpg' || ext === '.jpeg') {
            // For JPEG: compress with quality and progressive
            compressionCmd = `magick "${filePath}" -strip -quality 80 -interlace Plane "${tempPath}"`;
          } else {
            // For other formats: convert to JPEG with compression
            compressionCmd = `magick "${filePath}" -strip -quality 80 "${tempPath.replace(ext, '.jpg')}"`;
          }
      
          // Try compression
          execSync(compressionCmd, { stdio: 'pipe' });
      
          const compressedStats = await fs.stat(tempPath);
          const compressedSize = compressedStats.size;
      
          // If still too large, try more aggressive compression
          if (compressedSize > maxSizeBytes) {
            const finalPath = filePath.replace(ext, `.final${ext}`);
            let aggressiveCmd;
      
            if (ext === '.png') {
              aggressiveCmd = `magick "${tempPath}" -strip -resize 75% -quality 70 "${finalPath}"`;
            } else {
              aggressiveCmd = `magick "${tempPath}" -strip -quality 60 -sampling-factor 4:2:0 "${finalPath}"`;
            }
      
            execSync(aggressiveCmd, { stdio: 'pipe' });
            await fs.unlink(tempPath);
            await fs.rename(finalPath, filePath);
          } else {
            await fs.rename(tempPath, filePath);
          }
      
          const finalStats = await fs.stat(filePath);
          return { compressed: true, originalSize, finalSize: finalStats.size };
        } catch (error) {
          console.error('Compression error:', error.message);
          // If compression fails, keep original file
          try {
            const tempPath = filePath.replace(path.extname(filePath), '.temp' + path.extname(filePath));
            await fs.unlink(tempPath).catch(() => {});
          } catch {}
          return { compressed: false, originalSize, finalSize: originalSize };
        }
      }
      
      async function screenshot() {
        const args = parseArgs(process.argv.slice(2));
      
        if (!args.output) {
          outputError(new Error('--output is required'));
          return;
        }
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          // Navigate if URL provided
          if (args.url) {
            await page.goto(args.url, {
              waitUntil: args['wait-until'] || 'networkidle2'
            });
          }
      
          const screenshotOptions = {
            path: args.output,
            type: args.format || 'png',
            fullPage: args['full-page'] === 'true'
          };
      
          if (args.quality) {
            screenshotOptions.quality = parseInt(args.quality);
          }
      
          let buffer;
          if (args.selector) {
            // Parse and validate selector
            const parsed = parseSelector(args.selector);
      
            // Get element based on selector type
            const element = await getElement(page, parsed);
            if (!element) {
              throw new Error(`Element not found: ${args.selector}`);
            }
            buffer = await element.screenshot(screenshotOptions);
          } else {
            buffer = await page.screenshot(screenshotOptions);
          }
      
          const result = {
            success: true,
            output: path.resolve(args.output),
            size: buffer.length,
            url: page.url()
          };
      
          // Compress image if needed (unless --no-compress flag is set)
          if (args['no-compress'] !== 'true') {
            const maxSize = args['max-size'] ? parseFloat(args['max-size']) : 5;
            const compressionResult = await compressImageIfNeeded(args.output, maxSize);
      
            if (compressionResult.compressed) {
              result.compressed = true;
              result.originalSize = compressionResult.originalSize;
              result.size = compressionResult.finalSize;
              result.compressionRatio = ((1 - compressionResult.finalSize / compressionResult.originalSize) * 100).toFixed(2) + '%';
            }
          }
      
          outputJSON(result);
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          // Enhance error message if selector-related
          if (args.selector) {
            const enhanced = enhanceError(error, args.selector);
            outputError(enhanced);
          } else {
            outputError(error);
          }
          process.exit(1);
        }
      }
      
      screenshot();
      
    • snapshot.js 3.4 KB
      #!/usr/bin/env node
      /**
       * Get DOM snapshot with selectors
       * Usage: node snapshot.js [--url https://example.com] [--output snapshot.json]
       */
      import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
      import fs from 'fs/promises';
      
      async function snapshot() {
        const args = parseArgs(process.argv.slice(2));
      
        try {
          const browser = await getBrowser({
            headless: args.headless !== 'false'
          });
      
          const page = await getPage(browser);
      
          // Navigate if URL provided
          if (args.url) {
            await page.goto(args.url, {
              waitUntil: args['wait-until'] || 'networkidle2'
            });
          }
      
          // Get interactive elements with metadata
          const elements = await page.evaluate(() => {
            const interactiveSelectors = [
              'a[href]',
              'button',
              'input',
              'textarea',
              'select',
              '[onclick]',
              '[role="button"]',
              '[role="link"]',
              '[contenteditable]'
            ];
      
            const elements = [];
            const selector = interactiveSelectors.join(', ');
            const nodes = document.querySelectorAll(selector);
      
            nodes.forEach((el, index) => {
              const rect = el.getBoundingClientRect();
      
              // Generate unique selector
              let uniqueSelector = '';
              if (el.id) {
                uniqueSelector = `#${el.id}`;
              } else if (el.className) {
                const classes = Array.from(el.classList).join('.');
                uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`;
              } else {
                uniqueSelector = el.tagName.toLowerCase();
              }
      
              elements.push({
                index: index,
                tagName: el.tagName.toLowerCase(),
                type: el.type || null,
                id: el.id || null,
                className: el.className || null,
                name: el.name || null,
                value: el.value || null,
                text: el.textContent?.trim().substring(0, 100) || null,
                href: el.href || null,
                selector: uniqueSelector,
                xpath: getXPath(el),
                visible: rect.width > 0 && rect.height > 0,
                position: {
                  x: rect.x,
                  y: rect.y,
                  width: rect.width,
                  height: rect.height
                }
              });
            });
      
            function getXPath(element) {
              if (element.id) {
                return `//*[@id="${element.id}"]`;
              }
              if (element === document.body) {
                return '/html/body';
              }
              let ix = 0;
              const siblings = element.parentNode?.childNodes || [];
              for (let i = 0; i < siblings.length; i++) {
                const sibling = siblings[i];
                if (sibling === element) {
                  return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
                }
                if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
                  ix++;
                }
              }
              return '';
            }
      
            return elements;
          });
      
          const result = {
            success: true,
            url: page.url(),
            title: await page.title(),
            elementCount: elements.length,
            elements: elements
          };
      
          if (args.output) {
            await fs.writeFile(args.output, JSON.stringify(result, null, 2));
            outputJSON({
              success: true,
              output: args.output,
              elementCount: elements.length
            });
          } else {
            outputJSON(result);
          }
      
          if (args.close !== 'false') {
            await closeBrowser();
          }
        } catch (error) {
          outputError(error);
        }
      }
      
      snapshot();
      
  • .gitignore 17 B · in bundle
  • SKILL.md 12.1 KB
    ---
    name: chrome-devtools
    description: Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging.
    license: Apache-2.0
    ---
    
    # Chrome DevTools Agent Skill
    
    Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing.
    
    ## Quick Start
    
    **CRITICAL**: Always check `pwd` before running scripts.
    
    ### Installation
    
    #### Step 1: Install System Dependencies (Linux/WSL only)
    
    On Linux/WSL, Chrome requires system libraries. Install them first:
    
    ```bash
    pwd  # Should show current working directory
    cd .claude/skills/chrome-devtools/scripts
    ./install-deps.sh  # Auto-detects OS and installs required libs
    ```
    
    Supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro
    
    **macOS/Windows**: Skip this step (dependencies bundled with Chrome)
    
    #### Step 2: Install Node Dependencies
    
    ```bash
    npm install  # Installs puppeteer, debug, yargs
    ```
    
    #### Step 3: Install ImageMagick (Optional, Recommended)
    
    ImageMagick enables automatic screenshot compression to keep files under 5MB:
    
    **macOS:**
    ```bash
    brew install imagemagick
    ```
    
    **Ubuntu/Debian/WSL:**
    ```bash
    sudo apt-get install imagemagick
    ```
    
    **Verify:**
    ```bash
    magick -version  # or: convert -version
    ```
    
    Without ImageMagick, screenshots >5MB will not be compressed (may fail to load in Gemini/Claude).
    
    ### Test
    ```bash
    node navigate.js --url https://example.com
    # Output: {"success": true, "url": "https://example.com", "title": "Example Domain"}
    ```
    
    ## Available Scripts
    
    All scripts are in `.claude/skills/chrome-devtools/scripts/`
    
    **CRITICAL**: Always check `pwd` before running scripts.
    
    ### Script Usage
    - `./scripts/README.md`
    
    ### Core Automation
    - `navigate.js` - Navigate to URLs
    - `screenshot.js` - Capture screenshots (full page or element)
    - `click.js` - Click elements
    - `fill.js` - Fill form fields
    - `evaluate.js` - Execute JavaScript in page context
    
    ### Analysis & Monitoring
    - `snapshot.js` - Extract interactive elements with metadata
    - `console.js` - Monitor console messages/errors
    - `network.js` - Track HTTP requests/responses
    - `performance.js` - Measure Core Web Vitals + record traces
    
    ## Usage Patterns
    
    ### Single Command
    ```bash
    pwd  # Should show current working directory
    cd .claude/skills/chrome-devtools/scripts
    node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
    ```
    **Important**: Always save screenshots to `./docs/screenshots` directory.
    
    ### Automatic Image Compression
    Screenshots are **automatically compressed** if they exceed 5MB to ensure compatibility with Gemini API and Claude Code (which have 5MB limits). This uses ImageMagick internally:
    
    ```bash
    # Default: auto-compress if >5MB
    node screenshot.js --url https://example.com --output page.png
    
    # Custom size threshold (e.g., 3MB)
    node screenshot.js --url https://example.com --output page.png --max-size 3
    
    # Disable compression
    node screenshot.js --url https://example.com --output page.png --no-compress
    ```
    
    **Compression behavior:**
    - PNG: Resizes to 90% + quality 85 (or 75% + quality 70 if still too large)
    - JPEG: Quality 80 + progressive encoding (or quality 60 if still too large)
    - Other formats: Converted to JPEG with compression
    - Requires ImageMagick installed (see imagemagick skill)
    
    **Output includes compression info:**
    ```json
    {
      "success": true,
      "output": "/path/to/page.png",
      "compressed": true,
      "originalSize": 8388608,
      "size": 3145728,
      "compressionRatio": "62.50%",
      "url": "https://example.com"
    }
    ```
    
    ### Chain Commands (reuse browser)
    ```bash
    # Keep browser open with --close false
    node navigate.js --url https://example.com/login --close false
    node fill.js --selector "#email" --value "user@example.com" --close false
    node fill.js --selector "#password" --value "secret" --close false
    node click.js --selector "button[type=submit]"
    ```
    
    ### Parse JSON Output
    ```bash
    # Extract specific fields with jq
    node performance.js --url https://example.com | jq '.vitals.LCP'
    
    # Save to file
    node network.js --url https://example.com --output /tmp/requests.json
    ```
    
    ## Execution Protocol
    
    ### Working Directory Verification
    
    BEFORE executing any script:
    1. Check current working directory with `pwd`
    2. Verify in `.claude/skills/chrome-devtools/scripts/` directory
    3. If wrong directory, `cd` to correct location
    4. Use absolute paths for all output files
    
    Example:
    ```bash
    pwd  # Should show: .../chrome-devtools/scripts
    # If wrong:
    cd .claude/skills/chrome-devtools/scripts
    ```
    
    ### Output Validation
    
    AFTER screenshot/capture operations:
    1. Verify file created with `ls -lh <output-path>`
    2. Read screenshot using Read tool to confirm content
    3. Check JSON output for success:true
    4. Report file size and compression status
    
    Example:
    ```bash
    node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
    ls -lh ./docs/screenshots/page.png  # Verify file exists
    # Then use Read tool to visually inspect
    ```
    
    5. Restart working directory to the project root.
    
    ### Error Recovery
    
    If script fails:
    1. Check error message for selector issues
    2. Use snapshot.js to discover correct selectors
    3. Try XPath selector if CSS selector fails
    4. Verify element is visible and interactive
    
    Example:
    ```bash
    # CSS selector fails
    node click.js --url https://example.com --selector ".btn-submit"
    # Error: waiting for selector ".btn-submit" failed
    
    # Discover correct selector
    node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
    
    # Try XPath
    node click.js --url https://example.com --selector "//button[contains(text(),'Submit')]"
    ```
    
    ### Common Mistakes
    
    ❌ Wrong working directory → output files go to wrong location
    ❌ Skipping output validation → silent failures
    ❌ Using complex CSS selectors without testing → selector errors
    ❌ Not checking element visibility → timeout errors
    
    ✅ Always verify `pwd` before running scripts
    ✅ Always validate output after screenshots
    ✅ Use snapshot.js to discover selectors
    ✅ Test selectors with simple commands first
    
    ## Common Workflows
    
    ### Web Scraping
    ```bash
    node evaluate.js --url https://example.com --script "
      Array.from(document.querySelectorAll('.item')).map(el => ({
        title: el.querySelector('h2')?.textContent,
        link: el.querySelector('a')?.href
      }))
    " | jq '.result'
    ```
    
    ### Performance Testing
    ```bash
    PERF=$(node performance.js --url https://example.com)
    LCP=$(echo $PERF | jq '.vitals.LCP')
    if (( $(echo "$LCP < 2500" | bc -l) )); then
      echo "✓ LCP passed: ${LCP}ms"
    else
      echo "✗ LCP failed: ${LCP}ms"
    fi
    ```
    
    ### Form Automation
    ```bash
    node fill.js --url https://example.com --selector "#search" --value "query" --close false
    node click.js --selector "button[type=submit]"
    ```
    
    ### Error Monitoring
    ```bash
    node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount'
    ```
    
    ## Script Options
    
    All scripts support:
    - `--headless false` - Show browser window
    - `--close false` - Keep browser open for chaining
    - `--timeout 30000` - Set timeout (milliseconds)
    - `--wait-until networkidle2` - Wait strategy
    
    See `./scripts/README.md` for complete options.
    
    ## Output Format
    
    All scripts output JSON to stdout:
    ```json
    {
      "success": true,
      "url": "https://example.com",
      ... // script-specific data
    }
    ```
    
    Errors go to stderr:
    ```json
    {
      "success": false,
      "error": "Error message"
    }
    ```
    
    ## Finding Elements
    
    Use `snapshot.js` to discover selectors:
    ```bash
    node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'
    ```
    
    ## Troubleshooting
    
    ### Common Errors
    
    **"Cannot find package 'puppeteer'"**
    - Run: `npm install` in the scripts directory
    
    **"error while loading shared libraries: libnss3.so"** (Linux/WSL)
    - Missing system dependencies
    - Fix: Run `./install-deps.sh` in scripts directory
    - Manual install: `sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1`
    
    **"Failed to launch the browser process"**
    - Check system dependencies installed (Linux/WSL)
    - Verify Chrome downloaded: `ls ~/.cache/puppeteer`
    - Try: `npm rebuild` then `npm install`
    
    **Chrome not found**
    - Puppeteer auto-downloads Chrome during `npm install`
    - If failed, manually trigger: `npx puppeteer browsers install chrome`
    
    ### Script Issues
    
    **Element not found**
    - Get snapshot first to find correct selector: `node snapshot.js --url <url>`
    
    **Script hangs**
    - Increase timeout: `--timeout 60000`
    - Change wait strategy: `--wait-until load` or `--wait-until domcontentloaded`
    
    **Blank screenshot**
    - Wait for page load: `--wait-until networkidle2`
    - Increase timeout: `--timeout 30000`
    
    **Permission denied on scripts**
    - Make executable: `chmod +x *.sh`
    
    **Screenshot too large (>5MB)**
    - Install ImageMagick for automatic compression
    - Manually set lower threshold: `--max-size 3`
    - Use JPEG format instead of PNG: `--format jpeg --quality 80`
    - Capture specific element instead of full page: `--selector .main-content`
    
    **Compression not working**
    - Verify ImageMagick installed: `magick -version` or `convert -version`
    - Check file was actually compressed in output JSON: `"compressed": true`
    - For very large pages, use `--selector` to capture only needed area
    
    ## Resilience and Subagent Stability
    
    When using the browser subagent or Puppeteer scripts, you may encounter connectivity issues (e.g., "connection is reset") or tab loss. Follow these guardrails for professional-grade reliability:
    
    ### 1. State Verification First
    **Rule**: Always call `list_pages` (or a similar verification script) at the start of a new subagent task or if an error occurs.
    - Do not assume the tab you just used is still active.
    - If a "Connection Reset" error occurs, it usually means the browser instance crashed or closed. Immediately attempt to re-launch or re-initiate the session.
    
    ### 2. Handling Authenticated Sessions
    **Pattern**: Use Persistent Browser Mode for sites requiring login or complex state.
    - Launch with `node launch-persistent.js --headless false`.
    - This ensures the browser process stays alive even if the subagent session times out.
    - Subsequent subagents can reconnect via the WebSocket endpoint in `.browser-endpoint`.
    
    ### 3. Progressive Handoff
    **Rule**: Use `--close false` consistently if the workflow requires multiple steps.
    - When finishing a subagent task, explicitly check if the browser should remain open for the next phase.
    - Provide the user with the URL and a screenshot of the final state to confirm continuity.
    
    ### 4. Error Recovery Strategy
    If a "Page not found" or "Target closed" error occurs:
    1. **List all open pages** to see if the tab was accidentally closed or navigated away.
    2. **Check for session tokens** (cookies/local storage) if the site allows session persistence.
    3. **Re-navigate** to the last known URL if the tab is truly lost.
    4. **Notify the user** clearly about the reset instead of retrying silently 10 times.
    
    ### 5. Interaction Heartbeats
    Before a complex `click` or `fill` sequence, take a small snapshot or evaluate a simple script (e.g., `() => document.readyState`) to "ping" the page. If it fails, trigger the recovery strategy early.
    
    ---
    
    ## Reference Documentation
    
    Detailed guides available in `./references/`:
    - [CDP Domains Reference](./references/cdp-domains.md) - 47 Chrome DevTools Protocol domains
    - [Puppeteer Quick Reference](./references/puppeteer-reference.md) - Complete Puppeteer API patterns
    - [Performance Analysis Guide](./references/performance-guide.md) - Core Web Vitals optimization
    
    ## Advanced Usage
    
    ### Custom Scripts
    Create custom scripts using shared library:
    ```javascript
    import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js';
    // Your automation logic
    ```
    
    ### Direct CDP Access
    ```javascript
    const client = await page.createCDPSession();
    await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
    ```
    
    See reference documentation for advanced patterns and complete API coverage.
    
    ## External Resources
    
    - [Puppeteer Documentation](https://pptr.dev/)
    - [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
    - [Scripts README](./scripts/README.md)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related