From 1c87823db17f1ee86e31286695ba042b1a209f9b Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 09:05:10 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 ++ README.md | 3 + plugin.lock.json | 117 ++++++++++++++++ skills/README.md | 151 ++++++++++++++++++++ skills/SKILL.md | 193 ++++++++++++++++++++++++++ skills/package.json | 37 +++++ skills/scripts/click.ts | 91 ++++++++++++ skills/scripts/console.ts | 146 +++++++++++++++++++ skills/scripts/evaluate.ts | 68 +++++++++ skills/scripts/fill.ts | 104 ++++++++++++++ skills/scripts/install.sh | 155 +++++++++++++++++++++ skills/scripts/navigate.ts | 64 +++++++++ skills/scripts/network.ts | 254 ++++++++++++++++++++++++++++++++++ skills/scripts/performance.ts | 231 +++++++++++++++++++++++++++++++ skills/scripts/screenshot.ts | 91 ++++++++++++ skills/scripts/snapshot.ts | 172 +++++++++++++++++++++++ skills/tsconfig.json | 31 +++++ 17 files changed, 1920 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/README.md create mode 100644 skills/SKILL.md create mode 100644 skills/package.json create mode 100644 skills/scripts/click.ts create mode 100644 skills/scripts/console.ts create mode 100644 skills/scripts/evaluate.ts create mode 100644 skills/scripts/fill.ts create mode 100755 skills/scripts/install.sh create mode 100644 skills/scripts/navigate.ts create mode 100644 skills/scripts/network.ts create mode 100644 skills/scripts/performance.ts create mode 100644 skills/scripts/screenshot.ts create mode 100644 skills/scripts/snapshot.ts create mode 100644 skills/tsconfig.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..ea89d6c --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "browser-devtools", + "description": "Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.", + "version": "1.0.0", + "author": { + "name": "Truong Vu", + "email": "vukhanhtruong@gmail.com" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ebf202 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# browser-devtools + +Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..631343f --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,117 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:vukhanhtruong/claude-rock:plugins/browser-devtools", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "e51c3cedbeb21afc81ced385efb0aa6976fbe6c3", + "treeHash": "d6f5e26dd43ebcca96337e1538ddd9cc8f44110c8507e8a1c92364008d41a098", + "generatedAt": "2025-11-28T10:28:57.788668Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "browser-devtools", + "description": "Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "38dfdbd3c10af36ebfabf1203c02270d2045aa34a7eee604137bec119b2f8028" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "76a52444895477ed9620f3df67409a932e116337aa2060661b3e3355fe241931" + }, + { + "path": "skills/README.md", + "sha256": "e91b6a4167fa0f016c96601eb90fa165ea01a8cf13a24a0e276ca44abbd95d42" + }, + { + "path": "skills/package.json", + "sha256": "83e99bfc5e0b167b06cfab769d9532a60c51dd10016211f82191d20e31e390db" + }, + { + "path": "skills/SKILL.md", + "sha256": "28416849905ae07e830944ef6bdd831ab79d43068307497b79e52f9273ab63c6" + }, + { + "path": "skills/tsconfig.json", + "sha256": "25f0d1e202386f6e181165e1e5f1ca90a019d79a944d7f4f9931b4c6932a5a8c" + }, + { + "path": "skills/scripts/snapshot.ts", + "sha256": "9cccadf3bb954a526b0e670934c7532f1c53cdb49cffcbdcd424af9de300a7b7" + }, + { + "path": "skills/scripts/performance.ts", + "sha256": "dc9bb6a83df02602055e68ed1eea643c7d65ea676b8df023781f8ea1ff81e3d9" + }, + { + "path": "skills/scripts/screenshot.ts", + "sha256": "b9538022a209daf9d4dce77d4b94fa9a353316640a94f73d5c53eef66e25847d" + }, + { + "path": "skills/scripts/evaluate.ts", + "sha256": "35fa37032f7710ae4ba027d3cbaa33a19149339dcaa94924971657ee0906fb01" + }, + { + "path": "skills/scripts/network.ts", + "sha256": "3b4c6d4895b05f25b8c7b24c0d913e8dc0a56e09ab992fef2ad26e8a84399c79" + }, + { + "path": "skills/scripts/install.sh", + "sha256": "d7e1a4602369fcf7e5bf1c567b5be584a6a75fcaf419049c0146ad68543082a0" + }, + { + "path": "skills/scripts/navigate.ts", + "sha256": "100da6c50337b7f78b59d61236d2d4f07a4700cf3076eb6f613b351a911e9601" + }, + { + "path": "skills/scripts/fill.ts", + "sha256": "c941da84266b958d02a10017ea9b38cc3591e839400320d75623bfcefcb4dd3d" + }, + { + "path": "skills/scripts/console.ts", + "sha256": "7d7c259a15158278f39e70e6d68105ab659665523e063a2b4b4b1ec5963866da" + }, + { + "path": "skills/scripts/click.ts", + "sha256": "f61e65228e6b2e09ee9b108d774260b9f97b890839bfe4eb698fcf29b29a65ee" + }, + { + "path": "skills/lib/browser.ts", + "sha256": "c67574013642161b989bd92c697c30328b4ea15ec83430fc55926ee674fbaaef" + }, + { + "path": "skills/lib/browser.d.ts.map", + "sha256": "4ce628498e2ae037e302afd194f5bc03c74da42534e8603b76af80d643d89de4" + }, + { + "path": "skills/lib/browser.js.map", + "sha256": "2bb10742fba305c7e02db956a45375fb0388b61fd1c5e38a29955f33b043eadc" + }, + { + "path": "skills/lib/browser.js", + "sha256": "b8a28a98bd8ddf437dd82a9cc1ef1e4cc8abc0946e7753233fc7db7a618e8914" + }, + { + "path": "skills/lib/browser.d.ts", + "sha256": "f6aa94f88755cb3ff4391e32d6830888cd1e668a9ca071b322bee19cdc964a26" + } + ], + "dirSha256": "d6f5e26dd43ebcca96337e1538ddd9cc8f44110c8507e8a1c92364008d41a098" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..f482f18 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,151 @@ +# Browser Devtools + +Use natural language prompts to debug and automate web browsers with Playwright. + +## How to Use + +### Basic Navigation + +- "Navigate to https://example.com and tell me the page title" +- "Open my local app at http://localhost:3000" +- "Check if my website is accessible" +- "Show me what https://github.com/vukhanhtruong/claude-rock looks like" + +### Screenshots + +- "Take a screenshot of the entire page at https://example.com" +- "Capture just the header section of my local app" +- "Screenshot the login form and save it as login-page.png" +- "Take a full page screenshot of my dashboard" + +### Finding Elements + +- "Show me all the buttons on this page" +- "What interactive elements are on https://example.com?" +- "Find all input fields and forms on the page" +- "List all links and their text content" +- "Show me the CSS selectors for all buttons" + +### Console Debugging + +- "Check for JavaScript errors on my page" +- "Monitor the console for warnings and errors for 10 seconds" +- "Show me all console messages while I interact with the site" +- "Are there any 404 errors or missing resources?" +- "Watch the console for network failures" + +### Network Monitoring + +- "Show me all API calls being made by this page" +- "Monitor network requests for the next 5 seconds" +- "Are there any failed requests or slow loading resources?" +- "Track all XHR and fetch requests" +- "Show me the largest resources loading on the page" + +### Performance Analysis + +- "Check the Core Web Vitals for my site" +- "What's the LCP (Largest Contentful Paint) for this page?" +- "Is my site performing well? Give me the performance metrics" +- "Run a performance audit and show me the results" +- "Compare performance between my staging and production sites" + +### Form Testing + +- "Fill out the login form with test credentials" +- "Enter 'test@example.com' in the email field and 'password123' in the password field" +- "Submit the contact form and see what happens" +- "Test the search functionality by entering 'query' and clicking search" +- "Fill out all required fields in the registration form" + +### Element Interaction + +- "Click the submit button" +- "Click on the menu toggle" +- "Click the 'Add to Cart' button and wait for the result" +- "Interact with the dropdown menu" +- "Click on the first link in the navigation" + +### JavaScript Execution + +- "Run `document.title` to get the page title" +- "Execute `document.querySelectorAll('.error').length` to count errors" +- "Check if there are any console errors with JavaScript" +- "Run `localStorage.clear()` to clear storage" +- "Execute `window.scrollTo(0, document.body.scrollHeight)` to scroll to bottom" + +### Debugging Workflows + +- "I'm getting a JavaScript error on my site. Can you find it?" +- "My form isn't submitting properly. Can you test it?" +- "The page loads slowly. Can you analyze the performance?" +- "Some API calls are failing. Monitor the network for me" +- "I need to test my responsive design. Take screenshots at different sizes" + +### Development Testing + +- "Test my local development server at localhost:3000" +- "Check if my staging environment is working" +- "Monitor my app for errors while I use it" +- "Take before and after screenshots of my changes" +- "Verify that my new feature is working correctly" + +## Example Conversations + +### Debugging Console Errors + +``` +User: "My React app is showing errors at http://localhost:3000. Can you check the console?" +Claude: [Analyzes console output] "I found 3 JavaScript errors related to missing props..." +``` + +### Form Testing + +``` +User: "Test my login form here https://www.saucedemo.com/, use standard_user and secret_sauce" +Claude: "I'll navigate to your login page. What's the URL and what credentials should I use?" +User: "" +Claude: [Navigates, fills form, submits] "The form submitted successfully and redirected to dashboard" +``` + +### Performance Issues + +``` +User: "run performance test this URL https://myapp.com" +Claude: [Analyzes performance] "Your LCP is 4.2 seconds which is slow. The main bottleneck is..." +``` + +## Tips for Best Results + +- **Be specific about URLs** - Provide the full URL including http:// or https:// +- **Describe what you're testing** - Explain what you expect to happen +- **Mention local vs production** - Specify if you're testing localhost or a live site +- **Provide credentials when needed** - For form testing, give test usernames/passwords +- **Describe the problem** - Explain what issue you're trying to diagnose +- **Ask for specific metrics** - Request particular performance data or error types + +## Common Scenarios + +### "My site is broken" workflow: + +1. "Check if https://mysite.com loads" +2. "Look for JavaScript errors in the console" +3. "Monitor network requests for failures" +4. "Take a screenshot to see what's visible" + +### "Test my new feature" workflow: + +1. "Navigate to http://localhost:3000/new-feature" +2. "Click the [button/element] to activate it" +3. "Watch the console for any errors" +4. "Verify the expected result appears" + +### "Performance investigation" workflow: + +1. "Analyze performance metrics for https://mysite.com" +2. "Show me the largest resources loading" +3. "Check Core Web Vitals" +4. "Identify the main bottlenecks" + +Just describe what you want to do in plain language, and I'll handle the browser automation for you! + diff --git a/skills/SKILL.md b/skills/SKILL.md new file mode 100644 index 0000000..28e1c69 --- /dev/null +++ b/skills/SKILL.md @@ -0,0 +1,193 @@ +--- +name: browser-devtools +description: Browser debugging and automation using Playwright. Use for web debugging, console monitoring, network analysis, screenshot capture, DOM inspection, and UI testing during development. +license: Apache-2.0 +--- + +# Browser Devtools + +Browser automation and debugging using Playwright for web development testing and analysis. + +## Decision tree + +Here the decison tree when user want to inspect the console log, apply the same pattern for other prompt. + +``` +User task → Is URL provided? + ├─ Yes → Detect if it reachable. + │ ├─ Success → Implement Reconnaissance-then-action (RTA) + | | 1. Navigate to provided URL. + | | 2. Inspect console log + | | 3. Identify the errors + | | 4. Execute actions with discovered issues + │ └─ Fails → End + │ + └─ No → Is the development server already running? + ├─ No → End + │ + └─ Yes → Ask user if they want to test development server? + ├─ Yes → Reconnaissance-then-action (RTA) + ├─ No → End +``` + +## Quick Start + +Install dependencies: + +```bash +cd plugins/browser-devtools/skills +./scripts/install.sh +``` + +Test installation: + +```bash +node dist/scripts/navigate.js --url https://example.com +``` + +## Scripts + +All scripts located in `scripts/` (compiled to `dist/scripts/`): + +**Core Commands:** + +- `navigate.js` - Navigate to URLs +- `screenshot.js` - Capture screenshots (full page or elements) +- `snapshot.js` - DOM inspection and element discovery +- `evaluate.js` - Execute JavaScript in browser context +- `click.js` - Element interaction +- `fill.js` - Form input testing + +**Monitoring:** + +- `console.js` - Console log monitoring and error tracking +- `network.js` - Network request analysis and performance debugging +- `performance.js` - Core Web Vitals and performance metrics + +## Best Practices + +- **Compliance Decision tree**: The decision tree must be followed strictly. +- **Wait for elements**: Use appropriate wait strategies for dynamic content +- **Clean up sessions**: Close sessions when done to free resources + +## Usage + +### Single Commands + +```bash +# Screenshot +node dist/scripts/screenshot.js --url https://example.com --output page.png + +# Performance analysis +node dist/scripts/performance.js --url https://example.com | jq '.vitals.LCP' + +# Console monitoring (10 seconds) +node dist/scripts/console.js --url https://example.com --types error,warn --duration 10000 +``` + +### Chained Commands (reuse session) + +```bash +# Start session (keep browser open) +node dist/scripts/navigate.js --url https://example.com/login --close false + +# Continue with same browser +node dist/scripts/fill.js --selector "#email" --value "user@example.com" --close false +node dist/scripts/click.js --selector "button[type=submit]" +``` + +### Common Debugging Patterns + +```bash +# Find elements +node dist/scripts/snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}' + +# Check for errors +node dist/scripts/console.js --url https://example.com --types error + +# Monitor API calls +node dist/scripts/network.js --url https://example.com --types xhr,fetch --duration 5000 + +# Test forms +node dist/scripts/fill.js --url https://example.com --selector "#search" --value "query" --close false +node dist/scripts/click.js --selector "button[type=submit]" +``` + +## Options + +All scripts support: + +- `--headless false` - Show browser window (default: true) +- `--close false` - Keep browser open for chaining (default: true) +- `--timeout 30000` - Timeout in milliseconds +- `--browser chromium|firefox|webkit` - Browser engine (default: chromium) + +## Output Format + +**Success:** + +```json +{ + "success": true, + "url": "https://example.com", + "title": "Example Domain", + "sessionId": "session_1234567890_abc123", + "timestamp": "2024-01-01T12:00:00.000Z", + "...": "script-specific data" +} +``` + +**Error:** + +```json +{ + "success": false, + "error": "Element not found: .missing-element", + "stack": "Error: Element not found..." +} +``` + +## Selectors + +```bash +# CSS selectors +node dist/scripts/click.js --selector "#main .button.primary" + +# XPath +node dist/scripts/click.js --selector "xpath=//button[contains(text(), 'Submit')]" + +# Text selectors +node dist/scripts/click.js --selector "text=Click me" +``` + +## Troubleshooting + +**Browser not installed**: Run `npm run install-browsers` +**Element not found**: Use `snapshot.js` first to find correct selectors +**Script timeout**: Increase with `--timeout 60000` +**Permission denied**: Run `chmod +x scripts/install.sh` + +**Debug mode**: Use `--headless false` to see browser actions +**Stale sessions**: Start new session with `--close true` + +## Integration + +**Pre-commit testing:** + +```bash +node dist/scripts/navigate.js --url http://localhost:3000 && \ +node dist/scripts/performance.js --url http://localhost:3000 | jq '.vitals.LCP' +``` + +**Performance regression:** + +```bash +node dist/scripts/performance.js --url $APP_URL | jq '.vitals.LCP' | \ + awk '{print $1 < 2500 ? "PASS" : "FAIL"}' +``` + +## References + +- [Playwright Documentation](https://playwright.dev/) +- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) +- [Core Web Vitals](https://web.dev/vitals/) diff --git a/skills/package.json b/skills/package.json new file mode 100644 index 0000000..b074e0f --- /dev/null +++ b/skills/package.json @@ -0,0 +1,37 @@ +{ + "name": "browser-devtools", + "version": "1.0.0", + "description": "Browser debugging and automation skill using Playwright for developers", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "install-browsers": "playwright install", + "test": "node dist/scripts/test.js" + }, + "keywords": [ + "browser", + "debugging", + "automation", + "playwright", + "development" + ], + "author": "Claude Skills", + "license": "Apache-2.0", + "dependencies": { + "@playwright/test": "^1.40.0", + "playwright": "^1.40.0", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/node": "^20.8.0", + "@types/yargs": "^17.0.32", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=18.0.0" + } +} + diff --git a/skills/scripts/click.ts b/skills/scripts/click.ts new file mode 100644 index 0000000..ac7f16d --- /dev/null +++ b/skills/scripts/click.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Element interaction for testing UI behavior + * Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"] + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, waitForElement, getElementInfo } from '../lib/browser.js'; + +interface ClickArgs { + selector?: string; + url?: string; + 'wait-for'?: string; + 'wait-timeout'?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +async function click() { + const args = parseArgs(process.argv.slice(2)) as ClickArgs; + + if (!args.selector) { + return outputError(new Error('--selector is required')); + return; + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + }); + + // Navigate to URL if provided + if (args.url) { + await session.page.goto(args.url, { waitUntil: 'networkidle' }); + } + + // Wait for element to be available + const waitTimeout = args['wait-timeout'] ? parseInt(args['wait-timeout']) : 30000; + await waitForElement(session.page, args.selector, waitTimeout); + + // Get element info before click + const elementInfo = await getElementInfo(session.page, args.selector); + + // Check if element is visible and clickable + if (!elementInfo.visible) { + return outputError(new Error(`Element is not visible: ${args.selector}`)); + return; + } + + // Click the element + await session.page.click(args.selector); + + // Wait for navigation or new content if specified + if (args['wait-for']) { + try { + await waitForElement(session.page, args['wait-for'], waitTimeout); + } catch (error) { + // Don't fail if wait-for element doesn't appear, just log it + console.warn(`Warning: Wait-for element not found: ${args['wait-for']}`); + } + } + + const result = { + success: true, + url: session.page.url(), + title: await session.page.title(), + action: { + type: 'click', + selector: args.selector, + elementInfo: elementInfo, + waitFor: args['wait-for'] || null, + }, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(result); + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +click(); diff --git a/skills/scripts/console.ts b/skills/scripts/console.ts new file mode 100644 index 0000000..91db7ee --- /dev/null +++ b/skills/scripts/console.ts @@ -0,0 +1,146 @@ +#!/usr/bin/env node +/** + * Console log monitoring and error tracking + * Usage: node console.js [--url URL] [--types error,warn] [--duration 5000] [--no-navigation] + * + * Options: + * --url URL to navigate to (required if no existing browser session) + * --types Console message types to capture (comma-separated, default: all) + * --duration How long to monitor console (ms, default: 5000) + * --no-navigation Don't navigate, use existing browser session + * + * Examples: + * node console.js --url https://your-app.com --types error,warn + * node console.js --no-navigation true # Uses existing session + * node navigate.js --url https://your-app.com --close false + * node console.js --no-navigation true --types error # Monitor existing page + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, getExistingSession } from '../lib/browser.js'; + +interface ConsoleArgs { + url?: string; + types?: string; + duration?: string; + timeout?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; + 'no-navigation'?: string | boolean; +} + +interface ConsoleMessage { + type: string; + text: string; + location?: { + url: string; + lineNumber: number; + columnNumber: number; + }; + timestamp: number; +} + +async function monitorConsole() { + const args = parseArgs(process.argv.slice(2)) as ConsoleArgs; + + // Smart URL handling + let shouldNavigate = args['no-navigation'] !== 'true'; + let targetUrl = args.url; + + // If no URL provided, try to use existing session + if (!targetUrl) { + const existingSession = await getExistingSession(); + if (existingSession && existingSession.browser.isConnected()) { + // Use existing session, don't navigate + shouldNavigate = false; + targetUrl = existingSession.page.url(); + } else { + // No existing session and no URL provided - require explicit URL from user + return outputError(new Error('No browser session found. Please provide a URL with --url or navigate first using navigate.js. For example: --url https://your-site.com')); + } + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + timeout: args.timeout ? parseInt(args.timeout) : undefined, + }); + + const messages: ConsoleMessage[] = []; + const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null; + const duration = args.duration ? parseInt(args.duration) : 5000; + + // Set up console message listener + session.page.on('console', (msg) => { + const messageType = msg.type().toLowerCase(); + + // Filter by message types if specified + if (filterTypes && !filterTypes.includes(messageType)) { + return; + } + + const consoleMessage: ConsoleMessage = { + type: messageType, + text: msg.text(), + timestamp: Date.now(), + }; + + // Add location info if available + const location = msg.location(); + if (location) { + consoleMessage.location = { + url: location.url, + lineNumber: location.lineNumber, + columnNumber: location.columnNumber, + }; + } + + messages.push(consoleMessage); + }); + + // Navigate to the URL if needed + if (shouldNavigate && targetUrl) { + await session.page.goto(targetUrl, { waitUntil: 'networkidle' }); + } + + // Wait for the specified duration + await new Promise(resolve => setTimeout(resolve, duration)); + + // Count messages by type + const messageCount = messages.reduce((acc, msg) => { + acc[msg.type] = (acc[msg.type] || 0) + 1; + return acc; + }, {} as Record); + + const result = { + success: true, + url: session.page.url(), + title: await session.page.title(), + monitoring: { + duration: duration, + types: filterTypes || 'all', + messageCount: messages.length, + messageCountByType: messageCount, + usedExistingSession: !args.url && !shouldNavigate, + autoNavigated: shouldNavigate && args.url, + }, + messages: messages, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(result); + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +monitorConsole(); diff --git a/skills/scripts/evaluate.ts b/skills/scripts/evaluate.ts new file mode 100644 index 0000000..7c688bc --- /dev/null +++ b/skills/scripts/evaluate.ts @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/** + * Execute JavaScript for debugging and analysis + * Usage: node evaluate.js --url https://example.com --script "document.title" + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, executeScript } from '../lib/browser.js'; + +interface EvaluateArgs { + url?: string; + script?: string; + timeout?: string; + 'wait-until'?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +async function evaluate() { + const args = parseArgs(process.argv.slice(2)) as EvaluateArgs; + + if (!args.script) { + return outputError(new Error('--script is required')); + return; + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + timeout: args.timeout ? parseInt(args.timeout) : undefined, + }); + + // Navigate to URL if provided + if (args.url) { + await session.page.goto(args.url, { + waitUntil: args['wait-until'] as any || 'load', + timeout: args.timeout ? parseInt(args.timeout) : 30000, + }); + } + + // Execute the script + const result = await executeScript(session.page, args.script); + + const responseData = { + success: true, + url: session.page.url(), + title: await session.page.title(), + script: args.script, + result: result, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(responseData); + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +evaluate(); diff --git a/skills/scripts/fill.ts b/skills/scripts/fill.ts new file mode 100644 index 0000000..22b3259 --- /dev/null +++ b/skills/scripts/fill.ts @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * Form testing and input validation + * Usage: node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true] + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, waitForElement, getElementInfo } from '../lib/browser.js'; + +interface FillArgs { + selector?: string; + value?: string; + url?: string; + clear?: string | boolean; + 'wait-timeout'?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +async function fill() { + const args = parseArgs(process.argv.slice(2)) as FillArgs; + + if (!args.selector) { + return outputError(new Error('--selector is required')); + return; + } + + if (args.value === undefined) { + return outputError(new Error('--value is required')); + return; + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + }); + + // Navigate to URL if provided + if (args.url) { + await session.page.goto(args.url, { waitUntil: 'networkidle' }); + } + + // Wait for element to be available + const waitTimeout = args['wait-timeout'] ? parseInt(args['wait-timeout']) : 30000; + await waitForElement(session.page, args.selector, waitTimeout); + + // Get element info before filling + const elementInfo = await getElementInfo(session.page, args.selector); + + // Check if element is visible and fillable + if (!elementInfo.visible) { + return outputError(new Error(`Element is not visible: ${args.selector}`)); + return; + } + + // Check if element is an input-like element + const fillableTags = ['input', 'textarea', 'select']; + if (!fillableTags.includes(elementInfo.tagName)) { + return outputError(new Error(`Element is not fillable: ${elementInfo.tagName} (${args.selector})`)); + return; + } + + // Clear the field if requested + if (args.clear === 'true') { + await session.page.fill(args.selector, ''); + } + + // Fill the element with the value + await session.page.fill(args.selector, args.value); + + // Verify the value was set + const actualValue = await session.page.inputValue(args.selector); + + const result = { + success: true, + url: session.page.url(), + title: await session.page.title(), + action: { + type: 'fill', + selector: args.selector, + elementInfo: elementInfo, + value: args.value, + actualValue: actualValue, + cleared: args.clear === 'true', + }, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(result); + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +fill(); diff --git a/skills/scripts/install.sh b/skills/scripts/install.sh new file mode 100755 index 0000000..7601b2d --- /dev/null +++ b/skills/scripts/install.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# Installation script for browser-devtools skill + +set -e + +echo "🚀 Installing browser-devtools skill..." + +# Check if Node.js is installed +if ! command -v node &>/dev/null; then + echo "❌ Node.js is not installed. Please install Node.js 18+ first." + exit 1 +fi + +# Check Node.js version +NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt 18 ]; then + echo "❌ Node.js 18+ is required. Current version: $(node -v)" + exit 1 +fi + +echo "✅ Node.js $(node -v) detected" + +# Check if npm dependencies are already installed +if [ ! -d "node_modules" ] || ! npm list playwright &>/dev/null; then + echo "📦 Installing npm dependencies..." + npm install +else + echo "✅ npm dependencies already installed" +fi + +# Check if build is needed +BUILD_NEEDED=false +if [ ! -d "dist" ]; then + BUILD_NEEDED=true +else + # Check if any source files are newer than dist files + for ts_file in scripts/*.ts lib/*.ts; do + if [ -f "$ts_file" ]; then + js_file="dist/${ts_file%.*}.js" + if [ ! -f "$js_file" ] || [ "$ts_file" -nt "$js_file" ]; then + BUILD_NEEDED=true + break + fi + fi + done +fi + +if [ "$BUILD_NEEDED" = true ]; then + echo "📦 Building TypeScript files..." + npm run build +else + echo "✅ Build files are up to date" +fi + +# Check if Playwright browsers are installed +BROWSER_CHECK=$(node -e "try { require('playwright').chromium.executablePath(); console.log('OK'); } catch(e) { console.log('MISSING'); }" 2>/dev/null || echo "MISSING") +if [ "$BROWSER_CHECK" != "OK" ]; then + echo "🌐 Installing Playwright browsers..." + npx playwright install +else + echo "✅ Playwright browsers already installed" +fi + +# Install system dependencies on Linux +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "🔧 Installing system dependencies for Linux..." + + # Detect distribution + if [ -f /etc/debian_version ]; then + # Debian/Ubuntu + echo "Detected Debian/Ubuntu-based system" + sudo apt-get update + sudo apt-get install -y \ + libnss3 \ + libnspr4 \ + libasound2t64 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 + elif [ -f /etc/redhat-release ]; then + # RHEL/Fedora/CentOS + echo "Detected RedHat/Fedora-based system" + if command -v dnf &>/dev/null; then + sudo dnf install -y \ + nss \ + nspr \ + alsa-lib \ + atk \ + at-spi2-atk \ + cups-libs \ + libdrm \ + libxkbcommon \ + libXcomposite \ + libXdamage \ + libXfixes \ + libXrandr \ + mesa-libgbm + else + sudo yum install -y \ + nss \ + nspr \ + alsa-lib \ + atk \ + at-spi2-atk \ + cups-libs \ + libdrm \ + libxkbcommon \ + libXcomposite \ + libXdamage \ + libXfixes \ + libXrandr \ + mesa-libgbm + fi + elif [ -f /etc/arch-release ]; then + # Arch Linux + echo "Detected Arch-based system" + sudo pacman -S --needed \ + nss \ + nspr \ + alsa-lib \ + atk \ + at-spi2-atk \ + cups \ + libdrm \ + libxkbcommon \ + libxcomposite \ + libxdamage \ + libxfixes \ + libxrandr \ + mesa + else + echo "⚠️ Unknown Linux distribution. Please install browser dependencies manually." + echo "See: https://playwright.dev/docs/linux/" + fi +fi + +# Create necessary directories +mkdir -p docs/screenshots +mkdir -p docs/traces +mkdir -p docs/reports + +echo "✅ Installation completed successfully!" +echo "" +echo "🧪 Test the installation:" +echo " node dist/scripts/navigate.js --url https://example.com" +echo "" +echo "📚 For usage examples, see SKILL.md" + diff --git a/skills/scripts/navigate.ts b/skills/scripts/navigate.ts new file mode 100644 index 0000000..44ced99 --- /dev/null +++ b/skills/scripts/navigate.ts @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * Navigate to a URL for debugging + * Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000] + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js'; + +interface NavigateArgs { + url?: string; + 'wait-until'?: string; + timeout?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +async function navigate() { + const args = parseArgs(process.argv.slice(2)) as NavigateArgs; + + if (!args.url) { + return outputError(new Error('--url is required')); + return; + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + timeout: args.timeout ? parseInt(args.timeout) : undefined, + }); + + const waitUntil = args['wait-until'] as any || 'networkidle'; + + await session.page.goto(args.url, { + waitUntil, + timeout: args.timeout ? parseInt(args.timeout) : 30000, + }); + + const title = await session.page.title(); + const finalUrl = session.page.url(); + + const result = { + success: true, + url: finalUrl, + title: title, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(result); + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +navigate(); diff --git a/skills/scripts/network.ts b/skills/scripts/network.ts new file mode 100644 index 0000000..e55e73b --- /dev/null +++ b/skills/scripts/network.ts @@ -0,0 +1,254 @@ +#!/usr/bin/env node +/** + * Network request debugging and performance analysis + * Usage: node network.js --url https://example.com [--types xhr,fetch] [--duration 5000] [--output requests.json] + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js'; +import { writeFile } from 'fs/promises'; + +interface NetworkArgs { + url?: string; + types?: string; + duration?: string; + output?: string; + timeout?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +interface NetworkRequest { + url: string; + method: string; + status: number; + statusText: string; + type: string; + resourceType: string; + size: { + request: number; + response: number; + total: number; + }; + timing: { + startTime: number; + endTime: number; + duration: number; + }; + headers: { + request: Record; + response: Record; + }; + failed: boolean; + errorText?: string; +} + +async function monitorNetwork() { + const args = parseArgs(process.argv.slice(2)) as NetworkArgs; + + if (!args.url) { + return outputError(new Error('--url is required')); + return; + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + timeout: args.timeout ? parseInt(args.timeout) : undefined, + }); + + const requests: NetworkRequest[] = []; + const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null; + const duration = args.duration ? parseInt(args.duration) : 5000; + + // Set up request and response listeners + session.page.on('request', (request) => { + const resourceType = request.resourceType().toLowerCase(); + + // Filter by resource types if specified + if (filterTypes && !filterTypes.includes(resourceType)) { + return; + } + + const requestData: any = { + url: request.url(), + method: request.method(), + resourceType: resourceType, + type: 'request', + headers: request.headers(), + size: { + request: 0, // Playwright doesn't expose header size directly + response: 0, + total: 0, + }, + timing: { + startTime: Date.now(), + endTime: 0, + duration: 0, + }, + status: 0, + statusText: '', + failed: false, + }; + + // Store temporary request data + (request as any)._requestData = requestData; + }); + + session.page.on('response', async (response) => { + const request = response.request() as any; + const requestData = request._requestData; + + if (!requestData) { + return; // Skip if not tracked + } + + const resourceType = response.request().resourceType().toLowerCase(); + + // Filter by resource types if specified + if (filterTypes && !filterTypes.includes(resourceType)) { + return; + } + + const endTime = Date.now(); + const responseBody = await response.text(); + const responseSize = responseBody.length; + + const networkRequest: NetworkRequest = { + url: response.url(), + method: request.method(), + status: response.status(), + statusText: response.statusText(), + type: 'response', + resourceType: resourceType, + size: { + request: requestData.size.request, + response: responseSize, + total: requestData.size.request + responseSize, + }, + timing: { + startTime: requestData.timing.startTime, + endTime: endTime, + duration: endTime - requestData.timing.startTime, + }, + headers: { + request: requestData.headers, + response: response.headers(), + }, + failed: !response.ok(), + errorText: !response.ok() ? response.statusText() : undefined, + }; + + requests.push(networkRequest); + }); + + session.page.on('requestfailed', (request) => { + const requestData = (request as any)._requestData; + + if (!requestData) { + return; // Skip if not tracked + } + + const resourceType = request.resourceType().toLowerCase(); + + // Filter by resource types if specified + if (filterTypes && !filterTypes.includes(resourceType)) { + return; + } + + const endTime = Date.now(); + + const networkRequest: NetworkRequest = { + url: request.url(), + method: request.method(), + status: 0, + statusText: 'Failed', + type: 'failed', + resourceType: resourceType, + size: requestData.size, + timing: { + startTime: requestData.timing.startTime, + endTime: endTime, + duration: endTime - requestData.timing.startTime, + }, + headers: { + request: requestData.headers, + response: {}, + }, + failed: true, + errorText: (request as any).failure()?.errorText || 'Request failed', + }; + + requests.push(networkRequest); + }); + + // Navigate to the URL + await session.page.goto(args.url, { waitUntil: 'networkidle' }); + + // Wait for the specified duration + await new Promise(resolve => setTimeout(resolve, duration)); + + // Calculate summary statistics + const summary = { + totalRequests: requests.length, + successfulRequests: requests.filter(r => !r.failed).length, + failedRequests: requests.filter(r => r.failed).length, + totalSize: requests.reduce((sum, r) => sum + r.size.total, 0), + averageResponseTime: requests.length > 0 + ? requests.reduce((sum, r) => sum + r.timing.duration, 0) / requests.length + : 0, + requestsByType: requests.reduce((acc, r) => { + acc[r.resourceType] = (acc[r.resourceType] || 0) + 1; + return acc; + }, {} as Record), + requestsByStatus: requests.reduce((acc, r) => { + if (r.failed) { + acc['failed'] = (acc['failed'] || 0) + 1; + } else { + const statusGroup = Math.floor(r.status / 100) * 100; + acc[statusGroup] = (acc[statusGroup] || 0) + 1; + } + return acc; + }, {} as Record), + }; + + const result = { + success: true, + url: session.page.url(), + title: await session.page.title(), + monitoring: { + duration: duration, + types: filterTypes || 'all', + ...summary, + }, + requests: requests, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + // Save to file if output path provided + if (args.output) { + await writeFile(args.output, JSON.stringify(result, null, 2)); + outputJSON({ + success: true, + output: args.output, + ...summary, + url: session.page.url() + }); + } else { + outputJSON(result); + } + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +monitorNetwork(); diff --git a/skills/scripts/performance.ts b/skills/scripts/performance.ts new file mode 100644 index 0000000..ae574b8 --- /dev/null +++ b/skills/scripts/performance.ts @@ -0,0 +1,231 @@ +#!/usr/bin/env node +/** + * Performance metrics and Core Web Vitals + * Usage: node performance.js --url https://example.com [--trace trace.json] [--resources true] + */ + +import { + getBrowserSession, + closeBrowserSession, + parseArgs, + outputJSON, + outputError, +} from "../lib/browser.js"; +import { writeFile } from "fs/promises"; + +interface PerformanceArgs { + url?: string; + trace?: string; + resources?: string | boolean; + timeout?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +interface CoreWebVitals { + LCP: number | null; // Largest Contentful Paint + FID: number | null; // First Input Delay + CLS: number; // Cumulative Layout Shift + FCP: number | null; // First Contentful Paint + TTFB: number | null; // Time to First Byte +} + +interface PerformanceResource { + name: string; + type: string; + duration: number; + size: number; + startTime: number; + responseEnd: number; +} + +async function measurePerformance() { + const args = parseArgs(process.argv.slice(2)) as PerformanceArgs; + + if (!args.url) { + return outputError(new Error("--url is required")); + } + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== "false", + timeout: args.timeout ? parseInt(args.timeout) : undefined, + }); + + // Start tracing if requested + if (args.trace) { + await session.page.context().tracing.start({ + screenshots: true, + snapshots: true, + sources: true, + }); + } + + // Navigate to the URL + await session.page.goto(args.url, { waitUntil: "networkidle" }); + + // Stop tracing and save if requested + if (args.trace) { + await session.page.context().tracing.stop({ path: args.trace }); + } + + // Get performance metrics from Chrome DevTools Protocol + const client = await session.page.context().newCDPSession(session.page); + const metrics = await client.send("Performance.getMetrics"); + + // Get Core Web Vitals + const vitals = await session.page.evaluate(() => { + return new Promise((resolve) => { + const vitals: CoreWebVitals = { + LCP: null, + FID: null, + CLS: 0, + FCP: null, + TTFB: null, + }; + + // LCP - Largest Contentful Paint + try { + new PerformanceObserver((list) => { + const entries = list.getEntries(); + if (entries.length > 0) { + const lastEntry = entries[entries.length - 1] as any; + vitals.LCP = lastEntry.renderTime || lastEntry.loadTime; + } + }).observe({ + entryTypes: ["largest-contentful-paint"], + buffered: true, + }); + } catch (e) { + // LCP not supported + } + + // CLS - Cumulative Layout Shift + try { + new PerformanceObserver((list) => { + list.getEntries().forEach((entry: any) => { + if (!entry.hadRecentInput) { + vitals.CLS += entry.value; + } + }); + }).observe({ entryTypes: ["layout-shift"], buffered: true }); + } catch (e) { + // CLS not supported + } + + // FCP - First Contentful Paint + try { + const paintEntries = performance.getEntriesByType("paint"); + const fcpEntry = paintEntries.find( + (e: any) => e.name === "first-contentful-paint", + ); + if (fcpEntry) { + vitals.FCP = fcpEntry.startTime; + } + } catch (e) { + // FCP not supported + } + + // TTFB - Time to First Byte + try { + const [navigationEntry] = performance.getEntriesByType( + "navigation", + ) as any[]; + if (navigationEntry) { + vitals.TTFB = + navigationEntry.responseStart - navigationEntry.requestStart; + } + } catch (e) { + // Navigation timing not supported + } + + // Wait a bit for metrics to stabilize + setTimeout(() => resolve(vitals), 1000); + }); + }); + + // Get resource timing information + let resources: PerformanceResource[] = []; + if (args.resources === "true") { + resources = await session.page.evaluate(() => { + return performance.getEntriesByType("resource").map((r: any) => ({ + name: r.name, + type: r.initiatorType, + duration: r.duration, + size: r.transferSize || 0, + startTime: r.startTime, + responseEnd: r.responseEnd, + })); + }); + } + + const resourceSummary = { + count: resources.length, + totalDuration: resources.reduce((sum, r) => sum + r.duration, 0), + totalSize: resources.reduce((sum, r) => sum + r.size, 0), + averageDuration: + resources.length > 0 + ? resources.reduce((sum, r) => sum + r.duration, 0) / resources.length + : 0, + resourcesByType: resources.reduce( + (acc, r) => { + acc[r.type] = (acc[r.type] || 0) + 1; + return acc; + }, + {} as Record, + ), + }; + + // Process metrics into a more usable format + const processedMetrics: Record = {}; + metrics.metrics?.forEach((metric: any) => { + processedMetrics[metric.name] = metric.value; + }); + + const result = { + success: true, + url: session.page.url(), + title: await session.page.title(), + metrics: { + ...processedMetrics, + JSHeapUsedSizeMB: ( + (processedMetrics.JSHeapUsedSize || 0) / + 1024 / + 1024 + ).toFixed(2), + JSHeapTotalSizeMB: ( + (processedMetrics.JSHeapTotalSize || 0) / + 1024 / + 1024 + ).toFixed(2), + }, + vitals: vitals, + resources: + args.resources === "true" + ? { + ...resourceSummary, + items: resources, + } + : resourceSummary, + trace: args.trace || null, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(result); + + if (args.close !== "false") { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +measurePerformance(); + diff --git a/skills/scripts/screenshot.ts b/skills/scripts/screenshot.ts new file mode 100644 index 0000000..d0011a8 --- /dev/null +++ b/skills/scripts/screenshot.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Capture screenshots for visual debugging + * Usage: node screenshot.js --url https://example.com --output screenshot.png [--full-page true] [--selector .element] + */ + +import { + getBrowserSession, + closeBrowserSession, + parseArgs, + outputJSON, + outputError, + createScreenshot, + getOutputDirectory, +} from "../lib/browser.js"; +import { writeFile } from "fs/promises"; +import { join } from "path"; + +interface ScreenshotArgs { + url?: string; + output?: string; + "full-page"?: string | boolean; + selector?: string; + format?: string; + quality?: string; + "wait-until"?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +async function takeScreenshot() { + const args = parseArgs(process.argv.slice(2)) as ScreenshotArgs; + + if (!args.url && !args.selector) { + return outputError(new Error("Either --url or --selector is required")); + } + + // --output is optional - will auto-generate if not provided + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== "false", + }); + + // Navigate to URL if provided + if (args.url) { + await session.page.goto(args.url, { + waitUntil: (args["wait-until"] as any) || "load", + }); + } + + const outputPath = + args.output || join(getOutputDirectory(), `screenshot_${Date.now()}.png`); + const fullPage = args["full-page"] === "true"; + + // Create screenshot + const result = await createScreenshot(session.page, outputPath, { + fullPage, + selector: args.selector, + }); + + const screenshotData = { + success: true, + url: session.page.url(), + title: await session.page.title(), + outputPath: typeof result === "string" ? result : outputPath, + fullPage, + selector: args.selector || null, + size: typeof result === "string" ? null : result.length, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + outputJSON(screenshotData); + + if (args.close !== "false") { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError( + error instanceof Error ? error : new Error(String(error)), + ); + } +} + +takeScreenshot(); diff --git a/skills/scripts/snapshot.ts b/skills/scripts/snapshot.ts new file mode 100644 index 0000000..eec3894 --- /dev/null +++ b/skills/scripts/snapshot.ts @@ -0,0 +1,172 @@ +#!/usr/bin/env node +/** + * DOM inspection and element discovery + * Usage: node snapshot.js --url https://example.com [--output snapshot.json] + */ + +import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, getOutputDirectory } from '../lib/browser.js'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; + +interface SnapshotArgs { + url?: string; + output?: string; + timeout?: string; + headless?: string | boolean; + close?: string | boolean; + browser?: string; +} + +interface ElementInfo { + index: number; + tagName: string; + id: string | null; + className: string | null; + name: string | null; + value: string | null; + type: string | null; + text: string | null; + href: string | null; + selector: string; + xpath: string; + visible: boolean; + position: { + x: number; + y: number; + width: number; + height: number; + }; +} + +async function takeSnapshot() { + const args = parseArgs(process.argv.slice(2)) as SnapshotArgs; + + try { + const session = await getBrowserSession({ + browser: args.browser as any, + headless: args.headless !== 'false', + timeout: args.timeout ? parseInt(args.timeout) : undefined, + }); + + // Navigate if URL provided + if (args.url) { + await session.page.goto(args.url, { + waitUntil: 'networkidle', + timeout: args.timeout ? parseInt(args.timeout) : 30000, + }); + } + + // Get interactive elements with metadata + const elements = await session.page.evaluate(() => { + const interactiveSelectors = [ + 'a[href]', + 'button', + 'input', + 'textarea', + 'select', + '[onclick]', + '[role="button"]', + '[role="link"]', + '[contenteditable]', + '[tabindex]' + ]; + + const elements: ElementInfo[] = []; + const selector = interactiveSelectors.join(', '); + const nodes = document.querySelectorAll(selector); + + nodes.forEach((el, index) => { + const rect = el.getBoundingClientRect(); + const element = el as HTMLElement; + + // Generate unique selector + let uniqueSelector = ''; + if (element.id) { + uniqueSelector = `#${element.id}`; + } else if (element.className) { + const classes = Array.from(element.classList).join('.'); + uniqueSelector = `${element.tagName.toLowerCase()}.${classes}`; + } else { + uniqueSelector = element.tagName.toLowerCase(); + } + + elements.push({ + index: index, + tagName: element.tagName.toLowerCase(), + id: element.id || null, + className: element.className || null, + name: (element as any).name || null, + value: (element as any).value || null, + type: (element as any).type || null, + text: element.textContent?.trim().substring(0, 100) || null, + href: (element as HTMLAnchorElement).href || null, + selector: uniqueSelector, + xpath: getXPath(element), + visible: rect.width > 0 && rect.height > 0, + position: { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + } + }); + }); + + function getXPath(element: Element): string { + if (element.id) { + return `//*[@id="${element.id}"]`; + } + if (element === document.body) { + return '/html/body'; + } + let ix = 0; + const parent = element.parentNode as Element; + const siblings = parent?.children || []; + for (let i = 0; i < siblings.length; i++) { + const sibling = siblings[i]; + if (sibling === element) { + return getXPath(parent) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']'; + } + if (sibling.tagName === element.tagName) { + ix++; + } + } + return ''; + } + + return elements; + }); + + const result = { + success: true, + url: session.page.url(), + title: await session.page.title(), + elementCount: elements.length, + elements: elements, + sessionId: session.sessionId, + timestamp: new Date().toISOString(), + }; + + // Save to file if output path provided, otherwise save to project directory + const outputPath = args.output || join(getOutputDirectory(), `snapshot_${Date.now()}.json`); + await writeFile(outputPath, JSON.stringify(result, null, 2)); + + outputJSON({ + success: true, + output: outputPath, + elementCount: elements.length, + url: session.page.url() + }); + + if (args.close !== 'false') { + await closeBrowserSession(); + } else { + // Explicitly exit the process when keeping session open + process.exit(0); + } + } catch (error) { + return outputError(error instanceof Error ? error : new Error(String(error))); + } +} + +takeSnapshot(); diff --git a/skills/tsconfig.json b/skills/tsconfig.json new file mode 100644 index 0000000..0170f68 --- /dev/null +++ b/skills/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "allowJs": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "resolveJsonModule": true, + "lib": ["ES2018", "DOM", "DOM.Iterable"], + "types": ["node"] + }, + "include": [ + "scripts/**/*", + "lib/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +}