Browser Automation Browser automation that maintains page state across command executions. Write small, focused commands to accomplish tasks incrementally. Choosing Your Approach - Local/source-available sites : Read the source code first to write selectors directly - Unknown page layouts : Use to discover elements, then to interact - Visual debugging : Take to see current page state Prerequisites Running Commands All commands use from the skill directory: ⚠️ IMPORTANT : Always use , NOT . The command automatically handles Python and dependencies from . Adding breaks dependency resolution. Wo…

, line)\n if match:\n indent, role, name, rest = match.groups()\n key = (role, name)\n if key in ref_lookup:\n refs = ref_lookup[key]\n idx = current_index.get(key, 0)\n if idx \u003c len(refs):\n ref_id, nth = refs[idx]\n current_index[key] = idx + 1\n\n # Build ref string with optional nth\n ref_str = f\"[ref={ref_id}]\"\n if nth is not None and nth > 0:\n ref_str += f\" [nth={nth}]\"\n\n # Insert ref before any trailing colon\n if rest.endswith(\":\"):\n line = f'{indent}{role} \"{name}\" {ref_str}:'\n else:\n line = f'{indent}{role} \"{name}\" {ref_str}{rest}'\n result.append(line)\n\n return \"\\n\".join(result)\n\n def select_snapshot_ref(self, name: str, ref: str) -> ElementHandle:\n \"\"\"Get an element handle by its ref from the last getAISnapshot call.\"\"\"\n page = self.get_playwright_page(name)\n\n element_handle = page.evaluate_handle(\n \"\"\"(refId) => {\n const refs = window.__devBrowserRefs;\n if (!refs) {\n throw new Error(\"No snapshot refs found. Call getAISnapshot first.\");\n }\n const element = refs[refId];\n if (!element) {\n throw new Error('Ref \"' + refId + '\" not found. Available refs: ' + Object.keys(refs).join(\", \"));\n }\n return element;\n }\"\"\",\n ref,\n )\n\n element = element_handle.as_element()\n if not element:\n raise RuntimeError(f\"Ref '{ref}' did not resolve to an element\")\n\n return element\n\n def wait_for_page_load(\n self,\n name: str,\n timeout: int = 10000,\n poll_interval: int = 50,\n minimum_wait: int = 100,\n wait_for_network_idle: bool = True,\n ) -> WaitForPageLoadResult:\n \"\"\"Wait for a page to finish loading using document.readyState and performance API.\"\"\"\n page = self.get_playwright_page(name)\n\n start_time = time.time() * 1000 # ms\n last_state = None\n\n # Wait minimum time first\n if minimum_wait > 0:\n time.sleep(minimum_wait / 1000)\n\n # Poll until ready or timeout\n while (time.time() * 1000 - start_time) \u003c timeout:\n try:\n last_state = page.evaluate(\"\"\"() => {\n const perf = performance;\n const doc = document;\n const now = perf.now();\n const resources = perf.getEntriesByType(\"resource\");\n const pending = [];\n\n const adPatterns = [\n \"doubleclick.net\", \"googlesyndication.com\", \"googletagmanager.com\",\n \"google-analytics.com\", \"facebook.net\", \"connect.facebook.net\",\n \"analytics\", \"ads\", \"tracking\", \"pixel\", \"hotjar.com\", \"clarity.ms\",\n \"mixpanel.com\", \"segment.com\", \"newrelic.com\", \"nr-data.net\",\n \"/tracker/\", \"/collector/\", \"/beacon/\", \"/telemetry/\", \"/log/\",\n \"/events/\", \"/track.\", \"/metrics/\"\n ];\n\n const nonCriticalTypes = [\"img\", \"image\", \"icon\", \"font\"];\n\n for (const entry of resources) {\n if (entry.responseEnd === 0) {\n const url = entry.name;\n const isAd = adPatterns.some(pattern => url.includes(pattern));\n if (isAd) continue;\n if (url.startsWith(\"data:\") || url.length > 500) continue;\n\n const loadingDuration = now - entry.startTime;\n if (loadingDuration > 10000) continue;\n\n const resourceType = entry.initiatorType || \"unknown\";\n if (nonCriticalTypes.includes(resourceType) && loadingDuration > 3000) continue;\n\n const isImageUrl = /\\\\.(jpg|jpeg|png|gif|webp|svg|ico)(\\\\?|$)/i.test(url);\n if (isImageUrl && loadingDuration > 3000) continue;\n\n pending.push({\n url: url,\n loadingDurationMs: Math.round(loadingDuration),\n resourceType: resourceType\n });\n }\n }\n\n return {\n documentReadyState: doc.readyState,\n documentLoading: doc.readyState !== \"complete\",\n pendingRequests: pending\n };\n }\"\"\")\n\n document_ready = last_state[\"documentReadyState\"] == \"complete\"\n network_idle = (\n not wait_for_network_idle or len(last_state[\"pendingRequests\"]) == 0\n )\n\n if document_ready and network_idle:\n return WaitForPageLoadResult(\n success=True,\n ready_state=last_state[\"documentReadyState\"],\n pending_requests=len(last_state[\"pendingRequests\"]),\n wait_time_ms=int(time.time() * 1000 - start_time),\n timed_out=False,\n )\n except Exception:\n # Page may be navigating, continue polling\n pass\n\n time.sleep(poll_interval / 1000)\n\n # Timeout reached\n return WaitForPageLoadResult(\n success=False,\n ready_state=last_state[\"documentReadyState\"] if last_state else \"unknown\",\n pending_requests=len(last_state[\"pendingRequests\"]) if last_state else 0,\n wait_time_ms=int(time.time() * 1000 - start_time),\n timed_out=True,\n )\n\n\n# === CLI Commands ===\n\n\ndef cmd_list(client: BrowserClient, args):\n \"\"\"List all pages in current session.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n print(\"Please make sure Max is open.\")\n return 1\n\n pages = client.list_pages()\n if not pages:\n print(\"No pages in current session.\")\n return 0\n\n print(f\"Pages in session ({len(pages)}):\")\n for p in pages:\n print(f\" - {p.name}: {p.title or p.url or '(empty)'}\")\n return 0\n\n\ndef cmd_create(client: BrowserClient, args):\n \"\"\"Create a new page.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page_info = client.create_page(args.name, args.url)\n print(f\"Created page: {page_info.name}\")\n print(f\" targetId: {page_info.target_id}\")\n if args.url:\n print(f\" url: {args.url}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n\n\ndef cmd_goto(client: BrowserClient, args):\n \"\"\"Navigate a page to URL.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_or_create_page(args.name, args.url)\n if page.url != args.url:\n page.goto(args.url)\n print(f\"Navigated to: {args.url}\")\n print(f\"Title: {page.title()}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef _resize_if_needed(image_path: str, max_size: int = 1568):\n \"\"\"缩放图片,保持宽高比,确保最长边不超过 max_size\"\"\"\n from PIL import Image\n\n with Image.open(image_path) as img:\n w, h = img.size\n if w \u003c= max_size and h \u003c= max_size:\n return # 无需缩放\n\n ratio = max_size / max(w, h)\n new_size = (int(w * ratio), int(h * ratio))\n img = img.resize(new_size, Image.LANCZOS)\n img.save(image_path)\n\n\ndef cmd_screenshot(client: BrowserClient, args):\n \"\"\"Take a screenshot of a page.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n output_path = args.output or f\"{args.name}.png\"\n page.screenshot(path=output_path, full_page=args.full_page)\n _resize_if_needed(output_path)\n print(f\"Screenshot saved to: {output_path}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_click(client: BrowserClient, args):\n \"\"\"Click an element on a page.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n page.click(args.selector)\n print(f\"Clicked: {args.selector}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_fill(client: BrowserClient, args):\n \"\"\"Fill an input element with text.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n page.fill(args.selector, args.text)\n print(f\"Filled '{args.selector}' with: {args.text}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_hover(client: BrowserClient, args):\n \"\"\"Hover over an element.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n page.hover(args.selector)\n print(f\"Hovered: {args.selector}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_keyboard(client: BrowserClient, args):\n \"\"\"Press a keyboard key.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n page.keyboard.press(args.key)\n print(f\"Pressed key: {args.key}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_evaluate(client: BrowserClient, args):\n \"\"\"Execute JavaScript on a page.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n result = page.evaluate(args.script)\n print(f\"Result: {json.dumps(result, indent=2, ensure_ascii=False)}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_text(client: BrowserClient, args):\n \"\"\"Get text content of an element.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n text = page.text_content(args.selector)\n print(text or \"(empty)\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_snapshot(client: BrowserClient, args):\n \"\"\"Get AI snapshot of a page.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n snapshot = client.get_ai_snapshot(args.name, interactive=args.interactive)\n print(snapshot)\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_select_ref(client: BrowserClient, args):\n \"\"\"Select element by ref and perform action.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n element = client.select_snapshot_ref(args.name, args.ref)\n action = args.action.lower()\n\n if action == \"click\":\n element.click()\n print(f\"Clicked element ref: {args.ref}\")\n elif action == \"fill\":\n if not args.value:\n print(\"Error: fill action requires a value\")\n return 1\n element.fill(args.value)\n print(f\"Filled element ref: {args.ref}\")\n elif action == \"hover\":\n element.hover()\n print(f\"Hovered element ref: {args.ref}\")\n elif action == \"text\":\n text = element.text_content()\n print(text or \"(empty)\")\n else:\n print(\n f\"Error: Unknown action '{action}'. Supported: click, fill, hover, text\"\n )\n return 1\n\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_wait_selector(client: BrowserClient, args):\n \"\"\"Wait for a selector to appear.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n timeout = args.timeout or 30000\n page.wait_for_selector(args.selector, timeout=timeout)\n print(f\"Selector found: {args.selector}\")\n return 0\n except Exception as e:\n print(f\"Error: Timeout waiting for selector '{args.selector}': {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_wait_url(client: BrowserClient, args):\n \"\"\"Wait for URL to match pattern.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n page = client.get_playwright_page(args.name)\n timeout = args.timeout or 30000\n page.wait_for_url(args.url_pattern, timeout=timeout)\n print(f\"URL matched: {page.url}\")\n return 0\n except Exception as e:\n print(f\"Error: Timeout waiting for URL '{args.url_pattern}': {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_wait_load(client: BrowserClient, args):\n \"\"\"Wait for page to fully load.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n timeout = args.timeout or 10000\n result = client.wait_for_page_load(args.name, timeout=timeout)\n\n if result.success:\n print(\"Page loaded successfully\")\n print(f\" Ready state: {result.ready_state}\")\n print(f\" Wait time: {result.wait_time_ms}ms\")\n else:\n print(\"Page load timed out\")\n print(f\" Ready state: {result.ready_state}\")\n print(f\" Pending requests: {result.pending_requests}\")\n print(f\" Wait time: {result.wait_time_ms}ms\")\n return 1\n\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n finally:\n client.disconnect()\n\n\ndef cmd_close(client: BrowserClient, args):\n \"\"\"Close a page.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n if client.close_page(args.name):\n print(f\"Closed page: {args.name}\")\n return 0\n else:\n print(f\"Error: Page '{args.name}' not found\")\n return 1\n\n\ndef cmd_info(client: BrowserClient, args):\n \"\"\"Get page information.\"\"\"\n if not client._check_server():\n print(\"Error: Browser server is not running.\")\n return 1\n\n try:\n info = client.get_page_info(args.name)\n print(f\"Page: {info.name}\")\n print(f\" Title: {info.title}\")\n print(f\" URL: {info.url}\")\n print(f\" Target ID: {info.target_id}\")\n return 0\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Browser automation client for Max\")\n parser.add_argument(\n \"--session-id\",\n help=\"Session ID (defaults to MAX_SESSION_ID env var)\",\n )\n\n subparsers = parser.add_subparsers(dest=\"command\", help=\"Commands\")\n\n # list\n subparsers.add_parser(\"list\", help=\"List all pages in current session\")\n\n # create\n p_create = subparsers.add_parser(\"create\", help=\"Create a new page\")\n p_create.add_argument(\"name\", help=\"Page name\")\n p_create.add_argument(\"url\", nargs=\"?\", help=\"Initial URL\")\n\n # goto\n p_goto = subparsers.add_parser(\"goto\", help=\"Navigate a page to URL\")\n p_goto.add_argument(\"name\", help=\"Page name\")\n p_goto.add_argument(\"url\", help=\"URL to navigate to\")\n\n # screenshot\n p_screenshot = subparsers.add_parser(\"screenshot\", help=\"Take a screenshot\")\n p_screenshot.add_argument(\"name\", help=\"Page name\")\n p_screenshot.add_argument(\"output\", nargs=\"?\", help=\"Output file path\")\n p_screenshot.add_argument(\n \"--full-page\", action=\"store_true\", help=\"Capture full scrollable page\"\n )\n\n # click\n p_click = subparsers.add_parser(\"click\", help=\"Click an element\")\n p_click.add_argument(\"name\", help=\"Page name\")\n p_click.add_argument(\"selector\", help=\"CSS selector\")\n\n # fill\n p_fill = subparsers.add_parser(\"fill\", help=\"Fill an input element\")\n p_fill.add_argument(\"name\", help=\"Page name\")\n p_fill.add_argument(\"selector\", help=\"CSS selector\")\n p_fill.add_argument(\"text\", help=\"Text to fill\")\n\n # hover\n p_hover = subparsers.add_parser(\"hover\", help=\"Hover over an element\")\n p_hover.add_argument(\"name\", help=\"Page name\")\n p_hover.add_argument(\"selector\", help=\"CSS selector\")\n\n # keyboard\n p_keyboard = subparsers.add_parser(\"keyboard\", help=\"Press a keyboard key\")\n p_keyboard.add_argument(\"name\", help=\"Page name\")\n p_keyboard.add_argument(\"key\", help=\"Key to press (e.g., Enter, Tab, Escape)\")\n\n # evaluate\n p_evaluate = subparsers.add_parser(\"evaluate\", help=\"Execute JavaScript\")\n p_evaluate.add_argument(\"name\", help=\"Page name\")\n p_evaluate.add_argument(\"script\", help=\"JavaScript code to execute\")\n\n # text\n p_text = subparsers.add_parser(\"text\", help=\"Get text content of element\")\n p_text.add_argument(\"name\", help=\"Page name\")\n p_text.add_argument(\"selector\", help=\"CSS selector\")\n\n # snapshot\n p_snapshot = subparsers.add_parser(\"snapshot\", help=\"Get AI snapshot (ARIA tree)\")\n p_snapshot.add_argument(\"name\", help=\"Page name\")\n p_snapshot.add_argument(\n \"-i\", \"--interactive\", action=\"store_true\",\n help=\"Only show interactive elements (buttons, links, inputs, etc.)\"\n )\n\n # select-ref\n p_select_ref = subparsers.add_parser(\n \"select-ref\", help=\"Select element by ref and perform action\"\n )\n p_select_ref.add_argument(\"name\", help=\"Page name\")\n p_select_ref.add_argument(\"ref\", help=\"Element ref (e.g., e1, e2)\")\n p_select_ref.add_argument(\"action\", help=\"Action: click, fill, hover, text\")\n p_select_ref.add_argument(\"value\", nargs=\"?\", help=\"Value for fill action\")\n\n # wait-selector\n p_wait_selector = subparsers.add_parser(\"wait-selector\", help=\"Wait for selector\")\n p_wait_selector.add_argument(\"name\", help=\"Page name\")\n p_wait_selector.add_argument(\"selector\", help=\"CSS selector\")\n p_wait_selector.add_argument(\n \"--timeout\", type=int, help=\"Timeout in ms (default: 30000)\"\n )\n\n # wait-url\n p_wait_url = subparsers.add_parser(\"wait-url\", help=\"Wait for URL to match\")\n p_wait_url.add_argument(\"name\", help=\"Page name\")\n p_wait_url.add_argument(\"url_pattern\", help=\"URL pattern (string or regex)\")\n p_wait_url.add_argument(\n \"--timeout\", type=int, help=\"Timeout in ms (default: 30000)\"\n )\n\n # wait-load\n p_wait_load = subparsers.add_parser(\"wait-load\", help=\"Wait for page to fully load\")\n p_wait_load.add_argument(\"name\", help=\"Page name\")\n p_wait_load.add_argument(\n \"--timeout\", type=int, help=\"Timeout in ms (default: 10000)\"\n )\n\n # close\n p_close = subparsers.add_parser(\"close\", help=\"Close a page\")\n p_close.add_argument(\"name\", help=\"Page name\")\n\n # info\n p_info = subparsers.add_parser(\"info\", help=\"Get page information\")\n p_info.add_argument(\"name\", help=\"Page name\")\n\n args = parser.parse_args()\n\n if not args.command:\n parser.print_help()\n return 1\n\n try:\n client = BrowserClient(args.session_id)\n except RuntimeError as e:\n print(f\"Error: {e}\")\n return 1\n\n commands = {\n \"list\": cmd_list,\n \"create\": cmd_create,\n \"goto\": cmd_goto,\n \"screenshot\": cmd_screenshot,\n \"click\": cmd_click,\n \"fill\": cmd_fill,\n \"hover\": cmd_hover,\n \"keyboard\": cmd_keyboard,\n \"evaluate\": cmd_evaluate,\n \"text\": cmd_text,\n \"snapshot\": cmd_snapshot,\n \"select-ref\": cmd_select_ref,\n \"wait-selector\": cmd_wait_selector,\n \"wait-url\": cmd_wait_url,\n \"wait-load\": cmd_wait_load,\n \"close\": cmd_close,\n \"info\": cmd_info,\n }\n\n return commands[args.command](client, args)\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":33672,"content_sha256":"cf46d13d15c77af4a594039904a5c23d9b8b320ab8e7e61b6dfe7c337b9c9791"},{"filename":"refs/scraping.md","content":"# Data Scraping Guide\n\nFor large datasets (followers, posts, search results), **intercept and replay network requests** rather than scrolling and parsing the DOM. This is faster, more reliable, and handles pagination automatically.\n\n## Why Not Scroll?\n\nScrolling is slow, unreliable, and wastes time. APIs return structured data with pagination built in. Always prefer API replay.\n\n## Start Small, Then Scale\n\n**Don't try to automate everything at once.** Work incrementally:\n\n1. **Capture one request** - verify you're intercepting the right endpoint\n2. **Inspect one response** - understand the schema before writing extraction code\n3. **Extract a few items** - make sure your parsing logic works\n4. **Then scale up** - add pagination loop only after the basics work\n\nThis prevents wasting time debugging a complex script when the issue is a simple path like `data.user.timeline` vs `data.user.result.timeline`.\n\n## Step-by-Step Workflow\n\n### 1. Capture Request Details\n\nIntercept requests to find API endpoints and headers:\n\n```bash\ncd skills/browser && uv run python \u003c\u003c'EOF'\nfrom client import BrowserClient\nimport json\n\nclient = BrowserClient()\npage = client.get_playwright_page(\"main\")\n\ncaptured = []\ndef on_request(request):\n url = request.url\n if \"/api/\" in url or \"/graphql/\" in url:\n captured.append({\n \"url\": url,\n \"method\": request.method,\n \"headers\": dict(request.headers),\n })\n print(f\"Captured: {url[:80]}...\")\n\npage.on(\"request\", on_request)\npage.goto(\"https://example.com/profile\")\npage.wait_for_timeout(3000)\n\nwith open(\"tmp/request-details.json\", \"w\") as f:\n json.dump(captured, f, indent=2)\nprint(f\"Saved {len(captured)} requests\")\nEOF\n```\n\n### 2. Capture Response to Understand Schema\n\nSave a response to inspect the data structure:\n\n```bash\ncd skills/browser && uv run python \u003c\u003c'EOF'\nfrom client import BrowserClient\nimport json\n\nclient = BrowserClient()\npage = client.get_playwright_page(\"main\")\n\ndef on_response(response):\n url = response.url\n if \"api/posts\" in url or \"graphql\" in url:\n try:\n data = response.json()\n with open(\"tmp/api-response.json\", \"w\") as f:\n json.dump(data, f, indent=2)\n print(f\"Captured response from: {url[:60]}...\")\n except:\n pass\n\npage.on(\"response\", on_response)\npage.reload()\npage.wait_for_timeout(3000)\nEOF\n```\n\nThen analyze the structure to find:\n\n- Where the data array lives (e.g., `data.posts`, `data.items`)\n- Where pagination cursors are (e.g., `next_cursor`, `has_more`)\n- What fields you need to extract\n\n### 3. Replay API with Pagination\n\nOnce you understand the schema, replay requests directly:\n\n```bash\ncd skills/browser && uv run python \u003c\u003c'EOF'\nfrom client import BrowserClient\nimport json\nimport time\n\nclient = BrowserClient()\npage = client.get_playwright_page(\"main\")\n\n# Load captured headers\nwith open(\"tmp/request-details.json\") as f:\n headers = json.load(f)[0][\"headers\"]\n\nresults = {} # Use dict for deduplication\ncursor = None\nbase_url = \"https://example.com/api/posts\"\n\nwhile True:\n # Build URL with pagination\n url = f\"{base_url}?limit=20\"\n if cursor:\n url += f\"&cursor={cursor}\"\n\n # Fetch in browser context (inherits cookies)\n data = page.evaluate(\"\"\"\n async (params) => {\n const res = await fetch(params.url, { headers: params.headers });\n return res.json();\n }\n \"\"\", {\"url\": url, \"headers\": headers})\n\n # Extract items (deduplicate by id)\n items = data.get(\"posts\", [])\n for item in items:\n if item.get(\"id\") and item[\"id\"] not in results:\n results[item[\"id\"]] = item\n print(f\"Fetched {len(items)} items, total: {len(results)}\")\n\n # Check for more pages\n cursor = data.get(\"next_cursor\")\n if not cursor or not items:\n break\n\n time.sleep(0.5) # Rate limiting\n\n# Save results\nwith open(\"results.json\", \"w\") as f:\n json.dump(list(results.values()), f, indent=2)\nprint(f\"Saved {len(results)} items\")\nEOF\n```\n\n## Key Patterns\n\n| Pattern | Description |\n|---------|-------------|\n| `page.on(\"request\")` | Capture outgoing request URL + headers |\n| `page.on(\"response\")` | Capture response data to understand schema |\n| `page.evaluate(fetch)` | Replay requests in browser context (inherits auth) |\n| `dict` for deduplication | APIs often return overlapping data across pages |\n| Cursor-based pagination | Look for `cursor`, `next_token`, `offset` in responses |\n\n## Tips\n\n- **Rate limiting**: Add 500ms+ delays between requests to avoid blocks\n- **Stop conditions**: Check for empty results, missing cursor, or reaching a date/ID/count threshold\n- **Auth cookies**: Using `page.evaluate` + `fetch()` inherits the browser's cookies\n- **GraphQL APIs**: URL params often include `variables` and `features` JSON objects - capture and reuse them\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4875,"content_sha256":"0c5324d67957b0af19143b315d33880d8af21a6a009d015add340e59a1451e1c"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Browser Automation","type":"text"}]},{"type":"paragraph","content":[{"text":"Browser automation that maintains page state across command executions. Write small, focused commands to accomplish tasks incrementally.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Choosing Your Approach","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local/source-available sites","type":"text","marks":[{"type":"strong"}]},{"text":": Read the source code first to write selectors directly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unknown page layouts","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"snapshot","type":"text","marks":[{"type":"code_inline"}]},{"text":" to discover elements, then ","type":"text"},{"text":"select-ref","type":"text","marks":[{"type":"code_inline"}]},{"text":" to interact","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Visual debugging","type":"text","marks":[{"type":"strong"}]},{"text":": Take ","type":"text"},{"text":"screenshot","type":"text","marks":[{"type":"code_inline"}]},{"text":" to see current page state","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check browser server running (Max must be open)\ncurl -s http://localhost:9222/ | head -1 || echo \"SERVER_NOT_RUNNING\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Running Commands","type":"text"}]},{"type":"paragraph","content":[{"text":"All commands use ","type":"text"},{"text":"client.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the skill directory:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv run skills/browser/client.py \u003ccommand> [arguments]","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ ","type":"text"},{"text":"IMPORTANT","type":"text","marks":[{"type":"strong"}]},{"text":": Always use ","type":"text"},{"text":"uv run client.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"NOT","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"uv run python client.py","type":"text","marks":[{"type":"code_inline"}]},{"text":". The ","type":"text"},{"text":"uv run","type":"text","marks":[{"type":"code_inline"}]},{"text":" command automatically handles Python and dependencies from ","type":"text"},{"text":"pyproject.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":". Adding ","type":"text"},{"text":"python","type":"text","marks":[{"type":"code_inline"}]},{"text":" breaks dependency resolution.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow Loop","type":"text"}]},{"type":"paragraph","content":[{"text":"Follow this pattern for complex tasks:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run a command","type":"text","marks":[{"type":"strong"}]},{"text":" to perform one action","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Observe","type":"text","marks":[{"type":"strong"}]},{"text":" the output","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Evaluate","type":"text","marks":[{"type":"strong"}]},{"text":" - did it work? What's the current state?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Decide","type":"text","marks":[{"type":"strong"}]},{"text":" - is the task complete or do we need another command?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Repeat","type":"text","marks":[{"type":"strong"}]},{"text":" until task is done","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"No TypeScript in Browser Context","type":"text"}]},{"type":"paragraph","content":[{"text":"Code passed to ","type":"text"},{"text":"page.evaluate()","type":"text","marks":[{"type":"code_inline"}]},{"text":" runs in the browser, which doesn't understand TypeScript:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// ✅ Correct: plain JavaScript\nconst text = await page.evaluate(() => {\n return document.body.innerText;\n});\n\n// ❌ Wrong: TypeScript syntax will fail at runtime\nconst text = await page.evaluate(() => {\n const el: HTMLElement = document.body; // Type annotation breaks in browser!\n return el.innerText;\n});","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Waiting","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv run skills/browser/client.py wait-load main # After navigation\nuv run skills/browser/client.py wait-selector main \".results\" # For specific elements\nuv run skills/browser/client.py wait-url main \"**/success\" # For specific URL","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scraping Data","type":"text"}]},{"type":"paragraph","content":[{"text":"For large datasets, ","type":"text"},{"text":"intercept and replay API requests","type":"text","marks":[{"type":"strong"}]},{"text":" rather than scrolling DOM. See ","type":"text"},{"text":"refs/scraping.md","type":"text","marks":[{"type":"link","attrs":{"href":"refs/scraping.md","title":null}}]},{"text":" for the complete guide covering request capture, schema discovery, and paginated API replay.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Inspecting Page State","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Screenshots","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv run skills/browser/client.py screenshot main screenshot.png\nuv run skills/browser/client.py screenshot main full.png --full-page # Capture entire scrollable page","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"ARIA Snapshot (Element Discovery)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"snapshot","type":"text","marks":[{"type":"code_inline"}]},{"text":" to discover page elements. Returns YAML-formatted accessibility tree:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"- banner:\n - link \"Hacker News\" [ref=e1]\n - navigation:\n - link \"new\" [ref=e2]\n- main:\n - heading \"Products\" [ref=e3] [level=1]\n - list:\n - listitem:\n - link \"Article Title\" [ref=e4]\n - button \"Add to Cart\" [ref=e5]\n - listitem:\n - link \"Another Article\" [ref=e6]\n - button \"Add to Cart\" [ref=e7] [nth=1]\n- contentinfo:\n - textbox [ref=e8]\n - /placeholder: \"Search\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Interpreting refs:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"[ref=eN]","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Element reference for interaction","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"[nth=N]","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Nth duplicate element with same role+name (0-indexed, first one omitted)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"[checked]","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"[disabled]","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"[expanded]","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Element states","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"[level=N]","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Heading level","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"/url:","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/placeholder:","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Element properties","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Interacting with refs:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Get snapshot to find refs\nuv run skills/browser/client.py snapshot main\n\n# Only show interactive elements (buttons, links, inputs, etc.)\nuv run skills/browser/client.py snapshot main -i\n\n# Use ref to interact\nuv run skills/browser/client.py select-ref main e2 click\nuv run skills/browser/client.py select-ref main e7 click # Click second \"Add to Cart\"\nuv run skills/browser/client.py select-ref main e8 fill \"search term\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Recovery","type":"text"}]},{"type":"paragraph","content":[{"text":"Page state persists after failures. Debug with:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Take screenshot to see current state\nuv run skills/browser/client.py screenshot main debug.png\n\n# Get page info\nuv run skills/browser/client.py info main\n\n# Get text content\nuv run skills/browser/client.py text main \"body\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Command Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Page Management","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv run skills/browser/client.py list # List all pages\nuv run skills/browser/client.py create main # Create a new page\nuv run skills/browser/client.py create main \"https://...\" # Create and navigate\nuv run skills/browser/client.py goto main \"https://...\" # Navigate existing page\nuv run skills/browser/client.py close main # Close a page\nuv run skills/browser/client.py info main # Get page URL and title","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Element Interaction","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv run skills/browser/client.py click main \"button.submit\" # Click element\nuv run skills/browser/client.py fill main \"input#email\" \"[email protected]\" # Fill input\nuv run skills/browser/client.py hover main \".dropdown\" # Hover over element\nuv run skills/browser/client.py keyboard main \"Enter\" # Press key\nuv run skills/browser/client.py text main \"h1\" # Get element text","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"JavaScript Execution","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"uv run skills/browser/client.py evaluate main \"document.title\"\nuv run skills/browser/client.py evaluate main \"document.querySelectorAll('.item').length\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Python Script (Advanced)","type":"text"}]},{"type":"paragraph","content":[{"text":"For complex tasks requiring loops or ","type":"text"},{"text":"page.on()","type":"text","marks":[{"type":"code_inline"}]},{"text":" event handlers, use heredoc with ","type":"text"},{"text":"BrowserClient","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cd skills/browser && uv run python \u003c\u003c'EOF'\nfrom client import BrowserClient\n\nclient = BrowserClient()\npage = client.get_playwright_page(\"main\")\n\n# Full Playwright API available\npage.goto(\"https://example.com\")\npage.click(\"button\")\n\n# Event handlers for request interception\npage.on(\"response\", lambda r: print(r.url))\nEOF","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"page","type":"text","marks":[{"type":"code_inline"}]},{"text":" object is a standard Playwright Page.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"browser","author":"@skillopedia","source":{"stars":0,"repo_name":"vibe-ops-plugin","origin_url":"https://github.com/infquest/vibe-ops-plugin/blob/HEAD/skills/browser/SKILL.md","repo_owner":"infquest","body_sha256":"77693191f67458a3698a2ca6fcc782ce5ca77cb9587d61650bacacb5b2726c57","cluster_key":"ad1089548f67e04ba6377003eedae5f8fd422857fea2fc0e03cd0229980b2b35","clean_bundle":{"format":"clean-skill-bundle-v1","source":"infquest/vibe-ops-plugin/skills/browser/SKILL.md","attachments":[{"id":"929f0225-e6c1-59c7-98d7-5464b3f3a5c1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/929f0225-e6c1-59c7-98d7-5464b3f3a5c1/attachment.py","path":"client.py","size":33672,"sha256":"cf46d13d15c77af4a594039904a5c23d9b8b320ab8e7e61b6dfe7c337b9c9791","contentType":"text/x-python; charset=utf-8"},{"id":"731fde8f-db5b-589e-853a-85272dc7baf5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/731fde8f-db5b-589e-853a-85272dc7baf5/attachment.md","path":"refs/scraping.md","size":4875,"sha256":"0c5324d67957b0af19143b315d33880d8af21a6a009d015add340e59a1451e1c","contentType":"text/markdown; charset=utf-8"},{"id":"3e0d8bb6-d3ec-5027-b6dc-eeb40a3b0944","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3e0d8bb6-d3ec-5027-b6dc-eeb40a3b0944/attachment.js","path":"snapshot.js","size":5657,"sha256":"4de1940b0a31011bc4df7dbec885e479858034cf55b1fdb0ed79ef32756630a2","contentType":"application/javascript; charset=utf-8"}],"bundle_sha256":"e2a15a4a35da6340ffe74e690419924c9b7bfaaaa86c76cde5ee95a34d19f770","attachment_count":3,"text_attachments":3,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/browser/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"browser-automation-scraping","category_label":"Browser"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"browser-automation-scraping","import_tag":"clean-skills-v1","description":"Browser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include \"go to [url]\", \"click on\", \"fill out the form\", \"take a screenshot\", \"scrape\", \"automate\", \"test the website\", \"log into\", or any browser interaction request."}},"renderedAt":1782987917726}

Browser Automation Browser automation that maintains page state across command executions. Write small, focused commands to accomplish tasks incrementally. Choosing Your Approach - Local/source-available sites : Read the source code first to write selectors directly - Unknown page layouts : Use to discover elements, then to interact - Visual debugging : Take to see current page state Prerequisites Running Commands All commands use from the skill directory: ⚠️ IMPORTANT : Always use , NOT . The command automatically handles Python and dependencies from . Adding breaks dependency resolution. Wo…