Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "browser-tools",
|
||||||
|
"description": "Chrome automation tools for agent-assisted web testing and interaction using Chrome DevTools Protocol",
|
||||||
|
"version": "1.2.7",
|
||||||
|
"author": {
|
||||||
|
"name": "Lukas Trumm"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# browser-tools
|
||||||
|
|
||||||
|
Chrome automation tools for agent-assisted web testing and interaction using Chrome DevTools Protocol
|
||||||
88
commands/setup.md
Normal file
88
commands/setup.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(find:*), Bash(echo:*), Bash(cd:*), Bash(pnpm install:*), Bash(mkdir:*), Bash(chmod:*), Bash(ln:*), Bash(grep:*)
|
||||||
|
---
|
||||||
|
|
||||||
|
Set up browser-tools plugin by installing dependencies and creating global symlinks.
|
||||||
|
|
||||||
|
## Execution steps:
|
||||||
|
|
||||||
|
**Step 1: Find the plugin scripts directory**
|
||||||
|
|
||||||
|
Locate the browser-tools scripts directory (search in ~/.claude/plugins for browser-tools plugin):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SCRIPTS_DIR=$(find ~/.claude/plugins -type d -path "*/browser-tools/skills/browser-tools/scripts" 2>/dev/null | head -1)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify it was found:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "Found scripts at: $SCRIPTS_DIR"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$SCRIPTS_DIR" && pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Create ~/bin directory**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Make scripts executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x "$SCRIPTS_DIR"/browser-*.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Create symlinks (WITHOUT .js extension)**
|
||||||
|
|
||||||
|
IMPORTANT: Symlink names should NOT have .js extension:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$SCRIPTS_DIR/browser-start.js" ~/bin/browser-start
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$SCRIPTS_DIR/browser-nav.js" ~/bin/browser-nav
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$SCRIPTS_DIR/browser-eval.js" ~/bin/browser-eval
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$SCRIPTS_DIR/browser-screenshot.js" ~/bin/browser-screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$SCRIPTS_DIR/browser-pick.js" ~/bin/browser-pick
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$SCRIPTS_DIR/browser-cookies.js" ~/bin/browser-cookies
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6: Verify PATH**
|
||||||
|
|
||||||
|
Check if ~/bin is in PATH:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
case "$PATH" in
|
||||||
|
*"$HOME/bin"*) echo "✓ ~/bin is in PATH" ;;
|
||||||
|
*) echo "⚠ WARNING: Add ~/bin to PATH in your shell profile" ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
After completion, report:
|
||||||
|
|
||||||
|
- Dependencies installed: ✓
|
||||||
|
- Symlinks created: browser-start, browser-nav, browser-eval, browser-screenshot, browser-pick, browser-cookies
|
||||||
|
- PATH status: (OK or needs manual addition)
|
||||||
|
|
||||||
|
**The browser-tools skill is now ready!** Ask me to test web pages, take screenshots, or interact with browsers.
|
||||||
77
plugin.lock.json
Normal file
77
plugin.lock.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:lttr/claude-marketplace:plugins/browser-tools",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "2e0e2846b941b993e0bdab80f75c459ffad8b855",
|
||||||
|
"treeHash": "758e396c99a376f9d927464de74bfae7a68078b4942a36e37e82b9c32486fc60",
|
||||||
|
"generatedAt": "2025-11-28T10:20:21.620586Z",
|
||||||
|
"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-tools",
|
||||||
|
"description": "Chrome automation tools for agent-assisted web testing and interaction using Chrome DevTools Protocol",
|
||||||
|
"version": "1.2.7"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "d631cbdc1c1b93886be865602e555c07af337f53c07f44ad1e41595532e8f1fb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "bc511a83828882843f5e03f43fadab062048f2ce0823eccca57ab240136e39f9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/setup.md",
|
||||||
|
"sha256": "13c6dcf099dbe82c67db99348a7c4877c7d71d5d1d885318609162a9460b72b2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/SKILL.md",
|
||||||
|
"sha256": "0726cebe68850538c92cebc80fa6f68f17f0aa585f5d1c9326134e88e2cb6698"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/browser-start.js",
|
||||||
|
"sha256": "64f2ce9ed09278ec88ecb944db5befa22ab6586c4943a94f4569f0f0f422fd45"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/browser-screenshot.js",
|
||||||
|
"sha256": "f9ab1071682562c9da9300a74ba55a4880cdcfafedbaf24f2b84849bd3db9af7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/browser-cookies.js",
|
||||||
|
"sha256": "7ed111ab522ba03186aea0d9b9cf0c7bab6bd5997edd0796af0934df73caab93"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/browser-nav.js",
|
||||||
|
"sha256": "1cc5dc438ce4999b814e9ec90db76d78ee4766534416cdf4a74d1473c3b2e44a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/browser-eval.js",
|
||||||
|
"sha256": "40e8e29679f6ac32f7012827c7e4571cf5ab706004a8e773dbf4d6e98abd1889"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/package.json",
|
||||||
|
"sha256": "e76bca448ab197da8a3779f38471ae0811c3183b61660a912e8720d4d8a4085d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/browser-tools/scripts/browser-pick.js",
|
||||||
|
"sha256": "d134f9f8ae7c25904223a27c073afe242ddd00ce397dfdfc1108085e22bf5010"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "758e396c99a376f9d927464de74bfae7a68078b4942a36e37e82b9c32486fc60"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
144
skills/browser-tools/SKILL.md
Normal file
144
skills/browser-tools/SKILL.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
name: browser-tools
|
||||||
|
description: Use this skill when the user asks to test, verify, interact with, or automate web pages and browsers. Trigger for requests involving Chrome automation, browser testing, web scraping, screenshot capture, element selection, or checking web applications. Also trigger when user mentions "browser tools" explicitly.
|
||||||
|
allowed-tools: Bash(browser-start:*), Bash(browser-nav:*), Bash(browser-eval:*), Bash(browser-screenshot:*), Bash(browser-pick:*), Bash(browser-cookies:*), Read, Read(/tmp/screenshot*), Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# Browser Tools
|
||||||
|
|
||||||
|
Chrome DevTools Protocol automation for agent-assisted web testing and interaction. Uses Chrome running on `:9222` with remote debugging.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
**Run `/browser-tools:setup` once after plugin installation** - This installs dependencies and creates global symlinks for all browser scripts.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
When user asks to test, verify, or interact with web pages using browser tools:
|
||||||
|
|
||||||
|
1. **Start Chrome** - Launch with debugging enabled
|
||||||
|
2. **Navigate & interact** - Use scripts to navigate, evaluate JS, take screenshots
|
||||||
|
3. **Return results** - Show output/screenshots to user
|
||||||
|
|
||||||
|
**IMPORTANT**: Use command names directly (e.g., `browser-start`), NOT full paths (e.g., `~/bin/browser-start`). Commands are in PATH after setup.
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
All scripts located in `skills/browser-tools/scripts/`:
|
||||||
|
|
||||||
|
### browser-start
|
||||||
|
|
||||||
|
By default use the persistent profile at `/tmp/chrome-profile-browser-tools`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-start --profile # Persistent profile
|
||||||
|
browser-start # Fresh profile
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch Chrome with remote debugging on port 9222. Use `--profile` to maintain login state between sessions.
|
||||||
|
|
||||||
|
### browser-nav
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-nav https://example.com
|
||||||
|
browser-nav https://example.com --new
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigate to URLs. Use `--new` to open in new tab instead of current tab.
|
||||||
|
|
||||||
|
### browser-eval
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-eval 'document.title'
|
||||||
|
browser-eval 'document.querySelectorAll("a").length'
|
||||||
|
browser-eval 'Array.from(document.querySelectorAll("h1")).map(h => h.textContent)'
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute JavaScript in active tab. Runs in async context. Use for:
|
||||||
|
|
||||||
|
- Extract data from pages
|
||||||
|
- Inspect page state
|
||||||
|
- Manipulate DOM
|
||||||
|
- Test page functionality
|
||||||
|
|
||||||
|
### browser-screenshot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture current viewport, returns temp file path. Use Read tool to show screenshot to user.
|
||||||
|
|
||||||
|
### browser-pick
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-pick # Uses default message "Select element(s)"
|
||||||
|
browser-pick "Select the submit button"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Interactive element picker** - Launches UI overlay for user to click and select elements. Returns element details (tag, id, class, text, html, parent hierarchy). Message parameter is optional.
|
||||||
|
|
||||||
|
Use when:
|
||||||
|
|
||||||
|
- User says "click that button" or "extract those items"
|
||||||
|
- Need specific selectors but page structure is unclear
|
||||||
|
- User wants to identify elements visually
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
|
||||||
|
- Click to select single element
|
||||||
|
- Cmd/Ctrl+Click for multiple selections
|
||||||
|
- Enter to finish (when multiple selected)
|
||||||
|
- ESC to cancel
|
||||||
|
|
||||||
|
### browser-cookies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-cookies
|
||||||
|
```
|
||||||
|
|
||||||
|
Display all cookies for current tab (domain, path, httpOnly, secure flags). Use for debugging auth issues.
|
||||||
|
|
||||||
|
## Workflow Examples
|
||||||
|
|
||||||
|
### Test dev server feature
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start browser with persistent profile
|
||||||
|
browser-start --profile
|
||||||
|
|
||||||
|
# Navigate to dev server
|
||||||
|
browser-nav http://localhost:3000
|
||||||
|
|
||||||
|
# Test functionality
|
||||||
|
browser-eval 'document.querySelector("#new-feature").textContent'
|
||||||
|
|
||||||
|
# Take screenshot
|
||||||
|
SCREENSHOT=$(browser-screenshot)
|
||||||
|
# Then use Read tool to show screenshot at $SCREENSHOT path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-start --profile
|
||||||
|
browser-nav https://app.example.com/login
|
||||||
|
browser-cookies
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extract data from page
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-start
|
||||||
|
browser-nav https://example.com
|
||||||
|
browser-eval 'Array.from(document.querySelectorAll(".product")).map(p => ({name: p.querySelector(".title").textContent, price: p.querySelector(".price").textContent}))'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Chrome must be installed** - Scripts use `google-chrome` binary
|
||||||
|
- **Port 9222** - Chrome runs with `--remote-debugging-port=9222`
|
||||||
|
- **Profile location** - `/tmp/chrome-profile-browser-tools` when using `--profile`
|
||||||
|
- **Temp screenshots** - Screenshots saved to OS temp directory
|
||||||
|
- **Dependencies** - Requires `chrome-remote-interface` (installed via `/browser-tools:setup`)
|
||||||
|
- **Error handling** - If scripts fail, check Chrome is running and port 9222 is available
|
||||||
28
skills/browser-tools/scripts/browser-cookies.js
Executable file
28
skills/browser-tools/scripts/browser-cookies.js
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import puppeteer from "puppeteer-core"
|
||||||
|
|
||||||
|
const b = await puppeteer.connect({
|
||||||
|
browserURL: "http://localhost:9222",
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = (await b.pages()).at(-1)
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
console.error("✗ No active tab found")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = await p.cookies()
|
||||||
|
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
console.log(`${cookie.name}: ${cookie.value}`)
|
||||||
|
console.log(` domain: ${cookie.domain}`)
|
||||||
|
console.log(` path: ${cookie.path}`)
|
||||||
|
console.log(` httpOnly: ${cookie.httpOnly}`)
|
||||||
|
console.log(` secure: ${cookie.secure}`)
|
||||||
|
console.log("")
|
||||||
|
}
|
||||||
|
|
||||||
|
await b.disconnect()
|
||||||
51
skills/browser-tools/scripts/browser-eval.js
Executable file
51
skills/browser-tools/scripts/browser-eval.js
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import puppeteer from "puppeteer-core"
|
||||||
|
|
||||||
|
const code = process.argv.slice(2).join(" ")
|
||||||
|
if (!code) {
|
||||||
|
console.log("Usage: browser-eval.js 'code'")
|
||||||
|
console.log("\nExamples:")
|
||||||
|
console.log(' browser-eval.js "document.title"')
|
||||||
|
console.log(" browser-eval.js \"document.querySelectorAll('a').length\"")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const b = await puppeteer.connect({
|
||||||
|
browserURL: "http://localhost:9222",
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = (await b.pages()).at(-1)
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
console.error("✗ No active tab found")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await p.evaluate((c) => {
|
||||||
|
const AsyncFunction = (async () => {}).constructor
|
||||||
|
// Try as expression first, fall back to statements
|
||||||
|
try {
|
||||||
|
return new AsyncFunction(`return (${c})`)()
|
||||||
|
} catch {
|
||||||
|
return new AsyncFunction(c)()
|
||||||
|
}
|
||||||
|
}, code)
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
if (i > 0) console.log("")
|
||||||
|
for (const [key, value] of Object.entries(result[i])) {
|
||||||
|
console.log(`${key}: ${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof result === "object" && result !== null) {
|
||||||
|
for (const [key, value] of Object.entries(result)) {
|
||||||
|
console.log(`${key}: ${value}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
await b.disconnect()
|
||||||
66
skills/browser-tools/scripts/browser-nav.js
Executable file
66
skills/browser-tools/scripts/browser-nav.js
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import puppeteer from "puppeteer-core"
|
||||||
|
|
||||||
|
const url = process.argv[2]
|
||||||
|
const newTab = process.argv[3] === "--new"
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.log("Usage: browser-nav.js <url> [--new]")
|
||||||
|
console.log("\nExamples:")
|
||||||
|
console.log(
|
||||||
|
" browser-nav.js https://example.com # Navigate current tab",
|
||||||
|
)
|
||||||
|
console.log(" browser-nav.js https://example.com --new # Open in new tab")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect with retry logic in case Chrome just started
|
||||||
|
let b
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
try {
|
||||||
|
b = await puppeteer.connect({
|
||||||
|
browserURL: "http://localhost:9222",
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
} catch (err) {
|
||||||
|
if (i === 4) {
|
||||||
|
console.error("✗ Failed to connect to Chrome on :9222")
|
||||||
|
console.error(" Make sure Chrome is running (use browser-start.js)")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate with graceful handling of frame detachment errors.
|
||||||
|
* These occur when redirects or JS cause the frame to be replaced mid-navigation.
|
||||||
|
*/
|
||||||
|
async function navigate(page, targetUrl) {
|
||||||
|
try {
|
||||||
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded" })
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err.message.includes("frame was detached") ||
|
||||||
|
err.message.includes("Target closed")
|
||||||
|
) {
|
||||||
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTab) {
|
||||||
|
const p = await b.newPage()
|
||||||
|
await navigate(p, url)
|
||||||
|
console.log("✓ Opened:", url)
|
||||||
|
} else {
|
||||||
|
const p = (await b.pages()).at(-1)
|
||||||
|
await navigate(p, url)
|
||||||
|
console.log("✓ Navigated to:", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
await b.disconnect()
|
||||||
146
skills/browser-tools/scripts/browser-pick.js
Executable file
146
skills/browser-tools/scripts/browser-pick.js
Executable file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import puppeteer from "puppeteer-core"
|
||||||
|
|
||||||
|
const message = process.argv.slice(2).join(" ") || "Select element(s)"
|
||||||
|
|
||||||
|
const b = await puppeteer.connect({
|
||||||
|
browserURL: "http://localhost:9222",
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = (await b.pages()).at(-1)
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
console.error("✗ No active tab found")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject pick() helper into current page
|
||||||
|
await p.evaluate(() => {
|
||||||
|
if (!window.pick) {
|
||||||
|
window.pick = async (message = "Select element(s)") => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const selections = []
|
||||||
|
const selectedElements = new Set()
|
||||||
|
|
||||||
|
const overlay = document.createElement("div")
|
||||||
|
overlay.style.cssText =
|
||||||
|
"position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none"
|
||||||
|
|
||||||
|
const highlight = document.createElement("div")
|
||||||
|
highlight.style.cssText =
|
||||||
|
"position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s"
|
||||||
|
overlay.appendChild(highlight)
|
||||||
|
|
||||||
|
const banner = document.createElement("div")
|
||||||
|
banner.style.cssText =
|
||||||
|
"position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647"
|
||||||
|
|
||||||
|
const updateBanner = () => {
|
||||||
|
banner.textContent = `${message} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`
|
||||||
|
}
|
||||||
|
updateBanner()
|
||||||
|
|
||||||
|
document.body.append(banner, overlay)
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
document.removeEventListener("mousemove", onMove, true)
|
||||||
|
document.removeEventListener("click", onClick, true)
|
||||||
|
document.removeEventListener("keydown", onKey, true)
|
||||||
|
overlay.remove()
|
||||||
|
banner.remove()
|
||||||
|
selectedElements.forEach((el) => {
|
||||||
|
el.style.outline = ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMove = (e) => {
|
||||||
|
const el = document.elementFromPoint(e.clientX, e.clientY)
|
||||||
|
if (!el || overlay.contains(el) || banner.contains(el)) return
|
||||||
|
const r = el.getBoundingClientRect()
|
||||||
|
highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${r.top}px;left:${r.left}px;width:${r.width}px;height:${r.height}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildElementInfo = (el) => {
|
||||||
|
const parents = []
|
||||||
|
let current = el.parentElement
|
||||||
|
while (current && current !== document.body) {
|
||||||
|
const parentInfo = current.tagName.toLowerCase()
|
||||||
|
const id = current.id ? `#${current.id}` : ""
|
||||||
|
const cls = current.className
|
||||||
|
? `.${current.className.trim().split(/\s+/).join(".")}`
|
||||||
|
: ""
|
||||||
|
parents.push(parentInfo + id + cls)
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tag: el.tagName.toLowerCase(),
|
||||||
|
id: el.id || null,
|
||||||
|
class: el.className || null,
|
||||||
|
text: el.textContent?.trim().slice(0, 200) || null,
|
||||||
|
html: el.outerHTML.slice(0, 500),
|
||||||
|
parents: parents.join(" > "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = (e) => {
|
||||||
|
if (banner.contains(e.target)) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const el = document.elementFromPoint(e.clientX, e.clientY)
|
||||||
|
if (!el || overlay.contains(el) || banner.contains(el)) return
|
||||||
|
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
if (!selectedElements.has(el)) {
|
||||||
|
selectedElements.add(el)
|
||||||
|
el.style.outline = "3px solid #10b981"
|
||||||
|
selections.push(buildElementInfo(el))
|
||||||
|
updateBanner()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cleanup()
|
||||||
|
const info = buildElementInfo(el)
|
||||||
|
resolve(selections.length > 0 ? selections : info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
cleanup()
|
||||||
|
resolve(null)
|
||||||
|
} else if (e.key === "Enter" && selections.length > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
cleanup()
|
||||||
|
resolve(selections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onMove, true)
|
||||||
|
document.addEventListener("click", onClick, true)
|
||||||
|
document.addEventListener("keydown", onKey, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await p.evaluate((msg) => window.pick(msg), message)
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
if (i > 0) console.log("")
|
||||||
|
for (const [key, value] of Object.entries(result[i])) {
|
||||||
|
console.log(`${key}: ${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof result === "object" && result !== null) {
|
||||||
|
for (const [key, value] of Object.entries(result)) {
|
||||||
|
console.log(`${key}: ${value}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
await b.disconnect()
|
||||||
27
skills/browser-tools/scripts/browser-screenshot.js
Executable file
27
skills/browser-tools/scripts/browser-screenshot.js
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import puppeteer from "puppeteer-core"
|
||||||
|
|
||||||
|
const b = await puppeteer.connect({
|
||||||
|
browserURL: "http://localhost:9222",
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = (await b.pages()).at(-1)
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
console.error("✗ No active tab found")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
||||||
|
const filename = `screenshot-${timestamp}.png`
|
||||||
|
const filepath = join(tmpdir(), filename)
|
||||||
|
|
||||||
|
await p.screenshot({ path: filepath })
|
||||||
|
|
||||||
|
console.log(filepath)
|
||||||
|
|
||||||
|
await b.disconnect()
|
||||||
67
skills/browser-tools/scripts/browser-start.js
Executable file
67
skills/browser-tools/scripts/browser-start.js
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn, execSync } from "node:child_process"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import puppeteer from "puppeteer-core"
|
||||||
|
|
||||||
|
const useProfile = process.argv[2] === "--profile"
|
||||||
|
|
||||||
|
if (process.argv[2] && process.argv[2] !== "--profile") {
|
||||||
|
console.log("Usage: browser-start.js [--profile]")
|
||||||
|
console.log("\nOptions:")
|
||||||
|
console.log(" --profile Use persistent Chrome profile")
|
||||||
|
console.log("\nExamples:")
|
||||||
|
console.log(" browser-start.js # Start with fresh profile")
|
||||||
|
console.log(" browser-start.js --profile # Start with persistent profile")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill existing Chrome
|
||||||
|
try {
|
||||||
|
execSync("killall google-chrome chrome", { stdio: "ignore" })
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Wait a bit for processes to fully die
|
||||||
|
await new Promise((r) => setTimeout(r, 1000))
|
||||||
|
|
||||||
|
// Setup profile directory
|
||||||
|
const profileDir = useProfile
|
||||||
|
? "/tmp/chrome-profile-browser-tools"
|
||||||
|
: join(tmpdir(), `chrome-profile-${Date.now()}`)
|
||||||
|
|
||||||
|
execSync(`mkdir -p "${profileDir}"`, { stdio: "ignore" })
|
||||||
|
|
||||||
|
// Start Chrome in background (detached so Node can exit)
|
||||||
|
spawn(
|
||||||
|
"google-chrome",
|
||||||
|
["--remote-debugging-port=9222", `--user-data-dir=${profileDir}`],
|
||||||
|
{ detached: true, stdio: "ignore" },
|
||||||
|
).unref()
|
||||||
|
|
||||||
|
// Wait for Chrome to be ready by attempting to connect
|
||||||
|
let connected = false
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
try {
|
||||||
|
const browser = await puppeteer.connect({
|
||||||
|
browserURL: "http://localhost:9222",
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
await browser.disconnect()
|
||||||
|
// Brief delay to let Chrome fully stabilize after initial connection
|
||||||
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
connected = true
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
console.error("✗ Failed to connect to Chrome")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✓ Chrome started on :9222${useProfile ? " with persistent profile" : ""}`,
|
||||||
|
)
|
||||||
9
skills/browser-tools/scripts/package.json
Normal file
9
skills/browser-tools/scripts/package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "browser-tools-scripts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Chrome DevTools Protocol automation scripts for Claude Code browser-tools plugin",
|
||||||
|
"dependencies": {
|
||||||
|
"puppeteer-core": "^23.11.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user