Initial commit
This commit is contained in:
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