Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:52 +08:00
commit 6ec3196ecc
434 changed files with 125248 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
# 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": "..."
}
```

View File

@@ -0,0 +1,210 @@
/**
* 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');
});
});
});

View File

@@ -0,0 +1,79 @@
#!/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();

View File

@@ -0,0 +1,75 @@
#!/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();

View File

@@ -0,0 +1,49 @@
#!/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();

View File

@@ -0,0 +1,72 @@
#!/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();

View File

@@ -0,0 +1,181 @@
#!/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 ""

View File

@@ -0,0 +1,83 @@
#!/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 ""

View File

@@ -0,0 +1,46 @@
#!/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();

View File

@@ -0,0 +1,102 @@
#!/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();

View File

@@ -0,0 +1,15 @@
{
"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"
}
}

View File

@@ -0,0 +1,145 @@
#!/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();

View File

@@ -0,0 +1,180 @@
#!/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();

View File

@@ -0,0 +1,131 @@
#!/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();