mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-04 02:20:42 +08:00
add browser skill
This commit is contained in:
73
skills/browser/SKILL.md
Normal file
73
skills/browser/SKILL.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: browser
|
||||
description: This skill should be used for browser automation tasks using Chrome DevTools Protocol (CDP). Triggers when users need to launch Chrome with remote debugging, navigate pages, execute JavaScript in browser context, capture screenshots, or interactively select DOM elements. No MCP server required.
|
||||
---
|
||||
|
||||
# Browser Automation
|
||||
|
||||
Minimal Chrome DevTools Protocol (CDP) helpers for browser automation without MCP server setup.
|
||||
|
||||
## Setup
|
||||
|
||||
Install dependencies before first use:
|
||||
|
||||
```bash
|
||||
npm install --prefix ~/.claude/skills/browser/browser ws
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
All scripts connect to Chrome on `localhost:9222`.
|
||||
|
||||
### start.js - Launch Chrome
|
||||
|
||||
```bash
|
||||
scripts/start.js # Fresh profile
|
||||
scripts/start.js --profile # Use persistent profile (keeps cookies/auth)
|
||||
```
|
||||
|
||||
### nav.js - Navigate
|
||||
|
||||
```bash
|
||||
scripts/nav.js https://example.com # Navigate current tab
|
||||
scripts/nav.js https://example.com --new # Open in new tab
|
||||
```
|
||||
|
||||
### eval.js - Execute JavaScript
|
||||
|
||||
```bash
|
||||
scripts/eval.js 'document.title'
|
||||
scripts/eval.js '(() => { const x = 1; return x + 1; })()'
|
||||
```
|
||||
|
||||
Use single expressions or IIFE for multiple statements.
|
||||
|
||||
### screenshot.js - Capture Screenshot
|
||||
|
||||
```bash
|
||||
scripts/screenshot.js
|
||||
```
|
||||
|
||||
Returns `{ path, filename }` of saved PNG in temp directory.
|
||||
|
||||
### pick.js - Visual Element Picker
|
||||
|
||||
```bash
|
||||
scripts/pick.js "Click the submit button"
|
||||
```
|
||||
|
||||
Returns element metadata: tag, id, classes, text, href, selector, rect.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Launch Chrome: `scripts/start.js --profile` for authenticated sessions
|
||||
2. Navigate: `scripts/nav.js <url>`
|
||||
3. Inspect: `scripts/eval.js 'document.querySelector(...)'`
|
||||
4. Capture: `scripts/screenshot.js` or `scripts/pick.js`
|
||||
5. Return gathered data
|
||||
|
||||
## Key Points
|
||||
|
||||
- All operations run locally - credentials never leave the machine
|
||||
- Use `--profile` flag to preserve cookies and auth tokens
|
||||
- Scripts return structured JSON for agent consumption
|
||||
BIN
skills/browser/browser.zip
Normal file
BIN
skills/browser/browser.zip
Normal file
Binary file not shown.
33
skills/browser/package-lock.json
generated
Normal file
33
skills/browser/package-lock.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "browser",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
skills/browser/package.json
Normal file
5
skills/browser/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
62
skills/browser/scripts/eval.cjs
Executable file
62
skills/browser/scripts/eval.cjs
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
// Execute JavaScript in the active browser tab
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const code = process.argv[2];
|
||||
if (!code) {
|
||||
console.error('Usage: eval.js <javascript-expression>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
id: 1,
|
||||
method: 'Runtime.evaluate',
|
||||
params: {
|
||||
expression: code,
|
||||
returnByValue: true,
|
||||
awaitPromise: true
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
if (msg.result.exceptionDetails) {
|
||||
console.error('Error:', msg.result.exceptionDetails.text);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(msg.result.result.value ?? msg.result.result));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', e => {
|
||||
console.error('WebSocket error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
70
skills/browser/scripts/nav.cjs
Executable file
70
skills/browser/scripts/nav.cjs
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
// Navigate to URL in current or new tab
|
||||
const http = require('http');
|
||||
|
||||
const url = process.argv[2];
|
||||
const newTab = process.argv.includes('--new');
|
||||
|
||||
if (!url) {
|
||||
console.error('Usage: nav.js <url> [--new]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function createTab(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`http://localhost:9222/json/new?${encodeURIComponent(url)}`, res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function navigate(targetId, url) {
|
||||
const WebSocket = require('ws');
|
||||
const targets = await getTargets();
|
||||
const target = targets.find(t => t.id === targetId);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({ id: 1, method: 'Page.navigate', params: { url } }));
|
||||
});
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
resolve(msg.result);
|
||||
}
|
||||
});
|
||||
ws.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (newTab) {
|
||||
const tab = await createTab(url);
|
||||
console.log(JSON.stringify({ action: 'created', tabId: tab.id, url }));
|
||||
} else {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
await navigate(page.id, url);
|
||||
console.log(JSON.stringify({ action: 'navigated', tabId: page.id, url }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
87
skills/browser/scripts/pick.cjs
Executable file
87
skills/browser/scripts/pick.cjs
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
// Visual element picker - click to select DOM nodes
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const hint = process.argv[2] || 'Click an element to select it';
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const pickerScript = `
|
||||
(function(hint) {
|
||||
return new Promise(resolve => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:999999;cursor:crosshair;';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.textContent = hint;
|
||||
label.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:8px 16px;border-radius:4px;z-index:1000000;font:14px sans-serif;';
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
document.body.appendChild(label);
|
||||
|
||||
overlay.onclick = e => {
|
||||
overlay.remove();
|
||||
label.remove();
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el) return resolve(null);
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
resolve({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
classes: [...el.classList],
|
||||
text: el.textContent?.slice(0, 100)?.trim() || null,
|
||||
href: el.href || null,
|
||||
selector: el.id ? '#' + el.id : el.className ? el.tagName.toLowerCase() + '.' + [...el.classList].join('.') : el.tagName.toLowerCase(),
|
||||
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
||||
});
|
||||
};
|
||||
});
|
||||
})`;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
id: 1,
|
||||
method: 'Runtime.evaluate',
|
||||
params: {
|
||||
expression: `${pickerScript}(${JSON.stringify(hint)})`,
|
||||
returnByValue: true,
|
||||
awaitPromise: true
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
console.log(JSON.stringify(msg.result.result.value, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', e => {
|
||||
console.error('WebSocket error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
54
skills/browser/scripts/screenshot.cjs
Executable file
54
skills/browser/scripts/screenshot.cjs
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
// Capture screenshot of the active browser tab
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
id: 1,
|
||||
method: 'Page.captureScreenshot',
|
||||
params: { format: 'png' }
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
const filename = `screenshot-${Date.now()}.png`;
|
||||
const filepath = path.join(os.tmpdir(), filename);
|
||||
fs.writeFileSync(filepath, Buffer.from(msg.result.data, 'base64'));
|
||||
console.log(JSON.stringify({ path: filepath, filename }));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', e => {
|
||||
console.error('WebSocket error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
35
skills/browser/scripts/start.cjs
Executable file
35
skills/browser/scripts/start.cjs
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
// Launch Chrome with remote debugging on port 9222
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const useProfile = process.argv.includes('--profile');
|
||||
const port = 9222;
|
||||
|
||||
// Find Chrome executable
|
||||
const chromePaths = {
|
||||
darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
linux: '/usr/bin/google-chrome',
|
||||
win32: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
||||
};
|
||||
const chromePath = chromePaths[process.platform];
|
||||
|
||||
// Build args
|
||||
const args = [
|
||||
`--remote-debugging-port=${port}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check'
|
||||
];
|
||||
|
||||
if (useProfile) {
|
||||
const profileDir = path.join(os.homedir(), '.chrome-debug-profile');
|
||||
args.push(`--user-data-dir=${profileDir}`);
|
||||
} else {
|
||||
args.push(`--user-data-dir=${path.join(os.tmpdir(), 'chrome-debug-' + Date.now())}`);
|
||||
}
|
||||
|
||||
console.log(`Starting Chrome on port ${port}${useProfile ? ' (with profile)' : ''}...`);
|
||||
const chrome = spawn(chromePath, args, { detached: true, stdio: 'ignore' });
|
||||
chrome.unref();
|
||||
console.log(`Chrome launched (PID: ${chrome.pid})`);
|
||||
Reference in New Issue
Block a user