feat: add playwright-visit skill for browser automation
- visit.py CLI with subcommands: screenshot, text, html, pdf - Uses system chromium on NixOS (no browser download) - Fresh profile each run (no cookies/history) - flake.nix provides playwright devShell - Options: --wait, --full-page Useful for JS-heavy sites where WebFetch fails. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
efb7cdaffc
commit
d24bedcab3
|
|
@ -92,7 +92,7 @@
|
|||
{"id":"skills-qeh","title":"Add README.md for web-research skill","description":"web-research skill has SKILL.md and scripts but no README.md. AGENTS.md says README.md is for humans, contains installation instructions, usage examples, prerequisites.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T11:58:14.475647113-08:00","updated_at":"2025-12-28T22:37:48.339288261-05:00","closed_at":"2025-12-28T22:37:48.339288261-05:00","close_reason":"Added README.md with prerequisites, usage examples, and cross-references","dependencies":[{"issue_id":"skills-qeh","depends_on_id":"skills-vb5","type":"blocks","created_at":"2025-11-30T12:01:30.278784381-08:00","created_by":"daemon","metadata":"{}"}]}
|
||||
{"id":"skills-r5c","title":"Extract shared logging library from scripts","description":"Duplicated logging/color functions across multiple scripts:\n- bin/deploy-skill.sh\n- skills/tufte-press/scripts/generate-and-build.sh\n- Other .specify scripts\n\nPattern repeated:\n- info(), warn(), error() functions\n- Color definitions (RED, GREEN, etc.)\n- Same 15-20 lines in each file\n\nFix:\n- Create scripts/common-logging.sh\n- Source from all scripts that need it\n- Estimated reduction: 30+ lines of duplication\n\nSeverity: MEDIUM","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-24T02:50:58.324852578-05:00","updated_at":"2025-12-24T02:50:58.324852578-05:00"}
|
||||
{"id":"skills-rex","title":"Test integration on worklog skill","description":"Use worklog skill as first real test case:\n- Create wisp for worklog execution\n- Capture execution trace\n- Test squash → digest\n- Validate trace format captures enough info for replay\n\nMigrated from dotfiles-drs.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-23T19:21:18.75525644-05:00","updated_at":"2025-12-23T19:21:18.75525644-05:00","dependencies":[{"issue_id":"skills-rex","depends_on_id":"skills-3em","type":"blocks","created_at":"2025-12-23T19:22:00.34922734-05:00","created_by":"dan"}]}
|
||||
{"id":"skills-rpf","title":"Implement playwright-visit skill for browser automation","description":"## Overview\nBrowser automation skill using Playwright to visit web pages, take screenshots, and extract content.\n\n## Key Findings (from dotfiles investigation)\n\n### Working Setup\n- Use `python312Packages.playwright` from nixpkgs (handles Node driver binary patching for NixOS)\n- Use `executable_path='/run/current-system/sw/bin/chromium'` to use system chromium\n- No `playwright install` needed - no browser binary downloads\n\n### Profile Behavior\n- Fresh/blank profile every launch by default\n- No cookies, history, or logins from user's browser\n- Can persist state with `storage_state` parameter if needed\n\n### Example Code\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n browser = p.chromium.launch(\n executable_path='/run/current-system/sw/bin/chromium',\n headless=True\n )\n page = browser.new_page()\n page.goto('https://example.com')\n print(page.title())\n browser.close()\n```\n\n### Why Not uv/pip?\n- Playwright pip package bundles a Node.js driver binary\n- NixOS can't run dynamically linked executables without patching\n- nixpkgs playwright handles this properly\n\n## Implementation Plan\n1. Create `skills/playwright-visit/` directory\n2. Add flake.nix with devShell providing playwright\n3. Create CLI script with subcommands:\n - `screenshot \u003curl\u003e \u003coutput.png\u003e` - capture page\n - `text \u003curl\u003e` - extract text content \n - `html \u003curl\u003e` - get rendered HTML\n - `pdf \u003curl\u003e \u003coutput.pdf\u003e` - save as PDF\n4. Create skill definition for Claude Code integration\n5. Document usage in skill README\n\n## Dependencies\n- nixpkgs python312Packages.playwright\n- System chromium (already in dotfiles)\n\n## Related\n- dotfiles issue dotfiles-m09 (playwright skill request)","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-16T16:02:28.577381007-08:00","updated_at":"2025-12-16T16:02:28.577381007-08:00"}
|
||||
{"id":"skills-rpf","title":"Implement playwright-visit skill for browser automation","description":"## Overview\nBrowser automation skill using Playwright to visit web pages, take screenshots, and extract content.\n\n## Key Findings (from dotfiles investigation)\n\n### Working Setup\n- Use `python312Packages.playwright` from nixpkgs (handles Node driver binary patching for NixOS)\n- Use `executable_path='/run/current-system/sw/bin/chromium'` to use system chromium\n- No `playwright install` needed - no browser binary downloads\n\n### Profile Behavior\n- Fresh/blank profile every launch by default\n- No cookies, history, or logins from user's browser\n- Can persist state with `storage_state` parameter if needed\n\n### Example Code\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n browser = p.chromium.launch(\n executable_path='/run/current-system/sw/bin/chromium',\n headless=True\n )\n page = browser.new_page()\n page.goto('https://example.com')\n print(page.title())\n browser.close()\n```\n\n### Why Not uv/pip?\n- Playwright pip package bundles a Node.js driver binary\n- NixOS can't run dynamically linked executables without patching\n- nixpkgs playwright handles this properly\n\n## Implementation Plan\n1. Create `skills/playwright-visit/` directory\n2. Add flake.nix with devShell providing playwright\n3. Create CLI script with subcommands:\n - `screenshot \u003curl\u003e \u003coutput.png\u003e` - capture page\n - `text \u003curl\u003e` - extract text content \n - `html \u003curl\u003e` - get rendered HTML\n - `pdf \u003curl\u003e \u003coutput.pdf\u003e` - save as PDF\n4. Create skill definition for Claude Code integration\n5. Document usage in skill README\n\n## Dependencies\n- nixpkgs python312Packages.playwright\n- System chromium (already in dotfiles)\n\n## Related\n- dotfiles issue dotfiles-m09 (playwright skill request)","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2025-12-16T16:02:28.577381007-08:00","updated_at":"2025-12-28T23:36:17.277851709-05:00"}
|
||||
{"id":"skills-s92","title":"Add tests for config injection (deploy-skill.sh)","description":"File: bin/deploy-skill.sh (lines 112-137)\n\nCritical logic with NO test coverage:\n- Idempotency (running twice should be safe)\n- Correct brace matching in Nix\n- Syntax validity of injected config\n- Rollback on failure\n\nRisk: MEDIUM-HIGH - can break dotfiles Nix config\n\nFix:\n- Test idempotent injection\n- Validate Nix syntax after injection\n- Test with malformed input\n\nSeverity: MEDIUM","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-24T02:51:01.314513824-05:00","updated_at":"2025-12-24T02:51:01.314513824-05:00"}
|
||||
{"id":"skills-ty7","title":"Define trace levels (audit vs debug)","description":"Two trace levels to manage noise vs utility:\n\n1. Audit trace (minimal, safe, always on):\n - skill id/ref, start/end\n - high-level checkpoints\n - artifact hashes/paths\n - exit status\n\n2. Debug trace (opt-in, verbose):\n - tool calls with args\n - stdout/stderr snippets\n - expanded inputs\n - timing details\n\nConsider OpenTelemetry span model as reference.\nGPT proposed this; Gemini focused on rotation/caps instead.","status":"in_progress","priority":3,"issue_type":"task","created_at":"2025-12-23T19:49:48.514684945-05:00","updated_at":"2025-12-23T20:05:23.244193346-05:00"}
|
||||
{"id":"skills-u3d","title":"Define skill trigger conditions","description":"How does an agent know WHEN to apply a skill/checklist?\n\nOptions:\n- frontmatter triggers: field with patterns\n- File-based detection\n- Agent judgment from description\n- Beads hooks on state transitions\n- LLM-based pattern detection","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-23T17:59:09.69468767-05:00","updated_at":"2025-12-28T22:25:38.579989006-05:00","closed_at":"2025-12-28T22:25:38.579989006-05:00","close_reason":"Resolved: agent judgment from description is the standard. Good descriptions + 'When to Use' sections are sufficient. No new trigger mechanism needed - would add complexity without clear benefit."}
|
||||
|
|
|
|||
62
skills/playwright-visit/README.md
Normal file
62
skills/playwright-visit/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# playwright-visit
|
||||
|
||||
Browser automation using Playwright for visiting web pages, taking screenshots, and extracting content.
|
||||
|
||||
## Overview
|
||||
|
||||
Uses headless Chromium to render pages (including JavaScript) and extract content. Useful for:
|
||||
- Screenshots of web pages for visual analysis
|
||||
- Extracting text from JS-heavy sites (where WebFetch fails)
|
||||
- Getting rendered HTML after JavaScript execution
|
||||
- Saving pages as PDF
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- NixOS with system chromium (`/run/current-system/sw/bin/chromium`)
|
||||
- Playwright Python package (provided via flake.nix)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/playwright-visit
|
||||
nix develop
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Screenshot
|
||||
./scripts/visit.py screenshot "https://example.com" /tmp/shot.png
|
||||
./scripts/visit.py screenshot "https://example.com" /tmp/full.png --full-page
|
||||
|
||||
# Extract text
|
||||
./scripts/visit.py text "https://example.com"
|
||||
|
||||
# Get rendered HTML
|
||||
./scripts/visit.py html "https://example.com"
|
||||
|
||||
# Save as PDF
|
||||
./scripts/visit.py pdf "https://example.com" /tmp/page.pdf
|
||||
|
||||
# Wait longer for slow pages (default: 1000ms)
|
||||
./scripts/visit.py screenshot "https://slow-site.com" /tmp/shot.png --wait 3000
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Launches headless Chromium using system binary
|
||||
2. Creates fresh browser profile (no cookies/logins)
|
||||
3. Navigates to URL and waits for network idle
|
||||
4. Extracts requested content
|
||||
5. Closes browser
|
||||
|
||||
## Limitations
|
||||
|
||||
- No authentication support (fresh profile each run)
|
||||
- Requires NixOS with system chromium
|
||||
- Headless only (no visible browser window)
|
||||
|
||||
## See Also
|
||||
|
||||
- **WebFetch**: For simple HTTP fetches (faster, no browser)
|
||||
- **niri-window-capture**: For capturing local application windows
|
||||
63
skills/playwright-visit/SKILL.md
Normal file
63
skills/playwright-visit/SKILL.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
name: playwright-visit
|
||||
description: Visit web pages using Playwright browser automation. Capture screenshots, extract text, get rendered HTML, or save as PDF.
|
||||
---
|
||||
|
||||
# Playwright Visit
|
||||
|
||||
Browser automation skill using Playwright to visit web pages and extract content. Uses headless Chromium with a fresh profile (no cookies/history from user's browser).
|
||||
|
||||
## When to Use
|
||||
|
||||
- "Take a screenshot of [url]"
|
||||
- "Get the text content from [webpage]"
|
||||
- "Capture [url] as a screenshot"
|
||||
- "Extract the rendered HTML from [page]"
|
||||
- "Save [url] as a PDF"
|
||||
- When WebFetch fails on JavaScript-heavy sites
|
||||
|
||||
## Process
|
||||
|
||||
1. Identify the URL and desired output format from user request
|
||||
2. Run the appropriate helper script command
|
||||
3. Return the result (file path for screenshot/pdf, content for text/html)
|
||||
|
||||
## Helper Scripts
|
||||
|
||||
### visit.py
|
||||
|
||||
**Screenshot** - Capture page as PNG:
|
||||
```bash
|
||||
./scripts/visit.py screenshot "https://example.com" /tmp/screenshot.png
|
||||
```
|
||||
|
||||
**Text** - Extract visible text content:
|
||||
```bash
|
||||
./scripts/visit.py text "https://example.com"
|
||||
```
|
||||
|
||||
**HTML** - Get rendered HTML (after JavaScript):
|
||||
```bash
|
||||
./scripts/visit.py html "https://example.com"
|
||||
```
|
||||
|
||||
**PDF** - Save page as PDF:
|
||||
```bash
|
||||
./scripts/visit.py pdf "https://example.com" /tmp/page.pdf
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--wait <ms>` - Wait after page load (default: 1000ms)
|
||||
- `--full-page` - Capture full scrollable page (screenshot only)
|
||||
|
||||
## Requirements
|
||||
|
||||
- NixOS with `python312Packages.playwright` in devShell
|
||||
- System chromium at `/run/current-system/sw/bin/chromium`
|
||||
- Run from skill directory or use `nix develop` first
|
||||
|
||||
## Notes
|
||||
|
||||
- Uses fresh browser profile each run (no login state)
|
||||
- Headless by default
|
||||
- For authenticated pages, consider using `storage_state` parameter (not yet implemented)
|
||||
61
skills/playwright-visit/flake.lock
Normal file
61
skills/playwright-visit/flake.lock
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766902085,
|
||||
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
30
skills/playwright-visit/flake.nix
Normal file
30
skills/playwright-visit/flake.nix
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
description = "Playwright browser automation skill";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
python = pkgs.python312;
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
(python.withPackages (ps: [
|
||||
ps.playwright
|
||||
]))
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Playwright skill environment ready"
|
||||
echo "Run: ./scripts/visit.py --help"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
121
skills/playwright-visit/scripts/visit.py
Executable file
121
skills/playwright-visit/scripts/visit.py
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Browser automation CLI using Playwright."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
CHROMIUM_PATH = "/run/current-system/sw/bin/chromium"
|
||||
|
||||
|
||||
def get_browser(playwright):
|
||||
"""Launch headless chromium using system binary."""
|
||||
return playwright.chromium.launch(
|
||||
executable_path=CHROMIUM_PATH,
|
||||
headless=True,
|
||||
)
|
||||
|
||||
|
||||
def cmd_screenshot(args):
|
||||
"""Capture page as PNG screenshot."""
|
||||
with sync_playwright() as p:
|
||||
browser = get_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(args.url, wait_until="networkidle")
|
||||
page.wait_for_timeout(args.wait)
|
||||
page.screenshot(path=args.output, full_page=args.full_page)
|
||||
browser.close()
|
||||
print(args.output)
|
||||
|
||||
|
||||
def cmd_text(args):
|
||||
"""Extract visible text content from page."""
|
||||
with sync_playwright() as p:
|
||||
browser = get_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(args.url, wait_until="networkidle")
|
||||
page.wait_for_timeout(args.wait)
|
||||
text = page.inner_text("body")
|
||||
browser.close()
|
||||
print(text)
|
||||
|
||||
|
||||
def cmd_html(args):
|
||||
"""Get rendered HTML after JavaScript execution."""
|
||||
with sync_playwright() as p:
|
||||
browser = get_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(args.url, wait_until="networkidle")
|
||||
page.wait_for_timeout(args.wait)
|
||||
html = page.content()
|
||||
browser.close()
|
||||
print(html)
|
||||
|
||||
|
||||
def cmd_pdf(args):
|
||||
"""Save page as PDF."""
|
||||
with sync_playwright() as p:
|
||||
browser = get_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(args.url, wait_until="networkidle")
|
||||
page.wait_for_timeout(args.wait)
|
||||
page.pdf(path=args.output)
|
||||
browser.close()
|
||||
print(args.output)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Visit web pages using Playwright browser automation"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# Screenshot command
|
||||
p_screenshot = subparsers.add_parser("screenshot", help="Capture page as PNG")
|
||||
p_screenshot.add_argument("url", help="URL to visit")
|
||||
p_screenshot.add_argument("output", help="Output PNG path")
|
||||
p_screenshot.add_argument(
|
||||
"--wait", type=int, default=1000, help="Wait after load (ms)"
|
||||
)
|
||||
p_screenshot.add_argument(
|
||||
"--full-page", action="store_true", help="Capture full scrollable page"
|
||||
)
|
||||
p_screenshot.set_defaults(func=cmd_screenshot)
|
||||
|
||||
# Text command
|
||||
p_text = subparsers.add_parser("text", help="Extract visible text content")
|
||||
p_text.add_argument("url", help="URL to visit")
|
||||
p_text.add_argument("--wait", type=int, default=1000, help="Wait after load (ms)")
|
||||
p_text.set_defaults(func=cmd_text)
|
||||
|
||||
# HTML command
|
||||
p_html = subparsers.add_parser("html", help="Get rendered HTML")
|
||||
p_html.add_argument("url", help="URL to visit")
|
||||
p_html.add_argument("--wait", type=int, default=1000, help="Wait after load (ms)")
|
||||
p_html.set_defaults(func=cmd_html)
|
||||
|
||||
# PDF command
|
||||
p_pdf = subparsers.add_parser("pdf", help="Save page as PDF")
|
||||
p_pdf.add_argument("url", help="URL to visit")
|
||||
p_pdf.add_argument("output", help="Output PDF path")
|
||||
p_pdf.add_argument("--wait", type=int, default=1000, help="Wait after load (ms)")
|
||||
p_pdf.set_defaults(func=cmd_pdf)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check chromium exists
|
||||
if not Path(CHROMIUM_PATH).exists():
|
||||
print(f"Error: Chromium not found at {CHROMIUM_PATH}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
args.func(args)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue