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:
dan 2025-12-29 00:09:50 -05:00
parent efb7cdaffc
commit d24bedcab3
6 changed files with 338 additions and 1 deletions

View file

@ -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."}

View 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

View 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)

View 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
}

View 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"
'';
};
}
);
}

View 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()