diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d80f82e..f4831c8 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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."} diff --git a/skills/playwright-visit/README.md b/skills/playwright-visit/README.md new file mode 100644 index 0000000..8f405e4 --- /dev/null +++ b/skills/playwright-visit/README.md @@ -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 diff --git a/skills/playwright-visit/SKILL.md b/skills/playwright-visit/SKILL.md new file mode 100644 index 0000000..f2bfaee --- /dev/null +++ b/skills/playwright-visit/SKILL.md @@ -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 ` - 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) diff --git a/skills/playwright-visit/flake.lock b/skills/playwright-visit/flake.lock new file mode 100644 index 0000000..c2cc24a --- /dev/null +++ b/skills/playwright-visit/flake.lock @@ -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 +} diff --git a/skills/playwright-visit/flake.nix b/skills/playwright-visit/flake.nix new file mode 100644 index 0000000..d363b2a --- /dev/null +++ b/skills/playwright-visit/flake.nix @@ -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" + ''; + }; + } + ); +} diff --git a/skills/playwright-visit/scripts/visit.py b/skills/playwright-visit/scripts/visit.py new file mode 100755 index 0000000..8e48f0f --- /dev/null +++ b/skills/playwright-visit/scripts/visit.py @@ -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()