{
  "schemaVersion": "1.0",
  "item": {
    "slug": "pets-browser",
    "name": "Pets Browser",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/ekenesbek/pets-browser",
    "canonicalUrl": "https://clawhub.ai/ekenesbek/pets-browser",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/pets-browser",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=pets-browser",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "README.md",
      "SKILL.md",
      "package.json",
      "scripts/browser-daemon.js",
      "scripts/browser.js",
      "scripts/postinstall.js"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/pets-browser"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/pets-browser",
    "agentPageUrl": "https://openagent3.xyz/skills/pets-browser/agent",
    "manifestUrl": "https://openagent3.xyz/skills/pets-browser/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/pets-browser/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Clawnet",
        "body": "Stealth browser skill for AI agents. Installs and configures Chromium with anti-detection, residential proxies (10+ countries), CAPTCHA solving, and human-like behavior simulation."
      },
      {
        "title": "When to use",
        "body": "Use this skill whenever the agent needs to:\n\nBrowse websites, scrape data, or interact with web pages\nFill forms, click buttons, navigate multi-step flows\nBypass Cloudflare, DataDome, PerimeterX, or other bot detection\nSolve CAPTCHAs (reCAPTCHA v2/v3, hCaptcha, Turnstile)\nAppear as a real user from a specific country\nInteract with Shadow DOM elements or rich text editors\n\nAlways use this instead of raw Playwright/Puppeteer. This skill handles anti-detection automatically."
      },
      {
        "title": "Observation — how to read the page",
        "body": "ALWAYS use snapshotAI() instead of page.textContent() or evaluate(). It returns a structured accessibility tree with embedded [ref=eN] annotations. You can then click/fill/type by ref — no CSS selectors needed."
      },
      {
        "title": "Reading the page (preferred: snapshotAI + refs)",
        "body": "// BAD — dumps ALL text, 50-100K tokens, no structure, no refs\nconst text = await page.textContent('body');\n\n// BAD — brittle regex on raw DOM, breaks when HTML changes\nawait page.evaluate(() => document.querySelector('button').click());\n\n// GOOD — AI-optimized snapshot with clickable refs\nconst { snapshot } = await browser.snapshotAI();\n// Returns:\n//   - navigation \"Main\" [ref=e1]:\n//     - link \"Home\" [ref=e2]\n//   - heading \"Welcome\" [ref=e3]\n//   - textbox \"Email\" [ref=e4]\n//   - textbox \"Password\" [ref=e5]\n//   - button \"Sign in\" [ref=e6]\n\n// Then interact by ref:\nawait browser.fillRef('e4', 'user@example.com');\nawait browser.fillRef('e5', 'secret');\nawait browser.clickRef('e6');"
      },
      {
        "title": "Alternative: snapshot() (YAML without refs)",
        "body": "// Compact accessibility tree without refs — use when you don't need to interact\nconst tree = await browser.snapshot();\nconst interactive = await browser.snapshot({ interactiveOnly: true });\nconst formTree = await browser.snapshot({ selector: 'form' });"
      },
      {
        "title": "Observation workflow",
        "body": "Before every action, follow this sequence:\n\nDismiss overlays & accept cookies — after every page.goto(), call await browser.dismissOverlays() to auto-close cookie banners, consent popups, and notification prompts. If a cookie banner or consent dialog is still visible in the snapshot, click \"Accept\" / \"Accept all\" / \"Принять\" before doing anything else. Never skip this step — cookie overlays block interaction with page elements underneath.\nSnapshot — const { snapshot } = await browser.snapshotAI() to see the page with refs\nRead text — await browser.extractText() if you need clean readable text (menus, prices, articles)\nVisual check — await browser.takeScreenshot() only if you need to see colors, layout, maps, or images\nAct by ref — await browser.clickRef('e4'), await browser.fillRef('e5', 'text') etc.\nVerify — await browser.snapshotAI() again to confirm the action worked\nBatch — use batchActions() for multi-step flows"
      },
      {
        "title": "Targeting elements — use refs from snapshotAI()",
        "body": "ALWAYS use refs from snapshotAI() output. NEVER use CSS selectors or evaluate() with regex.\n\n// BAD — brittle CSS selectors that break when HTML changes\nawait page.click('#login_field');\nawait page.fill('input[name=\"email\"]', 'user@example.com');\n\n// BAD — regex on raw DOM, blind guessing\nawait page.evaluate(() => document.querySelectorAll('button').find(b => /sign in/i.test(b.innerText))?.click());\n\n// GOOD — ref-based from snapshotAI() output\nconst { snapshot } = await browser.snapshotAI();\n// snapshot shows: textbox \"Email\" [ref=e4], button \"Sign in\" [ref=e6]\nawait browser.fillRef('e4', 'user@example.com');\nawait browser.clickRef('e6');\n\n// ALSO GOOD — semantic locators (when you know the label)\nawait page.getByLabel('Email').fill('user@example.com');\nawait page.getByLabel('Password').fill('secret');\nawait page.getByRole('button', { name: 'Sign in' }).click();\n\n// Also available:\nawait page.getByPlaceholder('Search...').fill('query');\nawait page.getByText('Welcome back').isVisible();\nawait page.getByRole('link', { name: 'Home' }).click();\nawait page.getByRole('checkbox', { name: 'Remember me' }).check();\n\nWhen you see - textbox \"Email\" in the snapshot, use page.getByRole('textbox', { name: 'Email' }).\nWhen you see - button \"Submit\", use page.getByRole('button', { name: 'Submit' })."
      },
      {
        "title": "When to fall back to CSS selectors",
        "body": "Only use CSS selectors when:\n\nThe element has no accessible name or role (rare in modern sites)\nYou need to target by data-testid or other test attributes\nShadow DOM elements not reachable by semantic locators (use shadowFill/shadowClickButton)"
      },
      {
        "title": "Multi-tab — parallel tasks",
        "body": "Use multiple tabs only when the user needs different websites open at the same time. One tab per website/service — not one tab per action."
      },
      {
        "title": "When to open a new tab vs reuse the current one",
        "body": "New tab — different website or service that the user may want to come back to:\n\n\"Order a taxi AND book a restaurant\" → 2 tabs (Uber + OpenTable)\n\"Compare prices on Amazon and eBay\" → 2 tabs\n\nSame tab — same website, sequential actions:\n\n\"Order a taxi for me, then for my friend\" → 1 tab (Uber), two orders one after another\n\"Book a table for Saturday, then book another for Sunday\" → 1 tab (OpenTable), two bookings\n\"Search for Air Jordans, then search for Nike Dunks\" → 1 tab (Nike), two searches\n\nThink like a human: you wouldn't open a second Uber tab to order a second ride. You'd finish the first ride, then start the second one in the same tab."
      },
      {
        "title": "Opening tabs",
        "body": "launchBrowser() gives you the first tab. Open more with newTab():\n\nconst { launchBrowser } = require('clawnet/scripts/browser');\n\n// First tab — comes from launchBrowser()\nconst taxi = await launchBrowser({ country: 'us', mobile: false });\nawait taxi.page.goto('https://uber.com');\n\n// Open more tabs — each returns its own result object\nconst resto = await taxi.newTab({ url: 'https://opentable.com', label: 'restaurant' });\nconst shop  = await taxi.newTab({ url: 'https://nike.com', label: 'sneakers' });\n\nEach tab object (taxi, resto, shop) has the full API: page.goto(), snapshotAI(), clickRef(), fillRef(), takeScreenshot(), etc. — all scoped to that tab."
      },
      {
        "title": "Working with tabs",
        "body": "Rule: keep a named variable per tab. This is how you \"remember\" which tab is which.\n\n// Work on the taxi tab\nawait taxi.page.goto('https://uber.com/ride');\nconst { snapshot } = await taxi.snapshotAI();\nawait taxi.fillRef('e5', '123 Main St');        // pickup address\nawait taxi.clickRef('e9');                       // \"Request ride\"\n\n// Switch to the restaurant tab — just use the variable\nconst { snapshot: restoSnap } = await resto.snapshotAI();\nawait resto.fillRef('e3', '2 guests');\nawait resto.fillRef('e4', 'March 8, 7pm');\nawait resto.clickRef('e7');                      // \"Find a table\"\n\n// Switch to sneakers\nawait shop.snapshotAI();\nawait shop.clickRef('e12');                      // \"Air Jordan 1\"\n\nNo explicit \"switch tab\" call needed — just use the right variable. Each variable is bound to its tab."
      },
      {
        "title": "Checking all tabs",
        "body": "const { tabs } = await taxi.listTabs();\n// [\n//   { tabId: \"t_a1b2c3\", url: \"https://uber.com/ride\", label: \"\", active: false },\n//   { tabId: \"t_d4e5f6\", url: \"https://opentable.com/...\", label: \"restaurant\", active: false },\n//   { tabId: \"t_g7h8i9\", url: \"https://nike.com/...\", label: \"sneakers\", active: true },\n// ]"
      },
      {
        "title": "Going back to a tab",
        "body": "If you lost the variable (e.g., across script invocations), use switchTab(tabId):\n\n// From listTabs() you know the tabId\nconst uberTab = await taxi.switchTab('t_a1b2c3');\nawait uberTab.snapshotAI();  // see what's on the Uber tab now"
      },
      {
        "title": "Closing a tab",
        "body": "await shop.closeTab();  // close the sneakers tab\n// shop variable is now stale — don't use it"
      },
      {
        "title": "Multi-tab workflow pattern",
        "body": "When the user gives you multiple parallel tasks:\n\nPlan — identify separate tasks (taxi, restaurant, sneakers)\nOpen tabs — one newTab() per task, save each to a named variable\nWork round-robin — do a chunk of work on each tab, take screenshots\nReport — show the user screenshots from each tab so they see all progress\nGo back — when the user says \"cancel the taxi\" or \"check the menu\", switch to the right tab variable"
      },
      {
        "title": "Example: user says \"Order a taxi, book a table, and find sneakers\"",
        "body": "// Phase 1: open all tabs\nconst taxi  = await launchBrowser({ country: 'us', mobile: false });\nconst resto = await taxi.newTab({ url: 'https://opentable.com' });\nconst shop  = await taxi.newTab({ url: 'https://nike.com' });\n\n// Phase 2: start each task\nawait taxi.page.goto('https://uber.com');\nawait taxi.fillRef('e3', 'Airport');         // destination\nconst taxiSS = await taxi.takeScreenshot();\n\nawait resto.fillRef('e2', 'Italian');        // cuisine search\nawait resto.clickRef('e5');                  // search\nconst restoSS = await resto.takeScreenshot();\n\nawait shop.fillRef('e1', 'Air Jordan');      // search\nawait shop.clickRef('e3');                   // search button\nconst shopSS = await shop.takeScreenshot();\n\n// Phase 3: report to user (ALL tabs' screenshots)\n// \"Here's what I've set up: [taxi screenshot] [restaurant screenshot] [shop screenshot]\"\n\n// Phase 4: user says \"cancel the taxi, check restaurant prices\"\nawait taxi.clickRef('e15');                  // \"Cancel\" button\nconst cancelSS = await taxi.takeScreenshot();\n\nconst { text } = await resto.extractText();  // read menu prices\nconst pricesSS = await resto.takeScreenshot();"
      },
      {
        "title": "Key rules",
        "body": "One tab per website/service — not one tab per action. Sequential tasks on the same site happen in one tab\nNew tab only for a different site that the user may want to come back to\nOne variable per tab — don't reuse variables, name them by purpose\nTabs share cookies — login on one tab is visible on all tabs (same browser context)\nScreenshots from each tab — always show the user what's happening on each tab\nDon't open too many tabs — 2-4 is practical, more gets confusing for both you and the user\nTabs survive between script runs — the daemon keeps them alive. Use listTabs() to rediscover them"
      },
      {
        "title": "Screenshot rules",
        "body": "ALWAYS attach a screenshot when communicating with the user. The user cannot see the browser — you are their eyes. Every message to the user MUST include a screenshot. No exceptions."
      },
      {
        "title": "When to take screenshots",
        "body": "Every message you send to the user must have a screenshot attached. Specifically:\n\nBefore asking for confirmation — \"Book this table?\" + screenshot of the filled form. The user must SEE what they are confirming.\nWhen reporting an error — \"No slots available\" + screenshot proving the result. Without a screenshot, the user has no reason to trust you.\nWhen unable to complete an action — \"Authorization failed\" + screenshot showing what happened.\nAfter every key step — filled form, selected date, entered address, etc.\nWhen completing the task (MANDATORY) — \"Done! Order placed\" + screenshot of the final result/confirmation page. The user must see proof that the action was completed."
      },
      {
        "title": "How to take screenshots",
        "body": "Use the built-in helpers returned by launchBrowser():\n\nconst { page, takeScreenshot, screenshotAndReport } = await launchBrowser();\n\n// Option 1: just the base64 screenshot\nconst base64 = await takeScreenshot();\n\n// Option 2: screenshot + message bundled together\nconst report = await screenshotAndReport(\"Form filled. Confirm booking?\");\n// → { message: \"Form filled...\", screenshot: \"iVBOR...\", mimeType: \"image/png\" }\n\nOr directly via Playwright:\n\nconst screenshot = await page.screenshot({ type: 'png' });\nconst base64 = screenshot.toString('base64');"
      },
      {
        "title": "Rules",
        "body": "Never tell the user \"the form is empty\", \"widget is disabled\", or \"no results\" without a screenshot as proof.\nNever ask for confirmation without showing the current state of the page.\nNever say \"Done!\" without a screenshot of the final result.\nThe user must see what you see. Always."
      },
      {
        "title": "Installation",
        "body": "clawhub install clawnet\n\nThis installs Chromium via Playwright automatically. No separate browser download needed.\n\nImportant (Linux/Docker): If Chromium fails to launch with missing library errors (e.g. libnspr4.so), install system dependencies:\n\nnpx playwright install-deps chromium\n# or manually:\napt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 libpangocairo-1.0-0 libcairo2 libasound2"
      },
      {
        "title": "First-Run Onboarding",
        "body": "launchBrowser() is zero-config — it auto-registers a new agent and saves credentials if none exist.\n\nHowever, on first use the agent SHOULD check if the user already has credentials to avoid creating duplicate agents:"
      },
      {
        "title": "Step 1: Check for existing credentials",
        "body": "const fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst CREDS_FILE = path.join(os.homedir(), '.clawnet', 'agent-credentials.json');\nlet hasCreds = false;\ntry { hasCreds = Boolean(JSON.parse(fs.readFileSync(CREDS_FILE, 'utf-8')).agentId); } catch {}\n\nIf credentials exist on disk → skip to Step 3 (just launch)."
      },
      {
        "title": "Step 2: Ask the user",
        "body": "Ask the user: \"Do you have existing Clawnet credentials (agentId:agentSecret), or should I create new ones?\"\n\nIf user provides credentials → save them to CREDS_FILE:\n\nconst [agentId, agentSecret] = userInput.split(':');\nfs.mkdirSync(path.dirname(CREDS_FILE), { recursive: true, mode: 0o700 });\nfs.writeFileSync(CREDS_FILE, JSON.stringify({ agentId, agentSecret, createdAt: new Date().toISOString() }, null, 2), { mode: 0o600 });\n\nIf user says \"new\" or \"no\" → skip to Step 3. launchBrowser() will auto-register."
      },
      {
        "title": "Step 3: Launch",
        "body": "const { launchBrowser } = require('clawnet/scripts/browser');\nconst { browser, page } = await launchBrowser({ country: 'us' });\n\nThat's it. No env vars needed. launchBrowser() will:\n\nFind credentials on disk (or auto-register a new agent with the API)\nFetch managed proxy + CAPTCHA keys\nLaunch stealth Chromium with residential proxy\n\nAfter first launch, show credentials and support contacts to the user:\n\nYour Clawnet credentials:\n  agentId: <agentId>\n  agentSecret: <agentSecret>\n\nSave these for future use or transfer to another agent.\n\nIf you run into any issues: [Discord](https://discord.com/invite/wtA85mJM) | [Telegram](https://t.me/+nPjZFg8PJc4zZTIy)"
      },
      {
        "title": "Trial model",
        "body": "On the first launchBrowser() call, a 2-hour free trial starts. After expiry, getCredentials() returns upgradeUrl. Show it to the user:\n\nTrial expired. Subscribe to continue: <upgradeUrl>\nOr set your own proxy/CAPTCHA keys (BYO mode)."
      },
      {
        "title": "After payment",
        "body": "Subscription activates automatically within seconds (webhook). No manual steps needed — the next launchBrowser() call will receive managed credentials."
      },
      {
        "title": "Transfer / Recovery / Rotation",
        "body": "To transfer/recover on another agent, provide the same agentId + agentSecret during install.\nBackend rule: one subscriptionId can be linked to only one agentId at a time.\n\nTo rotate a compromised secret, keep the same agentId and issue a new agentSecret (authorized by current secret or recovery code). Old secret is invalidated immediately."
      },
      {
        "title": "Cancel subscription",
        "body": "If the user asks to cancel their subscription, call the cancel endpoint:\n\nconst creds = JSON.parse(fs.readFileSync(CREDS_FILE, 'utf-8'));\nconst token = `CN1.${creds.agentId}.${creds.agentSecret}`;\nconst resp = await fetch(`${apiUrl}/cancel-subscription`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${token}` },\n});\nconst result = await resp.json();\n// { canceled: true, accessUntil: \"2026-04-02T00:00:00Z\", message: \"...\" }\n\nShow the result to the user:\n\nYour subscription has been canceled. Access remains until <accessUntil>.\nIf you change your mind, you can resubscribe anytime.\n\nNeed help? [Discord](https://discord.com/invite/wtA85mJM) | [Telegram](https://t.me/+nPjZFg8PJc4zZTIy)"
      },
      {
        "title": "Option A: Managed credentials (default, recommended)",
        "body": "The onboarding flow above sets everything up automatically. Environment variables used:\n\nCN_API_URL=https://api.clawpets.io/clawnet/v1\n# Set automatically by onboarding, or manually:\nCN_AGENT_TOKEN=CN1.<agentId>.<agentSecret>\n# Or separately:\nCN_AGENT_ID=<agent-uuid>\nCN_AGENT_SECRET=<agent-secret>\n\nThe skill will automatically fetch Decodo proxy credentials and 2captcha API key on launch."
      },
      {
        "title": "Option B: BYO (Bring Your Own)",
        "body": "Set proxy and CAPTCHA credentials directly:\n\nCN_PROXY_PROVIDER=decodo          # decodo | brightdata | iproyal | nodemaven\nCN_PROXY_USER=your-proxy-user\nCN_PROXY_PASS=your-proxy-pass\nCN_PROXY_COUNTRY=us               # us, gb, de, nl, jp, fr, ca, au, sg, ro, br, in\nTWOCAPTCHA_KEY=your-2captcha-key"
      },
      {
        "title": "Option C: No proxy (local testing)",
        "body": "CN_NO_PROXY=1"
      },
      {
        "title": "Browser lifecycle",
        "body": "DO NOT close the browser between steps. The browser persists automatically via a background daemon. Just call launchBrowser() at the start of each script — it reconnects to the existing browser with all your tabs, cookies, and login sessions intact.\n\n// Script 1: agent logs into a site\nconst b = await launchBrowser({ country: 'us' });\nawait b.page.goto('https://example.com/login');\nawait b.fillRef('e2', 'user@example.com');\nawait b.clickRef('e5');\n// Script ends — browser stays alive\n\n// Script 2 (later): agent continues where it left off\nconst b = await launchBrowser({ country: 'us' });\n// Same browser, same tab, same cookies — still logged in\nawait b.snapshotAI();  // sees the logged-in page"
      },
      {
        "title": "What NOT to do",
        "body": "// BAD — kills the browser, loses all state\nawait browser.close();\nawait closeBrowser();\n\n// BAD — opening a new browser when you already have one\nconst b1 = await launchBrowser();\n// ... do some work ...\nconst b2 = await launchBrowser();  // this REUSES b1, doesn't create a new browser"
      },
      {
        "title": "When to actually close",
        "body": "Only close the browser when the user explicitly says they're done with ALL browser tasks:\n\n\"Close the browser\"\n\"I'm done, clean up\"\n\"Shut everything down\"\n\nOtherwise, leave it running. The daemon auto-shuts down after 5 minutes of inactivity anyway."
      },
      {
        "title": "Quick start",
        "body": "const { launchBrowser, solveCaptcha } = require('clawnet/scripts/browser');\n\n// Launch stealth browser with US residential proxy\nconst b = await launchBrowser({\n  country: 'us',\n  mobile: false,    // Desktop Chrome (true = iPhone 15 Pro)\n  headless: true,\n});\n\n// Browse normally — anti-detection is automatic\nawait b.page.goto('https://example.com');\n\n// Read the page\nconst { snapshot } = await b.snapshotAI();\n\n// Interact by ref\nawait b.fillRef('e4', 'user@example.com');\nawait b.clickRef('e6');\n\n// Solve CAPTCHA if present\nconst result = await b.solveCaptcha({ verbose: true });\n\n// Take a screenshot for the user\nconst ss = await b.takeScreenshot();\n\n// DO NOT close — browser stays alive for the next step"
      },
      {
        "title": "importCredentials(agentId, agentSecret)",
        "body": "Save user-provided agent credentials to disk. Use when transferring an existing account to a new machine.\n\nconst { importCredentials } = require('clawnet/scripts/browser');\nconst result = importCredentials('your-uuid', 'your-secret');\n// { ok: true, agentId: 'your-uuid' }"
      },
      {
        "title": "launchBrowser(opts)",
        "body": "Launch a stealth Chromium browser with residential proxy.\n\nOptionTypeDefaultDescriptioncountrystring'us'Proxy country: us, gb, de, nl, jp, fr, ca, au, sg, ro, br, inmobilebooleantruetrue = iPhone 15 Pro, false = Desktop ChromeheadlessbooleantrueRun headlessuseProxybooleantrueEnable residential proxysessionstringrandomSticky session ID (same IP across requests)profilestring'default'Persistent profile name (null = ephemeral)reusebooleantrueReuse running browser for this profile (new tab, same process)logLevelstring'actions''off' | 'actions' | 'verbose'. Env: CN_LOG_LEVELtaskstringnullUser's prompt / task description. Recorded in the session log for context.\n\nReturns: { browser, ctx, page, logger, tabId, newTab, listTabs, closeTab, switchTab, humanClick, humanMouseMove, humanType, humanScroll, humanRead, solveCaptcha, takeScreenshot, screenshotAndReport, snapshot, snapshotAI, dumpInteractiveElements, clickRef, fillRef, typeRef, selectRef, hoverRef, extractText, getCookies, setCookies, clearCookies, batchActions, sleep, rand, getSessionLog }"
      },
      {
        "title": "solveCaptcha(page, opts)",
        "body": "Auto-detect and solve CAPTCHA on the current page. Supports reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile.\n\nOptionTypeDefaultDescriptionapiKeystringenv TWOCAPTCHA_KEY2captcha API keytimeoutnumber120000Max wait time in msverbosebooleanfalseLog progress\n\nReturns: { token, type, sitekey }"
      },
      {
        "title": "takeScreenshot(page, opts)",
        "body": "Take a screenshot and return it as a base64-encoded PNG string.\n\nOptionTypeDefaultDescriptionfullPagebooleanfalseCapture the full scrollable page\n\nReturns: string (base64 PNG)"
      },
      {
        "title": "screenshotAndReport(page, message, opts)",
        "body": "Take a screenshot and pair it with a message. Returns an object ready to attach to an LLM response.\n\nOptionTypeDefaultDescriptionfullPagebooleanfalseCapture the full scrollable page\n\nReturns: { message, screenshot, mimeType } — screenshot is base64 PNG"
      },
      {
        "title": "snapshot(page, opts) / snapshot(opts) (from launchBrowser return)",
        "body": "Capture a compact accessibility tree of the page. Returns YAML string.\nUse this instead of page.textContent(). See \"Observation\" section above.\n\nOptionTypeDefaultDescriptionselectorstring'body'CSS selector to scope the snapshotinteractiveOnlybooleanfalseKeep only interactive elements (buttons, inputs, links)maxLengthnumber20000Truncate output to N characterstimeoutnumber5000Playwright timeout in ms\n\nReturns: string (YAML accessibility tree)"
      },
      {
        "title": "snapshotAI(opts) — AI-optimized snapshot with refs ⭐ PREFERRED",
        "body": "Returns a structured accessibility tree with embedded [ref=eN] annotations. Use this as the primary way to read pages.\n\nconst { snapshot, refs, truncated } = await browser.snapshotAI();\n// snapshot: \"- heading \\\"Welcome\\\" [ref=e1]\\n- textbox \\\"Email\\\" [ref=e2]\\n- button \\\"Sign in\\\" [ref=e3]\"\n// refs: { e1: true, e2: true, e3: true }\n\nOptionTypeDefaultDescriptionmaxCharsnumber20000Truncate snapshot to N characterstimeoutnumber5000Playwright timeout in ms\n\nReturns: { snapshot: string, refs: Object, truncated?: boolean }"
      },
      {
        "title": "clickRef(ref, opts) — Click element by ref",
        "body": "await browser.clickRef('e3');                          // left click\nawait browser.clickRef('e3', { doubleClick: true });   // double click"
      },
      {
        "title": "fillRef(ref, value, opts) — Fill input by ref",
        "body": "await browser.fillRef('e2', 'user@example.com');"
      },
      {
        "title": "typeRef(ref, text, opts) — Type text by ref",
        "body": "await browser.typeRef('e2', 'hello');                          // instant fill\nawait browser.typeRef('e2', 'hello', { slowly: true });        // human-like typing\nawait browser.typeRef('e2', 'hello', { submit: true });        // type + Enter"
      },
      {
        "title": "selectRef(ref, value, opts) — Select option by ref",
        "body": "await browser.selectRef('e5', 'US');"
      },
      {
        "title": "hoverRef(ref, opts) — Hover element by ref",
        "body": "await browser.hoverRef('e1');  // reveal tooltip/dropdown"
      },
      {
        "title": "newTab(opts) — Open a new tab",
        "body": "Opens a new browser tab and returns a new result object scoped to that tab. All methods on the returned object (page.goto, snapshotAI, clickRef, etc.) operate on the new tab.\n\nOptionTypeDefaultDescriptionurlstring-Navigate to this URL immediatelylabelstring''Human-readable label for the tab\n\nconst tab2 = await browser.newTab({ url: 'https://opentable.com', label: 'restaurant' });\nawait tab2.snapshotAI();  // snapshot of opentable.com"
      },
      {
        "title": "listTabs() — List all open tabs",
        "body": "Returns all open tabs with their IDs, URLs, labels, and active status.\n\nconst { tabs } = await browser.listTabs();\n// [{ tabId: \"t_abc\", url: \"https://...\", label: \"restaurant\", active: true, createdAt: \"...\" }]"
      },
      {
        "title": "closeTab(tabId?) — Close a tab",
        "body": "Closes the specified tab (or the current tab if no tabId given).\n\nawait tab2.closeTab();           // close this tab\nawait browser.closeTab('t_abc'); // close by ID"
      },
      {
        "title": "switchTab(tabId) — Switch to a tab",
        "body": "Returns a new result object scoped to the specified tab. Use when you need to return to a tab whose variable you lost (e.g., across script invocations).\n\nconst { tabs } = await browser.listTabs();\nconst uberTab = await browser.switchTab(tabs[0].tabId);\nawait uberTab.snapshotAI();"
      },
      {
        "title": "extractText(opts) (from launchBrowser return) / extractText(page, opts)",
        "body": "Extract clean readable text from the page, stripping navigation, ads, modals, and noise. Use when you need to READ the page content (menus, prices, articles) rather than interact with UI elements.\n\nOptionTypeDefaultDescriptionmodestring'readability''readability' strips noise, 'raw' returns body.innerTextmaxCharsnumberunlimitedTruncate text to N characters\n\nReturns: { url, title, text, truncated }\n\n// Read a restaurant menu\nconst { text } = await extractText({ mode: 'readability' });\n// → \"Pizza Menu\\n\\nMargherita\\nClassic pizza with mozzarella...\\nFrom 399 ₽\\n\\n...\"\n\n// Raw mode for simple pages\nconst { text: raw } = await extractText({ mode: 'raw', maxChars: 5000 });\n\nWhen to use extractText() vs snapshot():\n\nextractText() — reading text content (menus, prices, articles, descriptions)\nsnapshot() — understanding page structure and finding interactive elements (buttons, inputs, links)"
      },
      {
        "title": "getCookies(urls?) / setCookies(cookies) / clearCookies()",
        "body": "Manage browser cookies. Use for session persistence, login state checks, and cookie transfer between tasks.\n\n// Check if logged in\nconst cookies = await getCookies('https://example.com');\nconst hasAuth = cookies.some(c => c.name === 'session_id');\n\n// Set cookies (e.g., from a previous session)\nawait setCookies([\n  { name: 'session_id', value: 'abc123', url: 'https://example.com' },\n  { name: 'lang', value: 'en', url: 'https://example.com' },\n]);\n\n// Clear all cookies (logout)\nawait clearCookies();"
      },
      {
        "title": "batchActions(actions, opts) (from launchBrowser return) / batchActions(page, actions, opts)",
        "body": "Execute multiple actions sequentially in a single call. Reduces LLM round-trips for multi-step flows.\n\nOptionTypeDefaultDescriptionstopOnErrorbooleanfalseHalt on first failuredelayBetweennumber50ms delay between actions for realism\n\nEach action: { action, selector, text, value, key, ms, options }\n\nSupported actions: click, fill, type, press, hover, select, scroll, focus, wait, waitForSelector, humanClick, humanType, snapshot\n\nReturns: { results: [{index, success, result?, error?}], total, successful, failed }\n\n// Fill a booking form in one call\nconst result = await batchActions([\n  { action: 'fill',   selector: '#name',   text: 'John' },\n  { action: 'fill',   selector: '#phone',  text: '+1234567890' },\n  { action: 'select', selector: '#guests', value: '2' },\n  { action: 'humanClick', selector: '#submit' },\n], { stopOnError: true });\n// result.successful === 4, result.failed === 0"
      },
      {
        "title": "humanType(page, selector, text)",
        "body": "Type text with human-like speed (60-220ms/char) and occasional micro-pauses."
      },
      {
        "title": "humanClick(page, x, y)",
        "body": "Click with natural Bezier curve mouse movement."
      },
      {
        "title": "humanScroll(page, direction, amount)",
        "body": "Smooth multi-step scroll with jitter. Direction: 'down' or 'up'."
      },
      {
        "title": "humanRead(page, minMs, maxMs)",
        "body": "Pause as if reading the page. Optional light scroll."
      },
      {
        "title": "shadowFill(page, selector, value)",
        "body": "Fill an input inside Shadow DOM (works where page.fill() fails)."
      },
      {
        "title": "shadowClickButton(page, buttonText)",
        "body": "Click a button by text label, searching through Shadow DOM."
      },
      {
        "title": "pasteIntoEditor(page, editorSelector, text)",
        "body": "Paste text into Lexical, Draft.js, Quill, ProseMirror, or contenteditable editors."
      },
      {
        "title": "dumpInteractiveElements(page, opts) / dumpInteractiveElements(opts) (from launchBrowser return)",
        "body": "List all interactive elements using the accessibility tree. Equivalent to snapshot({ interactiveOnly: true }).\nReturns a compact YAML string with only buttons, inputs, links, and other interactive elements.\nFalls back to DOM querySelectorAll on Playwright < 1.49.\n\nOptionTypeDefaultDescriptionselectorstring'body'CSS selector to scope the dump"
      },
      {
        "title": "getSessionLogs()",
        "body": "List all session log files, newest first. Returns [{ sessionId, file, mtime, size }]."
      },
      {
        "title": "getSessionLog(sessionId)",
        "body": "Read a specific session log by ID. Returns an array of log entries."
      },
      {
        "title": "Action logging",
        "body": "Every browser session records comprehensive structured logs in ~/.clawnet/logs/<session-id>.jsonl.\nThe log captures the full picture: user's task → every agent action → page events → errors."
      },
      {
        "title": "What's logged",
        "body": "The logging system uses a Proxy on the Playwright page object to capture every method call —\nincluding chained locators like page.getByRole('button', { name: 'Submit' }).click().\n\nAutomatically captured:\n\nUser task — the task parameter from launchBrowser({ task: \"...\" })\nAll page actions — goto, click, fill, type, press, check, hover, selectOption, etc.\nAll locator chains — getByRole → click, getByLabel → fill, locator → nth → click, etc.\nObservation calls — snapshot(), takeScreenshot(), dumpInteractiveElements()\nPage events — navigations, popups, dialogs, downloads, page errors\nhuman* helpers — humanClick, humanType, humanScroll, etc.\nCAPTCHA — solveCaptcha attempts and results"
      },
      {
        "title": "Log levels",
        "body": "LevelWhat's loggedUse caseoffNothingProduction, no overheadactions (default)User task, navigation, clicks, fills, typing, locator chains, observation calls, page events, human* helpers, errorsStandard debugging — see what the agent doesverboseAll above + textContent results, evaluate expressions, HTTP 4xx/5xx, console errors/warnings, logger.note()Deep debugging — see what the agent reads and what goes wrong on the page\n\nSet via launchBrowser({ logLevel: 'verbose', task: 'Book a table at Aurora' }) or env CN_LOG_LEVEL=verbose."
      },
      {
        "title": "Example log output (actions level)",
        "body": "{\"ts\":\"...\",\"action\":\"launch\",\"country\":\"ru\",\"mobile\":true,\"profile\":\"default\",\"logLevel\":\"actions\"}\n{\"ts\":\"...\",\"action\":\"task\",\"prompt\":\"Войти в Telegram и отправить сообщение Привет\"}\n{\"ts\":\"...\",\"action\":\"goto\",\"method\":\"goto\",\"args\":[\"https://web.telegram.org\"],\"chain\":\"goto(\\\"https://web.telegram.org\\\")\",\"url\":\"about:blank\",\"ok\":true,\"status\":200}\n{\"ts\":\"...\",\"action\":\"navigated\",\"url\":\"https://web.telegram.org/a/\"}\n{\"ts\":\"...\",\"action\":\"snapshot\",\"selector\":\"body\",\"interactiveOnly\":false,\"length\":3842,\"url\":\"https://web.telegram.org/a/\"}\n{\"ts\":\"...\",\"action\":\"locator\",\"chain\":\"getByRole(\\\"link\\\", {\\\"name\\\":\\\"Log in by phone Number\\\"})\",\"url\":\"https://web.telegram.org/a/\"}\n{\"ts\":\"...\",\"action\":\"click\",\"method\":\"click\",\"args\":[],\"chain\":\"getByRole(\\\"link\\\", {\\\"name\\\":\\\"Log in by phone Number\\\"}) → click()\",\"url\":\"https://web.telegram.org/a/\",\"ok\":true}\n{\"ts\":\"...\",\"action\":\"navigated\",\"url\":\"https://web.telegram.org/a/#/login\"}\n{\"ts\":\"...\",\"action\":\"fill\",\"method\":\"fill\",\"args\":[\"77054595958\"],\"chain\":\"getByLabel(\\\"Phone number\\\") → fill(\\\"77054595958\\\")\",\"url\":\"https://web.telegram.org/a/#/login\",\"ok\":true}\n{\"ts\":\"...\",\"action\":\"screenshot\",\"url\":\"https://web.telegram.org/a/#/login\"}\n{\"ts\":\"...\",\"action\":\"humanClick\",\"args\":[\"page\",100,200],\"url\":\"https://web.telegram.org/a/#/login\",\"ok\":true}"
      },
      {
        "title": "Recording user task",
        "body": "Always pass the user's request via task so the log has full context:\n\nconst { page, logger } = await launchBrowser({\n  task: 'Забронировать столик в Aurora на 8 марта, 19:00, 2 гостя',\n  logLevel: 'verbose',\n  country: 'ru',\n});"
      },
      {
        "title": "Agent reasoning with logger.note()",
        "body": "At verbose level, the agent can record its reasoning:\n\nlogger.note('Navigating to booking page to check available slots');\nawait page.goto('https://restaurant.com/booking');\nlogger.note('Form is empty — need to fill date, time, guests before checking');"
      },
      {
        "title": "Reading logs",
        "body": "const { getSessionLogs, getSessionLog } = require('clawnet/scripts/browser');\n\n// List recent sessions\nconst sessions = getSessionLogs();\n// [{ sessionId: 'abc-123', mtime: '2026-03-01T...', size: 4096 }, ...]\n\n// Read a specific session\nconst log = getSessionLog(sessions[0].sessionId);\n// [{ ts: '...', action: 'task', prompt: 'Войти в Telegram...' },\n//  { ts: '...', action: 'goto', method: 'goto', args: ['https://web.telegram.org'], ... },\n//  { ts: '...', action: 'click', chain: 'getByRole(\"link\") → click()', ... }, ...]\n\n// Or from the current session\nconst { getSessionLog: currentLog } = await launchBrowser();\n// ... do work ...\nconst entries = currentLog();"
      },
      {
        "title": "getCredentials()",
        "body": "Fetch managed proxy + CAPTCHA credentials from Clawnet API. Called automatically by launchBrowser() on fresh launch (not on reuse). Starts the 2-hour trial clock on first call. Requires CN_API_URL and agent credentials (from install, CN_AGENT_TOKEN, or CN_AGENT_ID + CN_AGENT_SECRET)."
      },
      {
        "title": "makeProxy(sessionId, country)",
        "body": "Build proxy config from environment variables. Supports Decodo, Bright Data, IPRoyal, NodeMaven."
      },
      {
        "title": "Supported proxy providers",
        "body": "ProviderEnv prefixSticky sessionsCountriesDecodo (default)CN_PROXY_*Port-based (10001-49999)10+Bright DataCN_PROXY_*Session string195+IPRoyalCN_PROXY_*Password suffix190+NodeMavenCN_PROXY_*Session string150+"
      },
      {
        "title": "Login to a website",
        "body": "const { launchBrowser } = require('clawnet/scripts/browser');\nconst { page, snapshot } = await launchBrowser({ country: 'us', mobile: false });\n\nawait page.goto('https://github.com/login');\n\n// Observe the page first — see what's available\nconst tree = await snapshot({ interactiveOnly: true });\n// tree shows: textbox \"Username or email address\", textbox \"Password\", button \"Sign in\"\n\n// Use semantic locators that match the snapshot\nawait page.getByLabel('Username or email address').fill('myuser');\nawait page.getByLabel('Password').fill('mypass');\nawait page.getByRole('button', { name: 'Sign in' }).click();"
      },
      {
        "title": "Scrape with CAPTCHA bypass",
        "body": "const { launchBrowser, solveCaptcha } = require('clawnet/scripts/browser');\nconst { page, snapshot } = await launchBrowser({ country: 'de' });\n\nawait page.goto('https://protected-site.com');\n\n// Auto-detect and solve any CAPTCHA\ntry {\n  await solveCaptcha(page, { verbose: true });\n} catch (e) {\n  console.log('No CAPTCHA found or solving failed:', e.message);\n}\n\n// Read the content area compactly\nconst content = await snapshot({ selector: '.content' });"
      },
      {
        "title": "Fill Shadow DOM forms",
        "body": "const { launchBrowser, shadowFill, shadowClickButton } = require('clawnet/scripts/browser');\nconst { page } = await launchBrowser();\n\nawait page.goto('https://app-with-shadow-dom.com');\nawait shadowFill(page, 'input[name=\"email\"]', 'user@example.com');\nawait shadowClickButton(page, 'Submit');"
      }
    ],
    "body": "Clawnet\n\nStealth browser skill for AI agents. Installs and configures Chromium with anti-detection, residential proxies (10+ countries), CAPTCHA solving, and human-like behavior simulation.\n\nWhen to use\n\nUse this skill whenever the agent needs to:\n\nBrowse websites, scrape data, or interact with web pages\nFill forms, click buttons, navigate multi-step flows\nBypass Cloudflare, DataDome, PerimeterX, or other bot detection\nSolve CAPTCHAs (reCAPTCHA v2/v3, hCaptcha, Turnstile)\nAppear as a real user from a specific country\nInteract with Shadow DOM elements or rich text editors\n\nAlways use this instead of raw Playwright/Puppeteer. This skill handles anti-detection automatically.\n\nObservation — how to read the page\n\nALWAYS use snapshotAI() instead of page.textContent() or evaluate(). It returns a structured accessibility tree with embedded [ref=eN] annotations. You can then click/fill/type by ref — no CSS selectors needed.\n\nReading the page (preferred: snapshotAI + refs)\n// BAD — dumps ALL text, 50-100K tokens, no structure, no refs\nconst text = await page.textContent('body');\n\n// BAD — brittle regex on raw DOM, breaks when HTML changes\nawait page.evaluate(() => document.querySelector('button').click());\n\n// GOOD — AI-optimized snapshot with clickable refs\nconst { snapshot } = await browser.snapshotAI();\n// Returns:\n//   - navigation \"Main\" [ref=e1]:\n//     - link \"Home\" [ref=e2]\n//   - heading \"Welcome\" [ref=e3]\n//   - textbox \"Email\" [ref=e4]\n//   - textbox \"Password\" [ref=e5]\n//   - button \"Sign in\" [ref=e6]\n\n// Then interact by ref:\nawait browser.fillRef('e4', 'user@example.com');\nawait browser.fillRef('e5', 'secret');\nawait browser.clickRef('e6');\n\nAlternative: snapshot() (YAML without refs)\n// Compact accessibility tree without refs — use when you don't need to interact\nconst tree = await browser.snapshot();\nconst interactive = await browser.snapshot({ interactiveOnly: true });\nconst formTree = await browser.snapshot({ selector: 'form' });\n\nObservation workflow\n\nBefore every action, follow this sequence:\n\nDismiss overlays & accept cookies — after every page.goto(), call await browser.dismissOverlays() to auto-close cookie banners, consent popups, and notification prompts. If a cookie banner or consent dialog is still visible in the snapshot, click \"Accept\" / \"Accept all\" / \"Принять\" before doing anything else. Never skip this step — cookie overlays block interaction with page elements underneath.\nSnapshot — const { snapshot } = await browser.snapshotAI() to see the page with refs\nRead text — await browser.extractText() if you need clean readable text (menus, prices, articles)\nVisual check — await browser.takeScreenshot() only if you need to see colors, layout, maps, or images\nAct by ref — await browser.clickRef('e4'), await browser.fillRef('e5', 'text') etc.\nVerify — await browser.snapshotAI() again to confirm the action worked\nBatch — use batchActions() for multi-step flows\nTargeting elements — use refs from snapshotAI()\n\nALWAYS use refs from snapshotAI() output. NEVER use CSS selectors or evaluate() with regex.\n\n// BAD — brittle CSS selectors that break when HTML changes\nawait page.click('#login_field');\nawait page.fill('input[name=\"email\"]', 'user@example.com');\n\n// BAD — regex on raw DOM, blind guessing\nawait page.evaluate(() => document.querySelectorAll('button').find(b => /sign in/i.test(b.innerText))?.click());\n\n// GOOD — ref-based from snapshotAI() output\nconst { snapshot } = await browser.snapshotAI();\n// snapshot shows: textbox \"Email\" [ref=e4], button \"Sign in\" [ref=e6]\nawait browser.fillRef('e4', 'user@example.com');\nawait browser.clickRef('e6');\n\n// ALSO GOOD — semantic locators (when you know the label)\nawait page.getByLabel('Email').fill('user@example.com');\nawait page.getByLabel('Password').fill('secret');\nawait page.getByRole('button', { name: 'Sign in' }).click();\n\n// Also available:\nawait page.getByPlaceholder('Search...').fill('query');\nawait page.getByText('Welcome back').isVisible();\nawait page.getByRole('link', { name: 'Home' }).click();\nawait page.getByRole('checkbox', { name: 'Remember me' }).check();\n\n\nWhen you see - textbox \"Email\" in the snapshot, use page.getByRole('textbox', { name: 'Email' }). When you see - button \"Submit\", use page.getByRole('button', { name: 'Submit' }).\n\nWhen to fall back to CSS selectors\n\nOnly use CSS selectors when:\n\nThe element has no accessible name or role (rare in modern sites)\nYou need to target by data-testid or other test attributes\nShadow DOM elements not reachable by semantic locators (use shadowFill/shadowClickButton)\nMulti-tab — parallel tasks\n\nUse multiple tabs only when the user needs different websites open at the same time. One tab per website/service — not one tab per action.\n\nWhen to open a new tab vs reuse the current one\n\nNew tab — different website or service that the user may want to come back to:\n\n\"Order a taxi AND book a restaurant\" → 2 tabs (Uber + OpenTable)\n\"Compare prices on Amazon and eBay\" → 2 tabs\n\nSame tab — same website, sequential actions:\n\n\"Order a taxi for me, then for my friend\" → 1 tab (Uber), two orders one after another\n\"Book a table for Saturday, then book another for Sunday\" → 1 tab (OpenTable), two bookings\n\"Search for Air Jordans, then search for Nike Dunks\" → 1 tab (Nike), two searches\n\nThink like a human: you wouldn't open a second Uber tab to order a second ride. You'd finish the first ride, then start the second one in the same tab.\n\nOpening tabs\n\nlaunchBrowser() gives you the first tab. Open more with newTab():\n\nconst { launchBrowser } = require('clawnet/scripts/browser');\n\n// First tab — comes from launchBrowser()\nconst taxi = await launchBrowser({ country: 'us', mobile: false });\nawait taxi.page.goto('https://uber.com');\n\n// Open more tabs — each returns its own result object\nconst resto = await taxi.newTab({ url: 'https://opentable.com', label: 'restaurant' });\nconst shop  = await taxi.newTab({ url: 'https://nike.com', label: 'sneakers' });\n\n\nEach tab object (taxi, resto, shop) has the full API: page.goto(), snapshotAI(), clickRef(), fillRef(), takeScreenshot(), etc. — all scoped to that tab.\n\nWorking with tabs\n\nRule: keep a named variable per tab. This is how you \"remember\" which tab is which.\n\n// Work on the taxi tab\nawait taxi.page.goto('https://uber.com/ride');\nconst { snapshot } = await taxi.snapshotAI();\nawait taxi.fillRef('e5', '123 Main St');        // pickup address\nawait taxi.clickRef('e9');                       // \"Request ride\"\n\n// Switch to the restaurant tab — just use the variable\nconst { snapshot: restoSnap } = await resto.snapshotAI();\nawait resto.fillRef('e3', '2 guests');\nawait resto.fillRef('e4', 'March 8, 7pm');\nawait resto.clickRef('e7');                      // \"Find a table\"\n\n// Switch to sneakers\nawait shop.snapshotAI();\nawait shop.clickRef('e12');                      // \"Air Jordan 1\"\n\n\nNo explicit \"switch tab\" call needed — just use the right variable. Each variable is bound to its tab.\n\nChecking all tabs\nconst { tabs } = await taxi.listTabs();\n// [\n//   { tabId: \"t_a1b2c3\", url: \"https://uber.com/ride\", label: \"\", active: false },\n//   { tabId: \"t_d4e5f6\", url: \"https://opentable.com/...\", label: \"restaurant\", active: false },\n//   { tabId: \"t_g7h8i9\", url: \"https://nike.com/...\", label: \"sneakers\", active: true },\n// ]\n\nGoing back to a tab\n\nIf you lost the variable (e.g., across script invocations), use switchTab(tabId):\n\n// From listTabs() you know the tabId\nconst uberTab = await taxi.switchTab('t_a1b2c3');\nawait uberTab.snapshotAI();  // see what's on the Uber tab now\n\nClosing a tab\nawait shop.closeTab();  // close the sneakers tab\n// shop variable is now stale — don't use it\n\nMulti-tab workflow pattern\n\nWhen the user gives you multiple parallel tasks:\n\nPlan — identify separate tasks (taxi, restaurant, sneakers)\nOpen tabs — one newTab() per task, save each to a named variable\nWork round-robin — do a chunk of work on each tab, take screenshots\nReport — show the user screenshots from each tab so they see all progress\nGo back — when the user says \"cancel the taxi\" or \"check the menu\", switch to the right tab variable\nExample: user says \"Order a taxi, book a table, and find sneakers\"\n// Phase 1: open all tabs\nconst taxi  = await launchBrowser({ country: 'us', mobile: false });\nconst resto = await taxi.newTab({ url: 'https://opentable.com' });\nconst shop  = await taxi.newTab({ url: 'https://nike.com' });\n\n// Phase 2: start each task\nawait taxi.page.goto('https://uber.com');\nawait taxi.fillRef('e3', 'Airport');         // destination\nconst taxiSS = await taxi.takeScreenshot();\n\nawait resto.fillRef('e2', 'Italian');        // cuisine search\nawait resto.clickRef('e5');                  // search\nconst restoSS = await resto.takeScreenshot();\n\nawait shop.fillRef('e1', 'Air Jordan');      // search\nawait shop.clickRef('e3');                   // search button\nconst shopSS = await shop.takeScreenshot();\n\n// Phase 3: report to user (ALL tabs' screenshots)\n// \"Here's what I've set up: [taxi screenshot] [restaurant screenshot] [shop screenshot]\"\n\n// Phase 4: user says \"cancel the taxi, check restaurant prices\"\nawait taxi.clickRef('e15');                  // \"Cancel\" button\nconst cancelSS = await taxi.takeScreenshot();\n\nconst { text } = await resto.extractText();  // read menu prices\nconst pricesSS = await resto.takeScreenshot();\n\nKey rules\nOne tab per website/service — not one tab per action. Sequential tasks on the same site happen in one tab\nNew tab only for a different site that the user may want to come back to\nOne variable per tab — don't reuse variables, name them by purpose\nTabs share cookies — login on one tab is visible on all tabs (same browser context)\nScreenshots from each tab — always show the user what's happening on each tab\nDon't open too many tabs — 2-4 is practical, more gets confusing for both you and the user\nTabs survive between script runs — the daemon keeps them alive. Use listTabs() to rediscover them\nScreenshot rules\n\nALWAYS attach a screenshot when communicating with the user. The user cannot see the browser — you are their eyes. Every message to the user MUST include a screenshot. No exceptions.\n\nWhen to take screenshots\n\nEvery message you send to the user must have a screenshot attached. Specifically:\n\nBefore asking for confirmation — \"Book this table?\" + screenshot of the filled form. The user must SEE what they are confirming.\nWhen reporting an error — \"No slots available\" + screenshot proving the result. Without a screenshot, the user has no reason to trust you.\nWhen unable to complete an action — \"Authorization failed\" + screenshot showing what happened.\nAfter every key step — filled form, selected date, entered address, etc.\nWhen completing the task (MANDATORY) — \"Done! Order placed\" + screenshot of the final result/confirmation page. The user must see proof that the action was completed.\nHow to take screenshots\n\nUse the built-in helpers returned by launchBrowser():\n\nconst { page, takeScreenshot, screenshotAndReport } = await launchBrowser();\n\n// Option 1: just the base64 screenshot\nconst base64 = await takeScreenshot();\n\n// Option 2: screenshot + message bundled together\nconst report = await screenshotAndReport(\"Form filled. Confirm booking?\");\n// → { message: \"Form filled...\", screenshot: \"iVBOR...\", mimeType: \"image/png\" }\n\n\nOr directly via Playwright:\n\nconst screenshot = await page.screenshot({ type: 'png' });\nconst base64 = screenshot.toString('base64');\n\nRules\nNever tell the user \"the form is empty\", \"widget is disabled\", or \"no results\" without a screenshot as proof.\nNever ask for confirmation without showing the current state of the page.\nNever say \"Done!\" without a screenshot of the final result.\nThe user must see what you see. Always.\nInstallation\nclawhub install clawnet\n\n\nThis installs Chromium via Playwright automatically. No separate browser download needed.\n\nImportant (Linux/Docker): If Chromium fails to launch with missing library errors (e.g. libnspr4.so), install system dependencies:\n\nnpx playwright install-deps chromium\n# or manually:\napt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 libpangocairo-1.0-0 libcairo2 libasound2\n\nFirst-Run Onboarding\n\nlaunchBrowser() is zero-config — it auto-registers a new agent and saves credentials if none exist.\n\nHowever, on first use the agent SHOULD check if the user already has credentials to avoid creating duplicate agents:\n\nStep 1: Check for existing credentials\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst CREDS_FILE = path.join(os.homedir(), '.clawnet', 'agent-credentials.json');\nlet hasCreds = false;\ntry { hasCreds = Boolean(JSON.parse(fs.readFileSync(CREDS_FILE, 'utf-8')).agentId); } catch {}\n\n\nIf credentials exist on disk → skip to Step 3 (just launch).\n\nStep 2: Ask the user\n\nAsk the user: \"Do you have existing Clawnet credentials (agentId:agentSecret), or should I create new ones?\"\n\nIf user provides credentials → save them to CREDS_FILE:\nconst [agentId, agentSecret] = userInput.split(':');\nfs.mkdirSync(path.dirname(CREDS_FILE), { recursive: true, mode: 0o700 });\nfs.writeFileSync(CREDS_FILE, JSON.stringify({ agentId, agentSecret, createdAt: new Date().toISOString() }, null, 2), { mode: 0o600 });\n\nIf user says \"new\" or \"no\" → skip to Step 3. launchBrowser() will auto-register.\nStep 3: Launch\nconst { launchBrowser } = require('clawnet/scripts/browser');\nconst { browser, page } = await launchBrowser({ country: 'us' });\n\n\nThat's it. No env vars needed. launchBrowser() will:\n\nFind credentials on disk (or auto-register a new agent with the API)\nFetch managed proxy + CAPTCHA keys\nLaunch stealth Chromium with residential proxy\n\nAfter first launch, show credentials and support contacts to the user:\n\nYour Clawnet credentials:\n  agentId: <agentId>\n  agentSecret: <agentSecret>\n\nSave these for future use or transfer to another agent.\n\nIf you run into any issues: [Discord](https://discord.com/invite/wtA85mJM) | [Telegram](https://t.me/+nPjZFg8PJc4zZTIy)\n\nAgent Credentials & Subscription\nTrial model\n\nOn the first launchBrowser() call, a 2-hour free trial starts. After expiry, getCredentials() returns upgradeUrl. Show it to the user:\n\nTrial expired. Subscribe to continue: <upgradeUrl>\nOr set your own proxy/CAPTCHA keys (BYO mode).\n\nAfter payment\n\nSubscription activates automatically within seconds (webhook). No manual steps needed — the next launchBrowser() call will receive managed credentials.\n\nTransfer / Recovery / Rotation\n\nTo transfer/recover on another agent, provide the same agentId + agentSecret during install. Backend rule: one subscriptionId can be linked to only one agentId at a time.\n\nTo rotate a compromised secret, keep the same agentId and issue a new agentSecret (authorized by current secret or recovery code). Old secret is invalidated immediately.\n\nCancel subscription\n\nIf the user asks to cancel their subscription, call the cancel endpoint:\n\nconst creds = JSON.parse(fs.readFileSync(CREDS_FILE, 'utf-8'));\nconst token = `CN1.${creds.agentId}.${creds.agentSecret}`;\nconst resp = await fetch(`${apiUrl}/cancel-subscription`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${token}` },\n});\nconst result = await resp.json();\n// { canceled: true, accessUntil: \"2026-04-02T00:00:00Z\", message: \"...\" }\n\n\nShow the result to the user:\n\nYour subscription has been canceled. Access remains until <accessUntil>.\nIf you change your mind, you can resubscribe anytime.\n\nNeed help? [Discord](https://discord.com/invite/wtA85mJM) | [Telegram](https://t.me/+nPjZFg8PJc4zZTIy)\n\nSetup modes\nOption A: Managed credentials (default, recommended)\n\nThe onboarding flow above sets everything up automatically. Environment variables used:\n\nCN_API_URL=https://api.clawpets.io/clawnet/v1\n# Set automatically by onboarding, or manually:\nCN_AGENT_TOKEN=CN1.<agentId>.<agentSecret>\n# Or separately:\nCN_AGENT_ID=<agent-uuid>\nCN_AGENT_SECRET=<agent-secret>\n\n\nThe skill will automatically fetch Decodo proxy credentials and 2captcha API key on launch.\n\nOption B: BYO (Bring Your Own)\n\nSet proxy and CAPTCHA credentials directly:\n\nCN_PROXY_PROVIDER=decodo          # decodo | brightdata | iproyal | nodemaven\nCN_PROXY_USER=your-proxy-user\nCN_PROXY_PASS=your-proxy-pass\nCN_PROXY_COUNTRY=us               # us, gb, de, nl, jp, fr, ca, au, sg, ro, br, in\nTWOCAPTCHA_KEY=your-2captcha-key\n\nOption C: No proxy (local testing)\nCN_NO_PROXY=1\n\nBrowser lifecycle\n\nDO NOT close the browser between steps. The browser persists automatically via a background daemon. Just call launchBrowser() at the start of each script — it reconnects to the existing browser with all your tabs, cookies, and login sessions intact.\n\n// Script 1: agent logs into a site\nconst b = await launchBrowser({ country: 'us' });\nawait b.page.goto('https://example.com/login');\nawait b.fillRef('e2', 'user@example.com');\nawait b.clickRef('e5');\n// Script ends — browser stays alive\n\n// Script 2 (later): agent continues where it left off\nconst b = await launchBrowser({ country: 'us' });\n// Same browser, same tab, same cookies — still logged in\nawait b.snapshotAI();  // sees the logged-in page\n\nWhat NOT to do\n// BAD — kills the browser, loses all state\nawait browser.close();\nawait closeBrowser();\n\n// BAD — opening a new browser when you already have one\nconst b1 = await launchBrowser();\n// ... do some work ...\nconst b2 = await launchBrowser();  // this REUSES b1, doesn't create a new browser\n\nWhen to actually close\n\nOnly close the browser when the user explicitly says they're done with ALL browser tasks:\n\n\"Close the browser\"\n\"I'm done, clean up\"\n\"Shut everything down\"\n\nOtherwise, leave it running. The daemon auto-shuts down after 5 minutes of inactivity anyway.\n\nQuick start\nconst { launchBrowser, solveCaptcha } = require('clawnet/scripts/browser');\n\n// Launch stealth browser with US residential proxy\nconst b = await launchBrowser({\n  country: 'us',\n  mobile: false,    // Desktop Chrome (true = iPhone 15 Pro)\n  headless: true,\n});\n\n// Browse normally — anti-detection is automatic\nawait b.page.goto('https://example.com');\n\n// Read the page\nconst { snapshot } = await b.snapshotAI();\n\n// Interact by ref\nawait b.fillRef('e4', 'user@example.com');\nawait b.clickRef('e6');\n\n// Solve CAPTCHA if present\nconst result = await b.solveCaptcha({ verbose: true });\n\n// Take a screenshot for the user\nconst ss = await b.takeScreenshot();\n\n// DO NOT close — browser stays alive for the next step\n\nAPI Reference\nimportCredentials(agentId, agentSecret)\n\nSave user-provided agent credentials to disk. Use when transferring an existing account to a new machine.\n\nconst { importCredentials } = require('clawnet/scripts/browser');\nconst result = importCredentials('your-uuid', 'your-secret');\n// { ok: true, agentId: 'your-uuid' }\n\nlaunchBrowser(opts)\n\nLaunch a stealth Chromium browser with residential proxy.\n\nOption\tType\tDefault\tDescription\ncountry\tstring\t'us'\tProxy country: us, gb, de, nl, jp, fr, ca, au, sg, ro, br, in\nmobile\tboolean\ttrue\ttrue = iPhone 15 Pro, false = Desktop Chrome\nheadless\tboolean\ttrue\tRun headless\nuseProxy\tboolean\ttrue\tEnable residential proxy\nsession\tstring\trandom\tSticky session ID (same IP across requests)\nprofile\tstring\t'default'\tPersistent profile name (null = ephemeral)\nreuse\tboolean\ttrue\tReuse running browser for this profile (new tab, same process)\nlogLevel\tstring\t'actions'\t'off' | 'actions' | 'verbose'. Env: CN_LOG_LEVEL\ntask\tstring\tnull\tUser's prompt / task description. Recorded in the session log for context.\n\nReturns: { browser, ctx, page, logger, tabId, newTab, listTabs, closeTab, switchTab, humanClick, humanMouseMove, humanType, humanScroll, humanRead, solveCaptcha, takeScreenshot, screenshotAndReport, snapshot, snapshotAI, dumpInteractiveElements, clickRef, fillRef, typeRef, selectRef, hoverRef, extractText, getCookies, setCookies, clearCookies, batchActions, sleep, rand, getSessionLog }\n\nsolveCaptcha(page, opts)\n\nAuto-detect and solve CAPTCHA on the current page. Supports reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile.\n\nOption\tType\tDefault\tDescription\napiKey\tstring\tenv TWOCAPTCHA_KEY\t2captcha API key\ntimeout\tnumber\t120000\tMax wait time in ms\nverbose\tboolean\tfalse\tLog progress\n\nReturns: { token, type, sitekey }\n\ntakeScreenshot(page, opts)\n\nTake a screenshot and return it as a base64-encoded PNG string.\n\nOption\tType\tDefault\tDescription\nfullPage\tboolean\tfalse\tCapture the full scrollable page\n\nReturns: string (base64 PNG)\n\nscreenshotAndReport(page, message, opts)\n\nTake a screenshot and pair it with a message. Returns an object ready to attach to an LLM response.\n\nOption\tType\tDefault\tDescription\nfullPage\tboolean\tfalse\tCapture the full scrollable page\n\nReturns: { message, screenshot, mimeType } — screenshot is base64 PNG\n\nsnapshot(page, opts) / snapshot(opts) (from launchBrowser return)\n\nCapture a compact accessibility tree of the page. Returns YAML string. Use this instead of page.textContent(). See \"Observation\" section above.\n\nOption\tType\tDefault\tDescription\nselector\tstring\t'body'\tCSS selector to scope the snapshot\ninteractiveOnly\tboolean\tfalse\tKeep only interactive elements (buttons, inputs, links)\nmaxLength\tnumber\t20000\tTruncate output to N characters\ntimeout\tnumber\t5000\tPlaywright timeout in ms\n\nReturns: string (YAML accessibility tree)\n\nsnapshotAI(opts) — AI-optimized snapshot with refs ⭐ PREFERRED\n\nReturns a structured accessibility tree with embedded [ref=eN] annotations. Use this as the primary way to read pages.\n\nconst { snapshot, refs, truncated } = await browser.snapshotAI();\n// snapshot: \"- heading \\\"Welcome\\\" [ref=e1]\\n- textbox \\\"Email\\\" [ref=e2]\\n- button \\\"Sign in\\\" [ref=e3]\"\n// refs: { e1: true, e2: true, e3: true }\n\nOption\tType\tDefault\tDescription\nmaxChars\tnumber\t20000\tTruncate snapshot to N characters\ntimeout\tnumber\t5000\tPlaywright timeout in ms\n\nReturns: { snapshot: string, refs: Object, truncated?: boolean }\n\nclickRef(ref, opts) — Click element by ref\nawait browser.clickRef('e3');                          // left click\nawait browser.clickRef('e3', { doubleClick: true });   // double click\n\nfillRef(ref, value, opts) — Fill input by ref\nawait browser.fillRef('e2', 'user@example.com');\n\ntypeRef(ref, text, opts) — Type text by ref\nawait browser.typeRef('e2', 'hello');                          // instant fill\nawait browser.typeRef('e2', 'hello', { slowly: true });        // human-like typing\nawait browser.typeRef('e2', 'hello', { submit: true });        // type + Enter\n\nselectRef(ref, value, opts) — Select option by ref\nawait browser.selectRef('e5', 'US');\n\nhoverRef(ref, opts) — Hover element by ref\nawait browser.hoverRef('e1');  // reveal tooltip/dropdown\n\nnewTab(opts) — Open a new tab\n\nOpens a new browser tab and returns a new result object scoped to that tab. All methods on the returned object (page.goto, snapshotAI, clickRef, etc.) operate on the new tab.\n\nOption\tType\tDefault\tDescription\nurl\tstring\t-\tNavigate to this URL immediately\nlabel\tstring\t''\tHuman-readable label for the tab\nconst tab2 = await browser.newTab({ url: 'https://opentable.com', label: 'restaurant' });\nawait tab2.snapshotAI();  // snapshot of opentable.com\n\nlistTabs() — List all open tabs\n\nReturns all open tabs with their IDs, URLs, labels, and active status.\n\nconst { tabs } = await browser.listTabs();\n// [{ tabId: \"t_abc\", url: \"https://...\", label: \"restaurant\", active: true, createdAt: \"...\" }]\n\ncloseTab(tabId?) — Close a tab\n\nCloses the specified tab (or the current tab if no tabId given).\n\nawait tab2.closeTab();           // close this tab\nawait browser.closeTab('t_abc'); // close by ID\n\nswitchTab(tabId) — Switch to a tab\n\nReturns a new result object scoped to the specified tab. Use when you need to return to a tab whose variable you lost (e.g., across script invocations).\n\nconst { tabs } = await browser.listTabs();\nconst uberTab = await browser.switchTab(tabs[0].tabId);\nawait uberTab.snapshotAI();\n\nextractText(opts) (from launchBrowser return) / extractText(page, opts)\n\nExtract clean readable text from the page, stripping navigation, ads, modals, and noise. Use when you need to READ the page content (menus, prices, articles) rather than interact with UI elements.\n\nOption\tType\tDefault\tDescription\nmode\tstring\t'readability'\t'readability' strips noise, 'raw' returns body.innerText\nmaxChars\tnumber\tunlimited\tTruncate text to N characters\n\nReturns: { url, title, text, truncated }\n\n// Read a restaurant menu\nconst { text } = await extractText({ mode: 'readability' });\n// → \"Pizza Menu\\n\\nMargherita\\nClassic pizza with mozzarella...\\nFrom 399 ₽\\n\\n...\"\n\n// Raw mode for simple pages\nconst { text: raw } = await extractText({ mode: 'raw', maxChars: 5000 });\n\n\nWhen to use extractText() vs snapshot():\n\nextractText() — reading text content (menus, prices, articles, descriptions)\nsnapshot() — understanding page structure and finding interactive elements (buttons, inputs, links)\ngetCookies(urls?) / setCookies(cookies) / clearCookies()\n\nManage browser cookies. Use for session persistence, login state checks, and cookie transfer between tasks.\n\n// Check if logged in\nconst cookies = await getCookies('https://example.com');\nconst hasAuth = cookies.some(c => c.name === 'session_id');\n\n// Set cookies (e.g., from a previous session)\nawait setCookies([\n  { name: 'session_id', value: 'abc123', url: 'https://example.com' },\n  { name: 'lang', value: 'en', url: 'https://example.com' },\n]);\n\n// Clear all cookies (logout)\nawait clearCookies();\n\nbatchActions(actions, opts) (from launchBrowser return) / batchActions(page, actions, opts)\n\nExecute multiple actions sequentially in a single call. Reduces LLM round-trips for multi-step flows.\n\nOption\tType\tDefault\tDescription\nstopOnError\tboolean\tfalse\tHalt on first failure\ndelayBetween\tnumber\t50\tms delay between actions for realism\n\nEach action: { action, selector, text, value, key, ms, options }\n\nSupported actions: click, fill, type, press, hover, select, scroll, focus, wait, waitForSelector, humanClick, humanType, snapshot\n\nReturns: { results: [{index, success, result?, error?}], total, successful, failed }\n\n// Fill a booking form in one call\nconst result = await batchActions([\n  { action: 'fill',   selector: '#name',   text: 'John' },\n  { action: 'fill',   selector: '#phone',  text: '+1234567890' },\n  { action: 'select', selector: '#guests', value: '2' },\n  { action: 'humanClick', selector: '#submit' },\n], { stopOnError: true });\n// result.successful === 4, result.failed === 0\n\nhumanType(page, selector, text)\n\nType text with human-like speed (60-220ms/char) and occasional micro-pauses.\n\nhumanClick(page, x, y)\n\nClick with natural Bezier curve mouse movement.\n\nhumanScroll(page, direction, amount)\n\nSmooth multi-step scroll with jitter. Direction: 'down' or 'up'.\n\nhumanRead(page, minMs, maxMs)\n\nPause as if reading the page. Optional light scroll.\n\nshadowFill(page, selector, value)\n\nFill an input inside Shadow DOM (works where page.fill() fails).\n\nshadowClickButton(page, buttonText)\n\nClick a button by text label, searching through Shadow DOM.\n\npasteIntoEditor(page, editorSelector, text)\n\nPaste text into Lexical, Draft.js, Quill, ProseMirror, or contenteditable editors.\n\ndumpInteractiveElements(page, opts) / dumpInteractiveElements(opts) (from launchBrowser return)\n\nList all interactive elements using the accessibility tree. Equivalent to snapshot({ interactiveOnly: true }). Returns a compact YAML string with only buttons, inputs, links, and other interactive elements. Falls back to DOM querySelectorAll on Playwright < 1.49.\n\nOption\tType\tDefault\tDescription\nselector\tstring\t'body'\tCSS selector to scope the dump\ngetSessionLogs()\n\nList all session log files, newest first. Returns [{ sessionId, file, mtime, size }].\n\ngetSessionLog(sessionId)\n\nRead a specific session log by ID. Returns an array of log entries.\n\nAction logging\n\nEvery browser session records comprehensive structured logs in ~/.clawnet/logs/<session-id>.jsonl. The log captures the full picture: user's task → every agent action → page events → errors.\n\nWhat's logged\n\nThe logging system uses a Proxy on the Playwright page object to capture every method call — including chained locators like page.getByRole('button', { name: 'Submit' }).click().\n\nAutomatically captured:\n\nUser task — the task parameter from launchBrowser({ task: \"...\" })\nAll page actions — goto, click, fill, type, press, check, hover, selectOption, etc.\nAll locator chains — getByRole → click, getByLabel → fill, locator → nth → click, etc.\nObservation calls — snapshot(), takeScreenshot(), dumpInteractiveElements()\nPage events — navigations, popups, dialogs, downloads, page errors\nhuman* helpers — humanClick, humanType, humanScroll, etc.\nCAPTCHA — solveCaptcha attempts and results\nLog levels\nLevel\tWhat's logged\tUse case\noff\tNothing\tProduction, no overhead\nactions (default)\tUser task, navigation, clicks, fills, typing, locator chains, observation calls, page events, human* helpers, errors\tStandard debugging — see what the agent does\nverbose\tAll above + textContent results, evaluate expressions, HTTP 4xx/5xx, console errors/warnings, logger.note()\tDeep debugging — see what the agent reads and what goes wrong on the page\n\nSet via launchBrowser({ logLevel: 'verbose', task: 'Book a table at Aurora' }) or env CN_LOG_LEVEL=verbose.\n\nExample log output (actions level)\n{\"ts\":\"...\",\"action\":\"launch\",\"country\":\"ru\",\"mobile\":true,\"profile\":\"default\",\"logLevel\":\"actions\"}\n{\"ts\":\"...\",\"action\":\"task\",\"prompt\":\"Войти в Telegram и отправить сообщение Привет\"}\n{\"ts\":\"...\",\"action\":\"goto\",\"method\":\"goto\",\"args\":[\"https://web.telegram.org\"],\"chain\":\"goto(\\\"https://web.telegram.org\\\")\",\"url\":\"about:blank\",\"ok\":true,\"status\":200}\n{\"ts\":\"...\",\"action\":\"navigated\",\"url\":\"https://web.telegram.org/a/\"}\n{\"ts\":\"...\",\"action\":\"snapshot\",\"selector\":\"body\",\"interactiveOnly\":false,\"length\":3842,\"url\":\"https://web.telegram.org/a/\"}\n{\"ts\":\"...\",\"action\":\"locator\",\"chain\":\"getByRole(\\\"link\\\", {\\\"name\\\":\\\"Log in by phone Number\\\"})\",\"url\":\"https://web.telegram.org/a/\"}\n{\"ts\":\"...\",\"action\":\"click\",\"method\":\"click\",\"args\":[],\"chain\":\"getByRole(\\\"link\\\", {\\\"name\\\":\\\"Log in by phone Number\\\"}) → click()\",\"url\":\"https://web.telegram.org/a/\",\"ok\":true}\n{\"ts\":\"...\",\"action\":\"navigated\",\"url\":\"https://web.telegram.org/a/#/login\"}\n{\"ts\":\"...\",\"action\":\"fill\",\"method\":\"fill\",\"args\":[\"77054595958\"],\"chain\":\"getByLabel(\\\"Phone number\\\") → fill(\\\"77054595958\\\")\",\"url\":\"https://web.telegram.org/a/#/login\",\"ok\":true}\n{\"ts\":\"...\",\"action\":\"screenshot\",\"url\":\"https://web.telegram.org/a/#/login\"}\n{\"ts\":\"...\",\"action\":\"humanClick\",\"args\":[\"page\",100,200],\"url\":\"https://web.telegram.org/a/#/login\",\"ok\":true}\n\nRecording user task\n\nAlways pass the user's request via task so the log has full context:\n\nconst { page, logger } = await launchBrowser({\n  task: 'Забронировать столик в Aurora на 8 марта, 19:00, 2 гостя',\n  logLevel: 'verbose',\n  country: 'ru',\n});\n\nAgent reasoning with logger.note()\n\nAt verbose level, the agent can record its reasoning:\n\nlogger.note('Navigating to booking page to check available slots');\nawait page.goto('https://restaurant.com/booking');\nlogger.note('Form is empty — need to fill date, time, guests before checking');\n\nReading logs\nconst { getSessionLogs, getSessionLog } = require('clawnet/scripts/browser');\n\n// List recent sessions\nconst sessions = getSessionLogs();\n// [{ sessionId: 'abc-123', mtime: '2026-03-01T...', size: 4096 }, ...]\n\n// Read a specific session\nconst log = getSessionLog(sessions[0].sessionId);\n// [{ ts: '...', action: 'task', prompt: 'Войти в Telegram...' },\n//  { ts: '...', action: 'goto', method: 'goto', args: ['https://web.telegram.org'], ... },\n//  { ts: '...', action: 'click', chain: 'getByRole(\"link\") → click()', ... }, ...]\n\n// Or from the current session\nconst { getSessionLog: currentLog } = await launchBrowser();\n// ... do work ...\nconst entries = currentLog();\n\ngetCredentials()\n\nFetch managed proxy + CAPTCHA credentials from Clawnet API. Called automatically by launchBrowser() on fresh launch (not on reuse). Starts the 2-hour trial clock on first call. Requires CN_API_URL and agent credentials (from install, CN_AGENT_TOKEN, or CN_AGENT_ID + CN_AGENT_SECRET).\n\nmakeProxy(sessionId, country)\n\nBuild proxy config from environment variables. Supports Decodo, Bright Data, IPRoyal, NodeMaven.\n\nSupported proxy providers\nProvider\tEnv prefix\tSticky sessions\tCountries\nDecodo (default)\tCN_PROXY_*\tPort-based (10001-49999)\t10+\nBright Data\tCN_PROXY_*\tSession string\t195+\nIPRoyal\tCN_PROXY_*\tPassword suffix\t190+\nNodeMaven\tCN_PROXY_*\tSession string\t150+\nExamples\nLogin to a website\nconst { launchBrowser } = require('clawnet/scripts/browser');\nconst { page, snapshot } = await launchBrowser({ country: 'us', mobile: false });\n\nawait page.goto('https://github.com/login');\n\n// Observe the page first — see what's available\nconst tree = await snapshot({ interactiveOnly: true });\n// tree shows: textbox \"Username or email address\", textbox \"Password\", button \"Sign in\"\n\n// Use semantic locators that match the snapshot\nawait page.getByLabel('Username or email address').fill('myuser');\nawait page.getByLabel('Password').fill('mypass');\nawait page.getByRole('button', { name: 'Sign in' }).click();\n\nScrape with CAPTCHA bypass\nconst { launchBrowser, solveCaptcha } = require('clawnet/scripts/browser');\nconst { page, snapshot } = await launchBrowser({ country: 'de' });\n\nawait page.goto('https://protected-site.com');\n\n// Auto-detect and solve any CAPTCHA\ntry {\n  await solveCaptcha(page, { verbose: true });\n} catch (e) {\n  console.log('No CAPTCHA found or solving failed:', e.message);\n}\n\n// Read the content area compactly\nconst content = await snapshot({ selector: '.content' });\n\nFill Shadow DOM forms\nconst { launchBrowser, shadowFill, shadowClickButton } = require('clawnet/scripts/browser');\nconst { page } = await launchBrowser();\n\nawait page.goto('https://app-with-shadow-dom.com');\nawait shadowFill(page, 'input[name=\"email\"]', 'user@example.com');\nawait shadowClickButton(page, 'Submit');"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/ekenesbek/pets-browser",
    "publisherUrl": "https://clawhub.ai/ekenesbek/pets-browser",
    "owner": "ekenesbek",
    "version": "0.2.2",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/pets-browser",
    "downloadUrl": "https://openagent3.xyz/downloads/pets-browser",
    "agentUrl": "https://openagent3.xyz/skills/pets-browser/agent",
    "manifestUrl": "https://openagent3.xyz/skills/pets-browser/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/pets-browser/agent.md"
  }
}