[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nThis project uses [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation.\n\n## Adding a changeset\n\nWhen you make a change that should be released, run:\n\n```bash\npnpm changeset\n```\n\nThis will prompt you to:\n1. Select the type of change (patch, minor, major)\n2. Write a summary of your changes\n\nThe changeset file will be committed with your PR.\n\n## Release process\n\nWhen changesets are merged to `main`, the release workflow will:\n1. Create a \"Version Packages\" PR that updates version numbers and changelogs\n2. When that PR is merged, packages are automatically published to npm\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.1.1/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": []\n}\n"
  },
  {
    "path": ".claude-plugin/marketplace.json",
    "content": "{\n  \"$schema\": \"https://anthropic.com/claude-code/marketplace.schema.json\",\n  \"name\": \"agent-browser\",\n  \"description\": \"Headless browser automation for AI agents\",\n  \"owner\": {\n    \"name\": \"Vercel\",\n    \"email\": \"support@vercel.com\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"agent-browser\",\n      \"description\": \"Automates browser interactions for web testing, form filling, screenshots, and data extraction\",\n      \"source\": \"./\",\n      \"strict\": false,\n      \"skills\": [\"./skills/agent-browser\"],\n      \"category\": \"development\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  version-sync:\n    name: Version Sync Check\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Check version sync\n        run: node scripts/check-version-sync.js\n\n  rust:\n    name: Rust\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          components: rustfmt, clippy\n\n      - name: Cache Rust build artifacts\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: cli\n\n      - name: Format check\n        run: cargo fmt --manifest-path cli/Cargo.toml -- --check\n\n      - name: Clippy check\n        run: cargo clippy --manifest-path cli/Cargo.toml -- -D warnings\n\n      - name: Run Rust tests\n        run: cargo test --profile ci --manifest-path cli/Cargo.toml\n\n  rust-cross:\n    name: Rust (${{ matrix.os }} - ${{ matrix.target }})\n    if: github.event_name != 'pull_request'\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        include:\n          - os: macos-latest\n            target: aarch64-apple-darwin\n          - os: macos-latest\n            target: x86_64-apple-darwin\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.target }}\n\n      - name: Cache Rust build artifacts\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: cli\n\n      - name: Run Rust tests\n        run: cargo test --profile ci --manifest-path cli/Cargo.toml --target ${{ matrix.target }}\n\n  native-e2e:\n    name: Native E2E Tests\n    if: github.event_name != 'pull_request'\n    runs-on: ubuntu-latest\n    needs: rust\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Cache Rust build artifacts\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: cli\n\n      - name: Install Chrome\n        run: |\n          cargo run --manifest-path cli/Cargo.toml -- install --with-deps\n\n      - name: Run e2e tests\n        run: cargo test --profile ci --manifest-path cli/Cargo.toml e2e -- --ignored --test-threads=1\n\n  windows-integration:\n    name: Windows Integration Test\n    if: github.event_name != 'pull_request'\n    runs-on: windows-latest\n    needs: rust-cross\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: x86_64-pc-windows-msvc\n\n      - name: Cache Rust build artifacts\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: cli\n\n      - name: Build Rust CLI\n        run: cargo build --release --manifest-path cli/Cargo.toml --target x86_64-pc-windows-msvc\n\n      - name: Copy CLI binary to bin directory\n        run: |\n          Copy-Item cli/target/x86_64-pc-windows-msvc/release/agent-browser.exe bin/agent-browser-win32-x64.exe\n\n      - name: Test agent-browser install command\n        run: |\n          $env:PATH = \"$pwd\\bin;$env:PATH\"\n          for ($i = 1; $i -le 3; $i++) {\n            bin/agent-browser-win32-x64.exe install\n            if ($LASTEXITCODE -eq 0) { exit 0 }\n            Write-Host \"Attempt $i failed, retrying in 10 seconds...\"\n            Start-Sleep -Seconds 10\n          }\n          exit 1\n        shell: pwsh\n        timeout-minutes: 10\n\n      - name: Test daemon lifecycle (open, snapshot, close)\n        run: |\n          $env:PATH = \"$pwd\\bin;$env:PATH\"\n          Write-Host \"--- Opening page ---\"\n          bin/agent-browser-win32-x64.exe open https://example.com\n          if ($LASTEXITCODE -ne 0) { Write-Error \"open failed\"; exit 1 }\n          Write-Host \"--- Taking snapshot ---\"\n          $snapshot = bin/agent-browser-win32-x64.exe snapshot\n          if ($LASTEXITCODE -ne 0) { Write-Error \"snapshot failed\"; exit 1 }\n          Write-Host $snapshot\n          Write-Host \"--- Closing browser ---\"\n          bin/agent-browser-win32-x64.exe close\n          if ($LASTEXITCODE -ne 0) { Write-Error \"close failed\"; exit 1 }\n          Write-Host \"--- Windows daemon lifecycle test passed ---\"\n        shell: pwsh\n        timeout-minutes: 5\n\n  global-install:\n    name: Global Install (${{ matrix.os }})\n    if: github.event_name != 'pull_request'\n    runs-on: ${{ matrix.os }}\n    needs: rust-cross\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            binary: agent-browser-linux-x64\n          - os: macos-latest\n            target: aarch64-apple-darwin\n            binary: agent-browser-darwin-arm64\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n            binary: agent-browser-win32-x64.exe\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.target }}\n\n      - name: Cache Rust build artifacts\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: cli\n\n      - name: Build Rust CLI\n        run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}\n\n      - name: Copy CLI binary to bin directory (Unix)\n        if: runner.os != 'Windows'\n        run: cp cli/target/${{ matrix.target }}/release/agent-browser bin/${{ matrix.binary }}\n\n      - name: Copy CLI binary to bin directory (Windows)\n        if: runner.os == 'Windows'\n        run: Copy-Item cli/target/${{ matrix.target }}/release/agent-browser.exe bin/${{ matrix.binary }}\n\n      - name: Test npm global install\n        run: |\n          npm pack\n          npm install -g agent-browser-*.tgz\n          agent-browser --version\n        shell: bash\n\n      - name: Verify symlink points to native binary (Unix)\n        if: runner.os != 'Windows'\n        run: |\n          SYMLINK=$(npm prefix -g)/bin/agent-browser\n          TARGET=$(readlink \"$SYMLINK\")\n          echo \"Symlink: $SYMLINK\"\n          echo \"Target: $TARGET\"\n          if [[ \"$TARGET\" != *\"${{ matrix.binary }}\"* ]]; then\n            echo \"ERROR: Symlink should point to native binary, not JS wrapper\"\n            exit 1\n          fi\n          echo \"Symlink correctly points to native binary\"\n        shell: bash\n\n      - name: Verify shim points to native binary (Windows)\n        if: runner.os == 'Windows'\n        run: |\n          $shimPath = \"$(npm prefix -g)\\agent-browser.cmd\"\n          $content = Get-Content $shimPath -Raw\n          echo \"Shim path: $shimPath\"\n          echo \"Shim content:\"\n          echo $content\n          if ($content -notmatch \"agent-browser-win32-x64\\.exe\") {\n            echo \"ERROR: Shim should point to native .exe, not JS wrapper\"\n            exit 1\n          }\n          echo \"Shim correctly points to native binary\"\n        shell: pwsh\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  # Build native binaries for all platforms first\n  build-binaries:\n    name: Build ${{ matrix.name }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - name: Linux x64\n            os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            binary: agent-browser-linux-x64\n            use_zigbuild: true\n          - name: Linux ARM64\n            os: ubuntu-latest\n            target: aarch64-unknown-linux-gnu\n            binary: agent-browser-linux-arm64\n            use_zigbuild: true\n          - name: Linux musl x64\n            os: ubuntu-latest\n            target: x86_64-unknown-linux-musl\n            binary: agent-browser-linux-musl-x64\n            use_zigbuild: true\n          - name: Linux musl ARM64\n            os: ubuntu-latest\n            target: aarch64-unknown-linux-musl\n            binary: agent-browser-linux-musl-arm64\n            use_zigbuild: true\n          - name: Windows x64\n            os: ubuntu-latest\n            target: x86_64-pc-windows-gnu\n            binary: agent-browser-win32-x64.exe\n            use_zigbuild: false\n          - name: macOS x64\n            os: macos-latest\n            target: x86_64-apple-darwin\n            binary: agent-browser-darwin-x64\n            use_zigbuild: false\n          - name: macOS ARM64\n            os: macos-latest\n            target: aarch64-apple-darwin\n            binary: agent-browser-darwin-arm64\n            use_zigbuild: false\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '22'\n          cache: pnpm\n\n      - name: Install npm dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Sync version\n        run: pnpm run version:sync\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.target }}\n\n      - name: Install cross-compilation tools (Linux)\n        if: runner.os == 'Linux'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu mingw-w64\n\n      - name: Install cargo-zigbuild\n        if: matrix.use_zigbuild\n        run: |\n          pip3 install ziglang\n          cargo install cargo-zigbuild\n\n      - name: Configure Rust linkers\n        if: runner.os == 'Linux'\n        run: |\n          mkdir -p ~/.cargo\n          cat >> ~/.cargo/config.toml << 'EOF'\n          [target.aarch64-unknown-linux-gnu]\n          linker = \"aarch64-linux-gnu-gcc\"\n\n          [target.x86_64-pc-windows-gnu]\n          linker = \"x86_64-w64-mingw32-gcc\"\n          EOF\n\n      - name: Cache Rust build artifacts\n        uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: cli\n\n      - name: Build with zigbuild\n        if: matrix.use_zigbuild\n        run: cargo zigbuild --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}\n\n      - name: Build with cargo\n        if: '!matrix.use_zigbuild'\n        run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}\n\n      - name: Copy binary\n        run: |\n          mkdir -p artifacts\n          if [[ \"${{ matrix.target }}\" == *\"windows\"* ]]; then\n            cp cli/target/${{ matrix.target }}/release/agent-browser.exe artifacts/${{ matrix.binary }}\n          else\n            cp cli/target/${{ matrix.target }}/release/agent-browser artifacts/${{ matrix.binary }}\n            chmod +x artifacts/${{ matrix.binary }}\n          fi\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ matrix.binary }}\n          path: artifacts/${{ matrix.binary }}\n          retention-days: 7\n\n  # Create release PR or publish to npm (with binaries)\n  release:\n    name: Release\n    needs: build-binaries\n    runs-on: ubuntu-latest\n    outputs:\n      published: ${{ steps.changesets.outputs.published }}\n      publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}\n    steps:\n      - name: Checkout Repo\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '22'\n          cache: pnpm\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Install Dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Download all binary artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts/\n\n      - name: Move binaries to bin directory\n        run: |\n          mkdir -p bin\n          find artifacts -type f -name 'agent-browser-*' -exec mv {} bin/ \\;\n          rm -rf artifacts\n          chmod +x bin/agent-browser-* 2>/dev/null || true\n          echo \"Binaries in bin/:\"\n          ls -la bin/\n\n      - name: Verify all binaries exist\n        run: |\n          EXPECTED_BINARIES=(\n            \"agent-browser-linux-x64\"\n            \"agent-browser-linux-arm64\"\n            \"agent-browser-linux-musl-x64\"\n            \"agent-browser-linux-musl-arm64\"\n            \"agent-browser-win32-x64.exe\"\n            \"agent-browser-darwin-x64\"\n            \"agent-browser-darwin-arm64\"\n          )\n          MIN_SIZE=100000  # Binaries should be at least 100KB\n          ERRORS=0\n          for binary in \"${EXPECTED_BINARIES[@]}\"; do\n            if [ ! -f \"bin/$binary\" ]; then\n              echo \"ERROR: Missing bin/$binary\"\n              ERRORS=$((ERRORS + 1))\n            else\n              SIZE=$(stat -c%s \"bin/$binary\" 2>/dev/null || stat -f%z \"bin/$binary\")\n              if [ \"$SIZE\" -lt \"$MIN_SIZE\" ]; then\n                echo \"ERROR: bin/$binary is too small ($SIZE bytes, expected >= $MIN_SIZE)\"\n                ERRORS=$((ERRORS + 1))\n              else\n                echo \"OK: bin/$binary ($SIZE bytes)\"\n              fi\n            fi\n          done\n          if [ \"$ERRORS\" -gt 0 ]; then\n            echo \"Error: $ERRORS binary issues found\"\n            exit 1\n          fi\n          echo \"All 7 platform binaries present and valid\"\n\n      - name: Create Release Pull Request or Publish to npm\n        id: changesets\n        uses: changesets/action@v1\n        with:\n          version: pnpm ci:version\n          publish: pnpm ci:publish\n          title: 'chore: version packages'\n          commit: 'chore: version packages'\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}\n\n  # Create GitHub release with binaries after npm publish\n  github-release:\n    name: Create GitHub Release\n    needs: release\n    if: needs.release.outputs.published == 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repo\n        uses: actions/checkout@v4\n        with:\n          ref: main\n\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts/\n\n      - name: Move binaries to bin directory\n        run: |\n          mkdir -p bin\n          find artifacts -type f -name 'agent-browser-*' -exec mv {} bin/ \\;\n          rm -rf artifacts\n          chmod +x bin/agent-browser-* 2>/dev/null || true\n          ls -la bin/\n\n      - name: Verify binaries exist\n        run: |\n          BINARY_COUNT=$(ls bin/agent-browser-* 2>/dev/null | wc -l)\n          if [ \"$BINARY_COUNT\" -lt 7 ]; then\n            echo \"Error: Expected 7 binaries, found $BINARY_COUNT\"\n            ls -la bin/\n            exit 1\n          fi\n          echo \"Found $BINARY_COUNT binaries\"\n\n      - name: Create GitHub Release\n        run: |\n          VERSION=$(node -p \"require('./package.json').version\")\n          TAG=\"v$VERSION\"\n          \n          # Check if release already exists\n          if gh release view \"$TAG\" &>/dev/null; then\n            echo \"Release $TAG already exists, uploading binaries...\"\n            gh release upload \"$TAG\" bin/agent-browser-* --clobber\n          else\n            echo \"Creating release $TAG...\"\n            gh release create \"$TAG\" \\\n              --title \"$TAG\" \\\n              --generate-notes \\\n              bin/agent-browser-*\n          fi\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Dependencies\nnode_modules/\n\n# Build output\ndist/\n\n# Native binaries (keep the launcher scripts)\nbin/agent-browser-*\n!bin/agent-browser\n!bin/agent-browser.cmd\n\n# Rust build artifacts\ncli/target/\ncli/*.o\n\n# Logs\n*.log\nnpm-debug.log*\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n\n# OS\n.DS_Store\nThumbs.db\n\n# Python\n__pycache__/\n\n# Test artifacts\n*.png\n*.jpeg\n*.jpg\n*.webm\ntest/e2e/.dogfood-output/\n\n# Package manager\npackage-lock.json\nyarn.lock\n\n# Environment\n.env\n.env.local\n\n# opensrc - source code for packages\nopensrc/\n\n# Docs site\ndocs/node_modules/\ndocs/.next/\ndocs/out/\ndocs/package-lock.json\n\n# pnpm\n.pnpm-store/\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "node scripts/sync-version.js\ngit add cli/Cargo.toml cli/Cargo.lock\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"printWidth\": 100,\n  \"tabWidth\": 2\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\nInstructions for AI coding agents working with this codebase.\n\n## Package Manager\n\nThis project uses **pnpm**. Always use `pnpm` instead of `npm` or `yarn` for installing dependencies, running scripts, etc. (e.g., `pnpm install`, `pnpm run build`).\n\n## Code Style\n\n- Do not use emojis in code, output, or documentation. Unicode symbols (✓, ✗, →, ⚠) are acceptable.\n- CLI colored output uses `cli/src/color.rs`. This module respects the `NO_COLOR` environment variable. Never use hardcoded ANSI color codes.\n- CLI flags must always use kebab-case (e.g., `--auto-connect`, `--allow-file-access`). Never use camelCase for flags (e.g., `--autoConnect` is wrong).\n\n## Documentation\n\nWhen adding or changing user-facing features (new flags, commands, behaviors, environment variables, etc.), update **all** of the following:\n\n1. `cli/src/output.rs` -- `--help` output (flags list, examples, environment variables)\n2. `README.md` -- Options table, relevant feature sections, examples\n3. `skills/agent-browser/SKILL.md` -- so AI agents know about the feature\n4. `docs/src/app/` -- the Next.js docs site (MDX pages)\n5. Inline doc comments in the relevant source files\n\nThis applies to changes that either human users or AI agents would need to know about. Do not skip any of these locations.\n\nIn the `docs/src/app/` MDX files, always use HTML `<table>` syntax for tables (not markdown pipe tables). This matches the existing convention across the docs site.\n\n## Architecture\n\nThis is a Rust codebase. The browser automation daemon lives in `cli/src/native/` (daemon, actions, browser, CDP client, snapshot, state). The `--engine` flag selects Chrome vs Lightpanda. The `install` command downloads Chrome from Chrome for Testing directly.\n\n## Testing\n\n### Unit Tests\n\n```bash\ncd cli && cargo test\n```\n\nRuns all unit tests (~320 tests). These are fast and don't require Chrome.\n\n### End-to-End Tests\n\n```bash\ncd cli && cargo test e2e -- --ignored --test-threads=1\n```\n\nRuns 18 e2e tests that launch real headless Chrome instances and exercise the full native daemon command pipeline. Requirements:\n\n- Chrome must be installed\n- Must run serially (`--test-threads=1`) to avoid Chrome instance contention\n- Tests are `#[ignore]`'d so they don't run during normal `cargo test`\n\nThe e2e tests live in `cli/src/native/e2e_tests.rs` and cover: launch/close, navigation, snapshots, screenshots, form interaction, cookies, storage, tabs, element queries, viewport/emulation, domain filtering, diff, state management, error handling, and Phase 8 commands.\n\n### Linting and Formatting\n\n```bash\ncd cli && cargo fmt -- --check   # Check formatting\ncd cli && cargo clippy            # Lint\n```\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# agent-browser\n\n## 0.21.2\n\n### Patch Changes\n\n- 757626f: ### Bug Fixes\n\n  - **Deduplicate text content in snapshots** - Fixed an issue where duplicate text content appeared in page snapshots (#909)\n  - **Native mouse drag state** - Fixed incorrect raw native mouse drag state not being properly tracked across `down`, `move`, and `up` events (#872)\n  - **Chrome headless launch failures** - Fixed browser launch failures caused by the `--enable-unsafe-swiftshader` flag in Chrome headless mode (#915)\n  - **Origin-scoped `--headers` persistence** - Restored correct persistence of origin-scoped headers set via `--headers` across navigation commands (#894)\n  - **Relative URLs in WebSocket domain filter** - Fixed handling of relative URLs in the WebSocket domain filter script (#624)\n\n## 0.21.1\n\n### Patch Changes\n\n- 1e7619d: ### New Features\n\n  - **HAR 1.2 network capture** - Added commands to capture and export network traffic in HAR 1.2 format, including accurate request/response timing, headers, body sizes, and resource types sourced from Chrome DevTools Protocol events (#864)\n  - **Built-in `upgrade` command** - Added `agent-browser upgrade` to self-update the CLI; automatically detects your installation method (npm, Homebrew, or Cargo) and runs the appropriate update command (#898)\n\n  ### Documentation\n\n  - Added `upgrade` command to the README command reference and installation guide\n  - Added a dedicated **Updating** section to the README with usage instructions for `agent-browser upgrade`\n\n## 0.21.0\n\n### Minor Changes\n\n- c6de80b: ### New Features\n\n  - **`batch` command** -- Execute multiple commands from stdin in a single invocation. Accepts a JSON array of string arrays and returns results sequentially. Supports `--bail` to stop on first error and `--json` for structured output (#865)\n  - **iframe support** -- CLI interactions and snapshots now traverse into iframe content, enabling automation of cross-frame pages (#869)\n  - **`network har start/stop` command** -- Capture and export network traffic in HAR 1.2 format (#874)\n  - **WebSocket fallback for CDP discovery** -- When HTTP-based CDP endpoint discovery fails, the CLI now falls back to a WebSocket connection automatically (#873)\n\n  ### Improvements\n\n  - **`--full`/`-f` refactored to command-level flag** -- Moved from a global flag to a per-command flag for clearer scoping (#877)\n  - **Enhanced Chrome launch** -- Added `--user-data-dir` support and configurable launch timeout for more reliable browser startup (#852)\n\n  ### Bug Fixes\n\n  - Fixed `/json/list` fallback when `/json/version` endpoint is unavailable, improving compatibility with non-standard CDP implementations (#861)\n  - Fixed daemon liveness detection for PID namespace isolation (e.g. `unshare`). Uses socket connectivity as the sole liveness check instead of `kill(pid, 0)`, which fails when the caller cannot see the daemon's PID (#879)\n  - Fixed Ubuntu dependency install accidentally removing system packages (#884)\n\n## 0.20.14\n\n### Patch Changes\n\n- c0d4cf6: ### New Features\n\n  - **Idle timeout for daemon auto-shutdown** - Added `--idle-timeout` CLI flag (and `AGENT_BROWSER_IDLE_TIMEOUT_MS` environment variable) to automatically shut down the daemon after a period of inactivity. Accepts human-friendly formats such as `10s`, `3m`, `1h`, or raw milliseconds (#856)\n  - **Cursor-interactive elements in snapshot tree** - Cursor-interactive elements are now embedded directly into the snapshot tree for richer context (#855)\n\n  ### Bug Fixes\n\n  - Fixed **remote host support** in CDP discovery, enabling connection to browsers running on non-local hosts (#854)\n  - Fixed **CDP flag propagation** to the daemon process, ensuring reliable CDP reconnection across sessions (#857)\n  - Fixed **Windows auto-connect profiling** to correctly handle browser connection on Windows (#835, #840)\n  - Fixed **Windows transient error detection** by recognising Windows-specific socket error codes (`os error 10061` connection refused, `os error 10054` connection reset) during daemon reconnection attempts\n\n## 0.20.13\n\n### Patch Changes\n\n- eda956b: ### Bug Fixes\n\n  - **Network idle detection for cached pages** - Fixed an issue where `poll_network_idle` could return immediately when no network events were observed (e.g. pages served from cache). The idle timer is now only satisfied after a consistent **500 ms idle period** has elapsed, preventing false-positive idle detection. The core polling logic has also been extracted into a standalone `poll_network_idle` function to improve testability (#847)\n\n## 0.20.12\n\n### Patch Changes\n\n- 5fa2396: ### Bug Fixes\n\n  - Fixed **`snapshot -C`** and **`screenshot --annotate`** hanging when connected over WSS (WebSocket Secure) due to sequential CDP round-trips per interactive element (#842)\n\n  ### Performance\n\n  - **`snapshot -C` (cursor-interactive mode)** now batches CDP calls instead of issuing N×2 sequential round-trips per cursor-interactive element, preventing timeouts on high-latency WSS connections (#842)\n  - **`screenshot --annotate`** now batches element queries, reducing completion time from potentially 20–40s (e.g. 50+ buttons over WSS) to within expected bounds (#842)\n\n## 0.20.11\n\n### Patch Changes\n\n- 4b5fc78: ### Bug Fixes\n\n  - **Material Design checkbox/radio parity** - Restored Playwright-parity behavior for `check`/`uncheck` actions on Material Design controls. These components hide the native `<input>` off-screen and use overlay elements that intercept coordinate-based clicks; the actions now detect this pattern and fall back to a JS `.click()` to correctly toggle state. Also improves `ischecked` to handle nested hidden inputs and ARIA-only checkboxes (#837)\n  - **Punctuation handling in `type` command** - Fixed incorrect virtual key (VK) codes being used for punctuation characters (e.g. `.`, `@`) in the `type` action, which previously caused those characters to be dropped or mistyped (#836)\n\n## 0.20.10\n\n### Patch Changes\n\n- a3d9662: ### Bug Fixes\n\n  - **Restored WebSocket streaming** - Fixed broken WebSocket streaming in the native daemon by keeping the **StreamServer** instance alive so the broadcast channel remains open, and ensuring CDP session IDs and connection status are correctly propagated to stream clients (#826)\n  - **Filtered internal Chrome targets** - Fixed auto-connect discovery incorrectly attempting to attach to Chrome-internal pages (e.g. `chrome://`, `chrome-extension://`, `devtools://` URLs), which could cause unexpected connection failures (#827)\n\n## 0.20.9\n\n### Patch Changes\n\n- 51d9ab4: ### Bug Fixes\n\n  - **Appium v3 iOS capabilities** - Added `appium:` vendor prefix to iOS capabilities (e.g., `appium:automationName`, `appium:deviceName`, `appium:platformVersion`) to comply with the Appium v3 WebDriver protocol requirements (#810)\n  - **Snapshot `--selector` scoping** - Fixed `snapshot --selector` so that the output is properly scoped to the matched element's subtree rather than returning the full accessibility tree. The selector now resolves the target DOM node's backend IDs and filters the accessibility tree to only include nodes within that subtree (#825)\n\n## 0.20.8\n\n### Patch Changes\n\n- daf7263: ### Bug Fixes\n\n  - Fixed **video duration** being reported incorrectly when using real-time ffmpeg encoding for screen recording (#812)\n  - Removed obsolete **`BrowserManager` TypeScript API** references that no longer reflect the current CLI-based usage model (#821)\n\n  ### Documentation\n\n  - Updated README to replace outdated **`BrowserManager` programmatic API** examples with the current CLI-based approach using `execSync` and `agent-browser` commands (#821)\n  - Removed the **Programmatic API** section covering `BrowserManager` screencast and input injection methods, which are no longer part of the public API (#821)\n\n## 0.20.7\n\n### Patch Changes\n\n- 25a1526: ### New Features\n\n  - **Brave Browser support** - Added auto-discovery of Brave Browser for CDP connections on macOS, Linux, and Windows. The agent will now automatically detect and connect to Brave alongside Chrome, Chromium, and Canary installations (#817)\n\n  ### Improvements\n\n  - **Postinstall message** - The post-install message now detects existing Chrome installations on the system. If a compatible browser is found, it confirms the path and notes it will be used automatically instead of prompting an install. If no browser is detected, the warning is clearer and mentions that installation can be skipped when using `--cdp`, `--provider`, `--engine`, or `--executable-path` (#815)\n\n## 0.20.6\n\n### Patch Changes\n\n- fa91c22: ### Bug Fixes\n\n  - **Stale accessibility tree reference fallback** - Fixed an issue where interacting with an element whose **`backend_node_id`** had become stale (e.g. after the DOM was replaced) would fail with a `Could not compute box model` CDP error. Element resolution now re-queries the accessibility tree using role/name lookup to obtain a fresh node ID before retrying the operation (#806)\n\n## 0.20.5\n\n### Patch Changes\n\n- fc091d2: ### Bug Fixes\n\n  - **Daemon panic on broken stderr pipe** - Replaced all `eprintln!` calls with `writeln!(std::io::stderr(), ...)` wrapped in `let _ =` to silently discard write errors, preventing the daemon from panicking when the parent process drops the stderr pipe during Chrome launch (#802)\n\n## 0.20.4\n\n### Patch Changes\n\n- e2ebde2: ### Bug Fixes\n\n  - **Broadcast channel lag handling** - Fixed an issue where **broadcast channel lag** errors were incorrectly treated as stream closure, causing premature termination of event listeners in reload, response body, download, and navigation wait operations. Lagged messages are now skipped and the loop continues instead of breaking (#797)\n\n  ### Improvements\n\n  - Removed unused **pnpm setup** steps from the `global-install` CI job, simplifying the workflow configuration (#798)\n\n## 0.20.3\n\n### Patch Changes\n\n- e365909: ### Bug Fixes\n\n  - **Chrome launch retry** - Chrome will now retry launching up to 3 times with a 500ms delay between attempts, improving resilience against transient startup failures (#791)\n  - **Remote CDP snapshot hang** - Resolved an issue where snapshots would hang indefinitely over remote CDP (WSS) connections by removing WebSocket message and frame size limits to accommodate large responses (e.g. `Accessibility.getFullAXTree`), accepting binary frames from remote proxies such as Browserless, and immediately clearing pending commands when the connection closes rather than waiting for the 30-second timeout (#792)\n\n## 0.20.2\n\n### Patch Changes\n\n- 944fa01: ### New Features\n\n  - **Linux musl (Alpine) builds** - Added pre-built binaries for **linux-musl** targeting both **x64** and **arm64** architectures, enabling native support for Alpine Linux and other musl-based distributions without requiring glibc (#784)\n\n  ### Improvements\n\n  - **Consecutive `--auto-connect` commands** - Added support for issuing multiple consecutive `--auto-connect` commands without requiring a full browser relaunch; external connections are now correctly identified and reused (#786)\n  - **External browser disconnect behavior** - When using `--auto-connect` or `--cdp`, closing the agent session now disconnects cleanly without shutting down the user's browser process\n\n  ### Bug Fixes\n\n  - **Restored `refs` dict in `--json` snapshot output** - The `refs` map containing role and name metadata for referenced elements is now correctly included in JSON snapshot responses (#787)\n  - Fixed e2e test assertions for `diff_snapshot` and `domain_filter` to correctly reflect expected behavior (#783)\n  - Fixed Chrome temp-dir cleanup test failing on Windows (#766)\n\n## 0.20.1\n\n### Patch Changes\n\n- bd05917: ### Bug Fixes\n\n  - Fixed **AX tree deserialization** to accept integer `nodeId` and `childIds` values for compatibility with Lightpanda, which sends numeric IDs where Chrome sends strings (#775)\n  - Fixed **misleading SIGPIPE comment** to accurately describe the default Rust SIGPIPE behavior and why it is reset to `SIG_DFL` (#776)\n  - Fixed **WebM recording output** to use the VP9 codec (`libvpx-vp9`) instead of H.264, producing valid WebM files; also adds a padding filter to ensure even frame dimensions (#779)\n\n## 0.20.0\n\n### Minor Changes\n\n- 235fa88: ### Full Native Rust\n\n  - **100% native Rust** -- Removed the entire Node.js/Playwright daemon. The Rust native daemon is now the only implementation. No Node.js runtime or Playwright dependency required. (#754)\n  - **99x smaller install** -- Install size reduced from 710 MB to 7 MB by eliminating the Node.js dependency tree.\n  - **18x less memory** -- Daemon memory usage reduced from 143 MB to 8 MB.\n  - **1.6x faster cold start** -- Cold start time reduced from 1002ms to 617ms.\n  - **Benchmarks** -- Added benchmark suite comparing native vs Node.js daemon performance.\n  - **Chromium installer hardened** -- Fixed zip path traversal vulnerability in Chrome for Testing installer.\n\n  ### Bug Fixes\n\n  - Fixed `--headed false` flag not being respected in CLI (#757)\n  - Fixed \"not found\" error pattern in `to_ai_friendly_error` incorrectly catching non-element errors (#759)\n  - Fixed storage local key lookup parsing and text output (#761)\n  - Fixed Lightpanda engine launch with release binaries (#760)\n  - Hardened Lightpanda startup timeouts (#762)\n\n## 0.19.0\n\n### Minor Changes\n\n- 56bb92b: ### New Features\n\n  - **Browserless.io provider** -- Added browserless.io as a browser provider, supported in both Node.js and native daemon paths. Connect to remote Browserless instances with `--provider browserless` or `AGENT_BROWSER_PROVIDER=browserless`. Configurable via `BROWSERLESS_API_KEY`, `BROWSERLESS_API_URL`, and `BROWSERLESS_BROWSER_TYPE` environment variables. (#502, #746)\n  - **`clipboard` command** -- Read from and write to the browser clipboard. Supports `read`, `write <text>`, `copy` (simulates Ctrl+C), and `paste` (simulates Ctrl+V) operations. (#749)\n  - **Screenshot output configuration** -- New global flags `--screenshot-dir`, `--screenshot-quality`, `--screenshot-format` and corresponding `AGENT_BROWSER_SCREENSHOT_DIR`, `AGENT_BROWSER_SCREENSHOT_QUALITY`, `AGENT_BROWSER_SCREENSHOT_FORMAT` environment variables for persistent screenshot settings. (#749)\n\n  ### Bug Fixes\n\n  - Fixed `wait --text` not working in native daemon path (#749)\n  - Fixed `BrowserManager.navigate()` and package entry point (#748)\n  - Fixed extensions not being loaded from `config.json` (#750)\n  - Fixed scroll on page load (#747)\n  - Fixed HTML retrieval by using `browser.getLocator()` for selector operations (#745)\n\n## 0.18.0\n\n### Minor Changes\n\n- 942b8cd: ### New Features\n\n  - **`inspect` command** - Opens Chrome DevTools for the active page by launching a local proxy server that forwards the DevTools frontend to the browser's CDP WebSocket. Commands continue to work while DevTools is open. Implemented in both Node.js and native paths. (#736)\n  - **`get cdp-url` subcommand** - Retrieve the Chrome DevTools Protocol WebSocket URL for the active page, useful for external debugging tools. (#736)\n  - **Native screenshot annotate** - The `--annotate` flag for screenshots now works in the native Rust daemon, bringing parity with the Node.js path. (#706)\n\n  ### Improvements\n\n  - **KERNEL_API_KEY now optional** - External credential injection no longer requires `KERNEL_API_KEY` to be set, making it easier to use Kernel with pre-configured environments. (#687)\n  - **Browserbase simplified** - Removed the `BROWSERBASE_PROJECT_ID` requirement, reducing setup friction for Browserbase users. (#625)\n\n  ### Bug Fixes\n\n  - Fixed Browserbase API using incorrect endpoint to release sessions (#707)\n  - Fixed CDP connect paths using hardcoded 10s timeout instead of `getDefaultTimeout()` (#704)\n  - Fixed lone Unicode surrogates causing errors by sanitizing with `toWellFormed()` (#720)\n  - Fixed CDP connection failure on IPv6-first systems (#717)\n  - Fixed recordings not inheriting the current viewport settings (#718)\n\n## 0.17.1\n\n### Patch Changes\n\n- 94cd888: Added support for device scale factor (retina display) in the viewport command via an optional scale parameter. Also added webview target type support for better Electron application compatibility, and the pages list now includes target type information.\n\n## 0.17.0\n\n### Minor Changes\n\n- 94521e7: ### New Features\n\n  - **Lightpanda browser engine support** - Added `--engine <name>` flag to select the browser engine (`chrome` by default, or `lightpanda`), implying `--native` mode. Configurable via `AGENT_BROWSER_ENGINE` environment variable (#646)\n  - **Dialog dismiss command** - Added support for `dismiss` subcommand in dialog command parsing (#605)\n\n  ### Improvements\n\n  - **Daemon startup error reporting** - Daemon startup errors are now surfaced directly instead of showing an opaque timeout message (#614)\n  - **CDP port discovery** - Replaced broken hand-rolled HTTP client with `reqwest` for more reliable CDP port discovery (#619)\n  - **Chrome extensions** - Extensions now load correctly by forcing headed mode when extensions are present (#652)\n  - **Google Translate bar suppression** - Suppressed the Google Translate bar in native headless mode to avoid interference (#649)\n  - **Auth cookie persistence** - Auth cookies are now persisted on browser close in native mode (#650)\n\n  ### Bug Fixes\n\n  - Fixed native auth login failing due to incompatible encryption format (#648)\n\n  ### Documentation\n\n  - Improved snapshot usage guidance and added reproducibility check (#630)\n  - Added `--engine` flag to the README options table\n\n  ### Performance\n\n  - Added benchmarks to the CLI codebase (#637)\n\n## 0.16.3\n\n### Patch Changes\n\n- 7d2c895: Fixed an issue where the --native flag was being passed to child processes even when not explicitly specified on the command line. The flag is now only forwarded when the user explicitly provides it, consistent with how other CLI flags like --allow-file-access and --download-path are handled.\n\n## 0.16.2\n\n### Patch Changes\n\n- 01ac557: Added AGENT_BROWSER_HEADED environment variable support for running the browser in headed mode, and improved temporary profile cleanup when launching Chrome directly. Also includes documentation clarification that browser extensions work in both headed and headless modes.\n\n## 0.16.1\n\n### Patch Changes\n\n- c4180c8: Improved Chrome launch reliability by automatically detecting containerized environments (Docker, Podman, Kubernetes) and enabling --no-sandbox when needed. Added support for discovering Playwright-installed Chromium browsers and enhanced error messages with helpful diagnostics when Chrome fails to launch.\n\n## 0.16.0\n\n### Minor Changes\n\n- 05018b3: Added experimental native Rust daemon (`--native` flag, `AGENT_BROWSER_NATIVE=1` env, or `\"native\": true` in config). The native daemon communicates with Chrome directly via CDP, eliminating Node.js and Playwright dependencies. Supports 150+ commands with full parity to the default Node.js daemon. Includes WebDriver backend for Safari/iOS, CDP protocol codegen, request tracking, frame context management, and comprehensive e2e and parity tests.\n\n## 0.15.3\n\n### Patch Changes\n\n- 62241b5: Fixed Windows compatibility issues including proper handling of extended-length path prefixes from canonicalize(), prevention of MSYS/Git Bash path translation that could mangle arguments, and improved daemon startup reliability. Also added ARM64 Windows support in postinstall shims and expanded CI testing with a full daemon lifecycle test on Windows.\n\n## 0.15.2\n\n### Patch Changes\n\n- 6aea316: Documentation site improvements and internal tooling updates including enhanced code blocks, mobile navigation, and docs chat components. CLI connection and output handling refinements. Skill creator reference documentation and scripts have been reorganized.\n\n## 0.15.1\n\n### Patch Changes\n\n- 7bd8ce9: Added support for chrome:// and chrome-extension:// URLs in navigation and recording commands. These special browser URLs are now preserved as-is instead of having https:// incorrectly prepended.\n\n## 0.15.0\n\n### Minor Changes\n\n- 2e38882: - Added security hardening: authentication vault, content boundary markers, domain allowlist, action policy, action confirmation, and output length limits.\n  - Added `--download-path` flag (and `AGENT_BROWSER_DOWNLOAD_PATH` env / `downloadPath` config key) to set a default download directory.\n  - Added `--selector` flag to `scroll` command for scrolling within specific container elements.\n\n## 0.14.0\n\n### Minor Changes\n\n- b7665e5: - Added `keyboard` command for raw keyboard input -- type with real keystrokes, insert text, and press shortcuts at the currently focused element without needing a selector.\n  - Added `--color-scheme` flag and `AGENT_BROWSER_COLOR_SCHEME` env var for persistent dark/light mode preference across browser sessions.\n  - Fixed IPC EAGAIN errors (os error 35/11) by adding backpressure-aware socket writes, command serialization, and lowering the default Playwright timeout to 25s (configurable via `AGENT_BROWSER_DEFAULT_TIMEOUT`).\n  - Fixed remote debugging (CDP) reconnection.\n  - Fixed state load failing when no browser is running.\n  - Fixed `--annotate` flag warning appearing when not explicitly passed via CLI.\n\n## 0.13.0\n\n### Minor Changes\n\n- ebd8717: Added new diff commands for comparing snapshots, screenshots, and URLs between page states. You can now run visual pixel diffs against baseline images, compare accessibility tree snapshots with customizable depth and selectors, and diff two URLs side-by-side with optional screenshot comparison.\n\n## 0.12.0\n\n### Minor Changes\n\n- 69ffad0: Add annotated screenshots with the new --annotate flag, which overlays numbered labels on interactive elements and prints a legend mapping each label to its element ref. This enables multimodal AI models to reason about visual layout while using the same @eN refs for subsequent interactions. The flag can also be set via the AGENT_BROWSER_ANNOTATE environment variable.\n\n## 0.11.1\n\n### Patch Changes\n\n- c6fc7df: Added documentation for command chaining with && across README, CLI help output, docs, and skill files, explaining how to efficiently chain multiple agent-browser commands in a single shell invocation since the browser persists via a background daemon.\n\n## 0.11.0\n\n### Minor Changes\n\n- 5dc40b4: Added configuration file support with automatic loading from user and project directories, new profiler commands for Chrome DevTools profiling, computed styles getter, browser extension loading, storage state management, and iOS device emulation. Expanded click command with new-tab option, improved find command with additional actions and filtering options, and enhanced CDP connection to accept WebSocket URLs. Documentation has been significantly expanded with new sections for configuration, profiling, and proxy support.\n\n## 0.10.0\n\n### Minor Changes\n\n- 1112a16: Added session persistence with automatic save/restore of cookies and localStorage across browser restarts using --session-name flag, with optional AES-256-GCM encryption for saved state data. New state management commands allow listing, showing, renaming, clearing, and cleaning up old session files. Also added --new-tab option for click commands to open links in new tabs.\n\n## 0.9.4\n\n### Patch Changes\n\n- 323b6cd: Fix all Clippy lint warnings in the Rust CLI: remove redundant import, use `.first()` instead of `.get(0)`, use `.copied()` instead of `.map(|s| *s)`, use `.contains()` instead of `.iter().any()`, use `then_some` instead of lazy `then`, and simplify redundant match guards.\n\n## 0.9.3\n\n### Patch Changes\n\n- d03e238: Added support for custom executable path in CLI browser launch options. Documentation site received UI improvements including a new chat component with sheet-based interface and updated dependencies.\n\n## 0.9.2\n\n### Patch Changes\n\n- 76d23db: Documentation site migrated to MDX for improved content authoring, added AI-powered docs chat feature, and updated README with Homebrew installation instructions for macOS users.\n\n## 0.9.1\n\n### Patch Changes\n\n- ae34945: Added --allow-file-access flag to enable opening and interacting with local file:// URLs (PDFs, HTML files) by passing Chromium flags that allow JavaScript access to local files. Added -C/--cursor flag for snapshots to include cursor-interactive elements like divs with onclick handlers or cursor:pointer styles, which is useful for modern web apps using custom clickable elements.\n\n## 0.9.0\n\n### Minor Changes\n\n- 9d021bd: Add iOS Simulator and real device support for mobile Safari testing via Appium. New CLI commands include `device list` to show available simulators, `tap` and `swipe` for touch interactions, and the `--device` flag to specify which iOS device to use. Configure with `-p ios` provider flag or `AGENT_BROWSER_PROVIDER=ios` environment variable.\n\n## 0.8.10\n\n### Patch Changes\n\n- 17dba8f: Add --stdin flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts\n- daeede4: Add --stdin flag for the eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts. Also fix binary permission issues on macOS/Linux when postinstall scripts don't run (e.g., with bun).\n\n## 0.8.9\n\n### Patch Changes\n\n- 0dc36f2: Add --stdin flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts\n\n## 0.8.8\n\n### Patch Changes\n\n- 2771588: Added base64 encoding support for the eval command with -b/--base64 flag to avoid shell escaping issues when executing JavaScript. Updated documentation with AI agent setup instructions and reorganized the docs structure by consolidating agent mode content into the installation page.\n\n## 0.8.7\n\n### Patch Changes\n\n- d24f753: Fixed browser launch options not being passed correctly when using persistent profiles, ensuring args, userAgent, proxy, and ignoreHTTPSErrors settings now work properly. Added pre-flight checks for socket path length limits and directory write permissions to provide clearer error messages when daemon startup fails. Improved error handling to properly exit with failure status when browser launch fails.\n\n## 0.8.6\n\n### Patch Changes\n\n- d75350a: Improved daemon connection reliability by adding automatic retry logic for transient errors like connection resets, broken pipes, and temporary resource unavailability. The CLI now cleans up stale socket and PID files before starting a new daemon, and includes better detection of daemon responsiveness to handle race conditions during shutdown.\n\n## 0.8.5\n\n### Patch Changes\n\n- cb2f8c3: Fixed version synchronization to automatically update Cargo.lock alongside Cargo.toml during releases, and made the CLI binary executable. This ensures the Rust CLI version stays in sync with the npm package version.\n\n## 0.8.4\n\n### Patch Changes\n\n- 759302e: Fixed \"Daemon not found\" error when running through AI agents (e.g., Claude Code) by resolving symlinks in the executable path. Previously, npm global bin symlinks weren't being resolved correctly, causing intermittent daemon discovery failures.\n\n## 0.8.3\n\n### Patch Changes\n\n- 4116a8a: Replaced shell-based CLI wrappers with a cross-platform Node.js wrapper to enable npx support on Windows. Added postinstall logic to patch npm's bin entry on global installs, allowing the native binary to be invoked directly with zero overhead. Added CI tests to verify global installation works correctly across all platforms.\n\n## 0.8.2\n\n### Patch Changes\n\n- 7e6336f: Fixed the Windows CMD wrapper to use the native binary directly instead of routing through Node.js, improving startup performance and reliability. Added retry logic to the CI install command to handle transient failures during browser installation.\n\n## 0.8.1\n\n### Patch Changes\n\n- 8eec634: Improved release workflow to validate binary file sizes and ensure binaries are executable after npm install. Updated documentation site with a new mobile navigation system and added v0.8.0 changelog entries. Reformatted CHANGELOG.md for better readability.\n\n## v0.8.0\n\n### New Features\n\n- **Kernel cloud browser provider** - Connect to Kernel (https://kernel.sh) for remote browser infrastructure via `-p kernel` flag or `AGENT_BROWSER_PROVIDER=kernel`. Supports stealth mode, persistent profiles, and automatic profile find-or-create.\n- **Ignore HTTPS certificate errors** - New `--ignore-https-errors` flag for working with self-signed certificates and development environments\n- **Enhanced cookie management** - Extended `cookies set` command with `--url`, `--domain`, `--path`, `--httpOnly`, `--secure`, `--sameSite`, and `--expires` flags for setting cookies before page load\n\n### Bug Fixes\n\n- Fixed tab list command not recognizing new pages opened via clicks or `target=\"_blank\"` links (#275)\n- Fixed `check` command hanging indefinitely (#272)\n- Fixed `set device` not applying deviceScaleFactor - HiDPI screenshots now work correctly (#270)\n- Fixed state load and profile persistence not working in v0.7.6 (#268)\n- Screenshots now save to temp directory when no path is provided (#247)\n\n### Security\n\n- Daemon and stream server now reject cross-origin connections (#274)\n\n## 0.7.6\n\n### Patch Changes\n\n- a4d0c26: Allow null values for the screenshot selector field. Previously, passing a null selector would fail validation, but now it is properly handled as an optional value.\n\n## 0.7.5\n\n### Patch Changes\n\n- 8c2a6ec: Fix GitHub release workflow to handle existing releases. If a release already exists, binaries are uploaded to it instead of failing.\n\n## 0.7.4\n\n### Patch Changes\n\n- 957b5e5: Fix binary permissions on install. npm doesn't preserve execute bits, so postinstall now ensures the native binary is executable.\n\n## 0.7.3\n\n### Patch Changes\n\n- 161d8f5: Fix native binary distribution in npm package. Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now correctly included when publishing.\n\n## 0.7.2\n\n### Patch Changes\n\n- 6afede2: Fix native binary distribution in npm package\n\n  Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now included in the npm package. Previously, the release workflow published to npm before building binaries, causing \"No binary found\" errors on installation.\n\n## 0.7.1\n\n### Patch Changes\n\n- Fix native binary distribution in npm package. Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now included in the npm package. Previously, the release workflow published to npm before building binaries, causing \"No binary found\" errors on installation.\n\n## 0.7.0\n\n### Minor Changes\n\n- 316e649: ## New Features\n\n  - **Cloud browser providers** - Connect to Browserbase or Browser Use for remote browser infrastructure via `-p` flag or `AGENT_BROWSER_PROVIDER` env var\n  - **Persistent browser profiles** - Store cookies, localStorage, and login sessions across browser restarts with `--profile`\n  - **Remote CDP WebSocket URLs** - Connect to remote browser services via WebSocket URL (e.g., `--cdp \"wss://...\"`)\n  - **Download commands** - New `download` command and `wait --download` for file downloads with ref support\n  - **Browser launch configuration** - New `--args`, `--user-agent`, and `--proxy-bypass` flags for fine-grained browser control\n  - **Enhanced skills** - Hierarchical structure with references and templates for Claude Code\n\n  ## Bug Fixes\n\n  - Screenshot command now supports refs and has improved error messages\n  - WebSocket URLs work in `connect` command\n  - Fixed socket file location (uses `~/.agent-browser` instead of TMPDIR)\n  - Windows binary path fix (.exe extension)\n  - State load and path-based actions now show correct output messages\n\n  ## Documentation\n\n  - Added Claude Code marketplace plugin installation instructions\n  - Updated skill documentation with references and templates\n  - Improved error documentation\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1.  Definitions.\n\n    \"License\" shall mean the terms and conditions for use, reproduction,\n    and distribution as defined by Sections 1 through 9 of this document.\n\n    \"Licensor\" shall mean the copyright owner or entity authorized by\n    the copyright owner that is granting the License.\n\n    \"Legal Entity\" shall mean the union of the acting entity and all\n    other entities that control, are controlled by, or are under common\n    control with that entity. For the purposes of this definition,\n    \"control\" means (i) the power, direct or indirect, to cause the\n    direction or management of such entity, whether by contract or\n    otherwise, or (ii) ownership of fifty percent (50%) or more of the\n    outstanding shares, or (iii) beneficial ownership of such entity.\n\n    \"You\" (or \"Your\") shall mean an individual or Legal Entity\n    exercising permissions granted by this License.\n\n    \"Source\" form shall mean the preferred form for making modifications,\n    including but not limited to software source code, documentation\n    source, and configuration files.\n\n    \"Object\" form shall mean any form resulting from mechanical\n    transformation or translation of a Source form, including but\n    not limited to compiled object code, generated documentation,\n    and conversions to other media types.\n\n    \"Work\" shall mean the work of authorship, whether in Source or\n    Object form, made available under the License, as indicated by a\n    copyright notice that is included in or attached to the work\n    (an example is provided in the Appendix below).\n\n    \"Derivative Works\" shall mean any work, whether in Source or Object\n    form, that is based on (or derived from) the Work and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship. For the purposes\n    of this License, Derivative Works shall not include works that remain\n    separable from, or merely link (or bind by name) to the interfaces of,\n    the Work and Derivative Works thereof.\n\n    \"Contribution\" shall mean any work of authorship, including\n    the original version of the Work and any modifications or additions\n    to that Work or Derivative Works thereof, that is intentionally\n    submitted to Licensor for inclusion in the Work by the copyright owner\n    or by an individual or Legal Entity authorized to submit on behalf of\n    the copyright owner. For the purposes of this definition, \"submitted\"\n    means any form of electronic, verbal, or written communication sent\n    to the Licensor or its representatives, including but not limited to\n    communication on electronic mailing lists, source code control systems,\n    and issue tracking systems that are managed by, or on behalf of, the\n    Licensor for the purpose of discussing and improving the Work, but\n    excluding communication that is conspicuously marked or otherwise\n    designated in writing by the copyright owner as \"Not a Contribution.\"\n\n    \"Contributor\" shall mean Licensor and any individual or Legal Entity\n    on behalf of whom a Contribution has been received by Licensor and\n    subsequently incorporated within the Work.\n\n2.  Grant of Copyright License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    copyright license to reproduce, prepare Derivative Works of,\n    publicly display, publicly perform, sublicense, and distribute the\n    Work and such Derivative Works in Source or Object form.\n\n3.  Grant of Patent License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    (except as stated in this section) patent license to make, have made,\n    use, offer to sell, sell, import, and otherwise transfer the Work,\n    where such license applies only to those patent claims licensable\n    by such Contributor that are necessarily infringed by their\n    Contribution(s) alone or by combination of their Contribution(s)\n    with the Work to which such Contribution(s) was submitted. If You\n    institute patent litigation against any entity (including a\n    cross-claim or counterclaim in a lawsuit) alleging that the Work\n    or a Contribution incorporated within the Work constitutes direct\n    or contributory patent infringement, then any patent licenses\n    granted to You under this License for that Work shall terminate\n    as of the date such litigation is filed.\n\n4.  Redistribution. You may reproduce and distribute copies of the\n    Work or Derivative Works thereof in any medium, with or without\n    modifications, and in Source or Object form, provided that You\n    meet the following conditions:\n\n    (a) You must give any other recipients of the Work or\n    Derivative Works a copy of this License; and\n\n    (b) You must cause any modified files to carry prominent notices\n    stating that You changed the files; and\n\n    (c) You must retain, in the Source form of any Derivative Works\n    that You distribute, all copyright, patent, trademark, and\n    attribution notices from the Source form of the Work,\n    excluding those notices that do not pertain to any part of\n    the Derivative Works; and\n\n    (d) If the Work includes a \"NOTICE\" text file as part of its\n    distribution, then any Derivative Works that You distribute must\n    include a readable copy of the attribution notices contained\n    within such NOTICE file, excluding those notices that do not\n    pertain to any part of the Derivative Works, in at least one\n    of the following places: within a NOTICE text file distributed\n    as part of the Derivative Works; within the Source form or\n    documentation, if provided along with the Derivative Works; or,\n    within a display generated by the Derivative Works, if and\n    wherever such third-party notices normally appear. The contents\n    of the NOTICE file are for informational purposes only and\n    do not modify the License. You may add Your own attribution\n    notices within Derivative Works that You distribute, alongside\n    or as an addendum to the NOTICE text from the Work, provided\n    that such additional attribution notices cannot be construed\n    as modifying the License.\n\n    You may add Your own copyright statement to Your modifications and\n    may provide additional or different license terms and conditions\n    for use, reproduction, or distribution of Your modifications, or\n    for any such Derivative Works as a whole, provided Your use,\n    reproduction, and distribution of the Work otherwise complies with\n    the conditions stated in this License.\n\n5.  Submission of Contributions. Unless You explicitly state otherwise,\n    any Contribution intentionally submitted for inclusion in the Work\n    by You to the Licensor shall be under the terms and conditions of\n    this License, without any additional terms or conditions.\n    Notwithstanding the above, nothing herein shall supersede or modify\n    the terms of any separate license agreement you may have executed\n    with Licensor regarding such Contributions.\n\n6.  Trademarks. This License does not grant permission to use the trade\n    names, trademarks, service marks, or product names of the Licensor,\n    except as required for reasonable and customary use in describing the\n    origin of the Work and reproducing the content of the NOTICE file.\n\n7.  Disclaimer of Warranty. Unless required by applicable law or\n    agreed to in writing, Licensor provides the Work (and each\n    Contributor provides its Contributions) on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n    implied, including, without limitation, any warranties or conditions\n    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n    PARTICULAR PURPOSE. You are solely responsible for determining the\n    appropriateness of using or redistributing the Work and assume any\n    risks associated with Your exercise of permissions under this License.\n\n8.  Limitation of Liability. In no event and under no legal theory,\n    whether in tort (including negligence), contract, or otherwise,\n    unless required by applicable law (such as deliberate and grossly\n    negligent acts) or agreed to in writing, shall any Contributor be\n    liable to You for damages, including any direct, indirect, special,\n    incidental, or consequential damages of any character arising as a\n    result of this License or out of the use or inability to use the\n    Work (including but not limited to damages for loss of goodwill,\n    work stoppage, computer failure or malfunction, or any and all\n    other commercial damages or losses), even if such Contributor\n    has been advised of the possibility of such damages.\n\n9.  Accepting Warranty or Additional Liability. While redistributing\n    the Work or Derivative Works thereof, You may choose to offer,\n    and charge a fee for, acceptance of support, warranty, indemnity,\n    or other liability obligations and/or rights consistent with this\n    License. However, in accepting such obligations, You may act only\n    on Your own behalf and on Your sole responsibility, not on behalf\n    of any other Contributor, and only if You agree to indemnify,\n    defend, and hold each Contributor harmless for any liability\n    incurred by, or claims asserted against, such Contributor by reason\n    of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\nCopyright 2025 Vercel Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# agent-browser\n\nHeadless browser automation CLI for AI agents. Fast native Rust CLI.\n\n## Installation\n\n### Global Installation (recommended)\n\nInstalls the native Rust binary:\n\n```bash\nnpm install -g agent-browser\nagent-browser install  # Download Chrome from Chrome for Testing (first time only)\n```\n\n### Project Installation (local dependency)\n\nFor projects that want to pin the version in `package.json`:\n\n```bash\nnpm install agent-browser\nagent-browser install\n```\n\nThen use via `package.json` scripts or by invoking `agent-browser` directly.\n\n### Homebrew (macOS)\n\n```bash\nbrew install agent-browser\nagent-browser install  # Download Chrome from Chrome for Testing (first time only)\n```\n\n### Cargo (Rust)\n\n```bash\ncargo install agent-browser\nagent-browser install  # Download Chrome from Chrome for Testing (first time only)\n```\n\n### From Source\n\n```bash\ngit clone https://github.com/vercel-labs/agent-browser\ncd agent-browser\npnpm install\npnpm build\npnpm build:native   # Requires Rust (https://rustup.rs)\npnpm link --global  # Makes agent-browser available globally\nagent-browser install\n```\n\n### Linux Dependencies\n\nOn Linux, install system dependencies:\n\n```bash\nagent-browser install --with-deps\n```\n\n### Updating\n\nUpgrade to the latest version:\n\n```bash\nagent-browser upgrade\n```\n\nDetects your installation method (npm, Homebrew, or Cargo) and runs the appropriate update command automatically.\n\n### Requirements\n\n- **Chrome** - Run `agent-browser install` to download Chrome from [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (Google's official automation channel). No Playwright or Node.js required for the daemon.\n- **Rust** - Only needed when building from source (see From Source above).\n\n## Quick Start\n\n```bash\nagent-browser open example.com\nagent-browser snapshot                    # Get accessibility tree with refs\nagent-browser click @e2                   # Click by ref from snapshot\nagent-browser fill @e3 \"test@example.com\" # Fill by ref\nagent-browser get text @e1                # Get text by ref\nagent-browser screenshot page.png\nagent-browser close\n```\n\n### Traditional Selectors (also supported)\n\n```bash\nagent-browser click \"#submit\"\nagent-browser fill \"#email\" \"test@example.com\"\nagent-browser find role button click --name \"Submit\"\n```\n\n## Commands\n\n### Core Commands\n\n```bash\nagent-browser open <url>              # Navigate to URL (aliases: goto, navigate)\nagent-browser click <sel>             # Click element (--new-tab to open in new tab)\nagent-browser dblclick <sel>          # Double-click element\nagent-browser focus <sel>             # Focus element\nagent-browser type <sel> <text>       # Type into element\nagent-browser fill <sel> <text>       # Clear and fill\nagent-browser press <key>             # Press key (Enter, Tab, Control+a) (alias: key)\nagent-browser keyboard type <text>    # Type with real keystrokes (no selector, current focus)\nagent-browser keyboard inserttext <text>  # Insert text without key events (no selector)\nagent-browser keydown <key>           # Hold key down\nagent-browser keyup <key>             # Release key\nagent-browser hover <sel>             # Hover element\nagent-browser select <sel> <val>      # Select dropdown option\nagent-browser check <sel>             # Check checkbox\nagent-browser uncheck <sel>           # Uncheck checkbox\nagent-browser scroll <dir> [px]       # Scroll (up/down/left/right, --selector <sel>)\nagent-browser scrollintoview <sel>    # Scroll element into view (alias: scrollinto)\nagent-browser drag <src> <tgt>        # Drag and drop\nagent-browser upload <sel> <files>    # Upload files\nagent-browser screenshot [path]       # Take screenshot (--full for full page, saves to a temporary directory if no path)\nagent-browser screenshot --annotate   # Annotated screenshot with numbered element labels\nagent-browser screenshot --screenshot-dir ./shots    # Save to custom directory\nagent-browser screenshot --screenshot-format jpeg --screenshot-quality 80\nagent-browser pdf <path>              # Save as PDF\nagent-browser snapshot                # Accessibility tree with refs (best for AI)\nagent-browser eval <js>               # Run JavaScript (-b for base64, --stdin for piped input)\nagent-browser connect <port>          # Connect to browser via CDP\nagent-browser close                   # Close browser (aliases: quit, exit)\n```\n\n### Get Info\n\n```bash\nagent-browser get text <sel>          # Get text content\nagent-browser get html <sel>          # Get innerHTML\nagent-browser get value <sel>         # Get input value\nagent-browser get attr <sel> <attr>   # Get attribute\nagent-browser get title               # Get page title\nagent-browser get url                 # Get current URL\nagent-browser get cdp-url             # Get CDP WebSocket URL (for DevTools, debugging)\nagent-browser get count <sel>         # Count matching elements\nagent-browser get box <sel>           # Get bounding box\nagent-browser get styles <sel>        # Get computed styles\n```\n\n### Check State\n\n```bash\nagent-browser is visible <sel>        # Check if visible\nagent-browser is enabled <sel>        # Check if enabled\nagent-browser is checked <sel>        # Check if checked\n```\n\n### Find Elements (Semantic Locators)\n\n```bash\nagent-browser find role <role> <action> [value]       # By ARIA role\nagent-browser find text <text> <action>               # By text content\nagent-browser find label <label> <action> [value]     # By label\nagent-browser find placeholder <ph> <action> [value]  # By placeholder\nagent-browser find alt <text> <action>                # By alt text\nagent-browser find title <text> <action>              # By title attr\nagent-browser find testid <id> <action> [value]       # By data-testid\nagent-browser find first <sel> <action> [value]       # First match\nagent-browser find last <sel> <action> [value]        # Last match\nagent-browser find nth <n> <sel> <action> [value]     # Nth match\n```\n\n**Actions:** `click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`\n\n**Options:** `--name <name>` (filter role by accessible name), `--exact` (require exact text match)\n\n**Examples:**\n\n```bash\nagent-browser find role button click --name \"Submit\"\nagent-browser find text \"Sign In\" click\nagent-browser find label \"Email\" fill \"test@test.com\"\nagent-browser find first \".item\" click\nagent-browser find nth 2 \"a\" text\n```\n\n### Wait\n\n```bash\nagent-browser wait <selector>         # Wait for element to be visible\nagent-browser wait <ms>               # Wait for time (milliseconds)\nagent-browser wait --text \"Welcome\"   # Wait for text to appear (substring match)\nagent-browser wait --url \"**/dash\"    # Wait for URL pattern\nagent-browser wait --load networkidle # Wait for load state\nagent-browser wait --fn \"window.ready === true\"  # Wait for JS condition\n\n# Wait for text/element to disappear\nagent-browser wait --fn \"!document.body.innerText.includes('Loading...')\"\nagent-browser wait \"#spinner\" --state hidden\n```\n\n**Load states:** `load`, `domcontentloaded`, `networkidle`\n\n### Batch Execution\n\nExecute multiple commands in a single invocation by piping a JSON array of\nstring arrays to `batch`. This avoids per-command process startup overhead\nwhen running multi-step workflows.\n\n```bash\n# Pipe commands as JSON\necho '[\n  [\"open\", \"https://example.com\"],\n  [\"snapshot\", \"-i\"],\n  [\"click\", \"@e1\"],\n  [\"screenshot\", \"result.png\"]\n]' | agent-browser batch --json\n\n# Stop on first error\nagent-browser batch --bail < commands.json\n```\n\n### Clipboard\n\n```bash\nagent-browser clipboard read                      # Read text from clipboard\nagent-browser clipboard write \"Hello, World!\"     # Write text to clipboard\nagent-browser clipboard copy                      # Copy current selection (Ctrl+C)\nagent-browser clipboard paste                     # Paste from clipboard (Ctrl+V)\n```\n\n### Mouse Control\n\n```bash\nagent-browser mouse move <x> <y>      # Move mouse\nagent-browser mouse down [button]     # Press button (left/right/middle)\nagent-browser mouse up [button]       # Release button\nagent-browser mouse wheel <dy> [dx]   # Scroll wheel\n```\n\n### Browser Settings\n\n```bash\nagent-browser set viewport <w> <h> [scale]  # Set viewport size (scale for retina, e.g. 2)\nagent-browser set device <name>       # Emulate device (\"iPhone 14\")\nagent-browser set geo <lat> <lng>     # Set geolocation\nagent-browser set offline [on|off]    # Toggle offline mode\nagent-browser set headers <json>      # Extra HTTP headers\nagent-browser set credentials <u> <p> # HTTP basic auth\nagent-browser set media [dark|light]  # Emulate color scheme\n```\n\n### Cookies & Storage\n\n```bash\nagent-browser cookies                 # Get all cookies\nagent-browser cookies set <name> <val> # Set cookie\nagent-browser cookies clear           # Clear cookies\n\nagent-browser storage local           # Get all localStorage\nagent-browser storage local <key>     # Get specific key\nagent-browser storage local set <k> <v>  # Set value\nagent-browser storage local clear     # Clear all\n\nagent-browser storage session         # Same for sessionStorage\n```\n\n### Network\n\n```bash\nagent-browser network route <url>              # Intercept requests\nagent-browser network route <url> --abort      # Block requests\nagent-browser network route <url> --body <json>  # Mock response\nagent-browser network unroute [url]            # Remove routes\nagent-browser network requests                 # View tracked requests\nagent-browser network requests --filter api    # Filter requests\nagent-browser network har start                # Start HAR recording\nagent-browser network har stop [output.har]    # Stop and save HAR (temp path if omitted)\n```\n\n### Tabs & Windows\n\n```bash\nagent-browser tab                     # List tabs\nagent-browser tab new [url]           # New tab (optionally with URL)\nagent-browser tab <n>                 # Switch to tab n\nagent-browser tab close [n]           # Close tab\nagent-browser window new              # New window\n```\n\n### Frames\n\n```bash\nagent-browser frame <sel>             # Switch to iframe\nagent-browser frame main              # Back to main frame\n```\n\n### Dialogs\n\n```bash\nagent-browser dialog accept [text]    # Accept (with optional prompt text)\nagent-browser dialog dismiss          # Dismiss\n```\n\n### Diff\n\n```bash\nagent-browser diff snapshot                              # Compare current vs last snapshot\nagent-browser diff snapshot --baseline before.txt        # Compare current vs saved snapshot file\nagent-browser diff snapshot --selector \"#main\" --compact # Scoped snapshot diff\nagent-browser diff screenshot --baseline before.png      # Visual pixel diff against baseline\nagent-browser diff screenshot --baseline b.png -o d.png  # Save diff image to custom path\nagent-browser diff screenshot --baseline b.png -t 0.2    # Adjust color threshold (0-1)\nagent-browser diff url https://v1.com https://v2.com     # Compare two URLs (snapshot diff)\nagent-browser diff url https://v1.com https://v2.com --screenshot  # Also visual diff\nagent-browser diff url https://v1.com https://v2.com --wait-until networkidle  # Custom wait strategy\nagent-browser diff url https://v1.com https://v2.com --selector \"#main\"  # Scope to element\n```\n\n### Debug\n\n```bash\nagent-browser trace start [path]      # Start recording trace\nagent-browser trace stop [path]       # Stop and save trace\nagent-browser profiler start          # Start Chrome DevTools profiling\nagent-browser profiler stop [path]    # Stop and save profile (.json)\nagent-browser console                 # View console messages (log, error, warn, info)\nagent-browser console --clear         # Clear console\nagent-browser errors                  # View page errors (uncaught JavaScript exceptions)\nagent-browser errors --clear          # Clear errors\nagent-browser highlight <sel>         # Highlight element\nagent-browser inspect                 # Open Chrome DevTools for the active page\nagent-browser state save <path>       # Save auth state\nagent-browser state load <path>       # Load auth state\nagent-browser state list              # List saved state files\nagent-browser state show <file>       # Show state summary\nagent-browser state rename <old> <new> # Rename state file\nagent-browser state clear [name]      # Clear states for session\nagent-browser state clear --all       # Clear all saved states\nagent-browser state clean --older-than <days>  # Delete old states\n```\n\n### Navigation\n\n```bash\nagent-browser back                    # Go back\nagent-browser forward                 # Go forward\nagent-browser reload                  # Reload page\n```\n\n### Setup\n\n```bash\nagent-browser install                 # Download Chrome from Chrome for Testing (Google's official automation channel)\nagent-browser install --with-deps     # Also install system deps (Linux)\nagent-browser upgrade                 # Upgrade agent-browser to the latest version\n```\n\n## Authentication\n\nagent-browser provides multiple ways to persist login sessions so you don't re-authenticate every run.\n\n### Quick summary\n\n| Approach | Best for | Flag / Env |\n|----------|----------|------------|\n| **Persistent profile** | Full browser state (cookies, IndexedDB, service workers, cache) across restarts | `--profile <path>` / `AGENT_BROWSER_PROFILE` |\n| **Session persistence** | Auto-save/restore cookies + localStorage by name | `--session-name <name>` / `AGENT_BROWSER_SESSION_NAME` |\n| **Import from your browser** | Grab auth from a Chrome session you already logged into | `--auto-connect` + `state save` |\n| **State file** | Load a previously saved state JSON on launch | `--state <path>` / `AGENT_BROWSER_STATE` |\n| **Auth vault** | Store credentials locally (encrypted), login by name | `auth save` / `auth login` |\n\n### Import auth from your browser\n\nIf you are already logged in to a site in Chrome, you can grab that auth state and reuse it:\n\n```bash\n# 1. Launch Chrome with remote debugging enabled\n#    macOS:\n\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" --remote-debugging-port=9222\n#    Or use --auto-connect to discover an already-running Chrome\n\n# 2. Connect and save the authenticated state\nagent-browser --auto-connect state save ./my-auth.json\n\n# 3. Use the saved auth in future sessions\nagent-browser --state ./my-auth.json open https://app.example.com/dashboard\n\n# 4. Or use --session-name for automatic persistence\nagent-browser --session-name myapp state load ./my-auth.json\n# From now on, --session-name myapp auto-saves/restores this state\n```\n\n> **Security notes:**\n> - `--remote-debugging-port` exposes full browser control on localhost. Any local process can connect. Only use on trusted machines and close Chrome when done.\n> - State files contain session tokens in plaintext. Add them to `.gitignore` and delete when no longer needed. For encryption at rest, set `AGENT_BROWSER_ENCRYPTION_KEY` (see [State Encryption](#state-encryption)).\n\nFor full details on login flows, OAuth, 2FA, cookie-based auth, and the auth vault, see the [Authentication](docs/src/app/sessions/page.mdx) docs.\n\n## Sessions\n\nRun multiple isolated browser instances:\n\n```bash\n# Different sessions\nagent-browser --session agent1 open site-a.com\nagent-browser --session agent2 open site-b.com\n\n# Or via environment variable\nAGENT_BROWSER_SESSION=agent1 agent-browser click \"#btn\"\n\n# List active sessions\nagent-browser session list\n# Output:\n# Active sessions:\n# -> default\n#    agent1\n\n# Show current session\nagent-browser session\n```\n\nEach session has its own:\n\n- Browser instance\n- Cookies and storage\n- Navigation history\n- Authentication state\n\n## Persistent Profiles\n\nBy default, browser state (cookies, localStorage, login sessions) is ephemeral and lost when the browser closes. Use `--profile` to persist state across browser restarts:\n\n```bash\n# Use a persistent profile directory\nagent-browser --profile ~/.myapp-profile open myapp.com\n\n# Login once, then reuse the authenticated session\nagent-browser --profile ~/.myapp-profile open myapp.com/dashboard\n\n# Or via environment variable\nAGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com\n```\n\nThe profile directory stores:\n\n- Cookies and localStorage\n- IndexedDB data\n- Service workers\n- Browser cache\n- Login sessions\n\n**Tip**: Use different profile paths for different projects to keep their browser state isolated.\n\n## Session Persistence\n\nAlternatively, use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:\n\n```bash\n# Auto-save/load state for \"twitter\" session\nagent-browser --session-name twitter open twitter.com\n\n# Login once, then state persists automatically\n# State files stored in ~/.agent-browser/sessions/\n\n# Or via environment variable\nexport AGENT_BROWSER_SESSION_NAME=twitter\nagent-browser open twitter.com\n```\n\n### State Encryption\n\nEncrypt saved session data at rest with AES-256-GCM:\n\n```bash\n# Generate key: openssl rand -hex 32\nexport AGENT_BROWSER_ENCRYPTION_KEY=<64-char-hex-key>\n\n# State files are now encrypted automatically\nagent-browser --session-name secure open example.com\n```\n\n| Variable                          | Description                                        |\n| --------------------------------- | -------------------------------------------------- |\n| `AGENT_BROWSER_SESSION_NAME`      | Auto-save/load state persistence name              |\n| `AGENT_BROWSER_ENCRYPTION_KEY`    | 64-char hex key for AES-256-GCM encryption         |\n| `AGENT_BROWSER_STATE_EXPIRE_DAYS` | Auto-delete states older than N days (default: 30) |\n\n## Security\n\nagent-browser includes security features for safe AI agent deployments. All features are opt-in -- existing workflows are unaffected until you explicitly enable a feature:\n\n- **Authentication Vault** -- Store credentials locally (always encrypted), reference by name. The LLM never sees passwords. A key is auto-generated at `~/.agent-browser/.encryption-key` if `AGENT_BROWSER_ENCRYPTION_KEY` is not set: `echo \"pass\" | agent-browser auth save github --url https://github.com/login --username user --password-stdin` then `agent-browser auth login github`\n- **Content Boundary Markers** -- Wrap page output in delimiters so LLMs can distinguish tool output from untrusted content: `--content-boundaries`\n- **Domain Allowlist** -- Restrict navigation to trusted domains (wildcards like `*.example.com` also match the bare domain): `--allowed-domains \"example.com,*.example.com\"`. Sub-resource requests (scripts, images, fetch) and WebSocket/EventSource connections to non-allowed domains are also blocked. Include any CDN domains your target pages depend on (e.g., `*.cdn.example.com`).\n- **Action Policy** -- Gate destructive actions with a static policy file: `--action-policy ./policy.json`\n- **Action Confirmation** -- Require explicit approval for sensitive action categories: `--confirm-actions eval,download`\n- **Output Length Limits** -- Prevent context flooding: `--max-output 50000`\n\n| Variable                            | Description                              |\n| ----------------------------------- | ---------------------------------------- |\n| `AGENT_BROWSER_CONTENT_BOUNDARIES`  | Wrap page output in boundary markers     |\n| `AGENT_BROWSER_MAX_OUTPUT`          | Max characters for page output           |\n| `AGENT_BROWSER_ALLOWED_DOMAINS`     | Comma-separated allowed domain patterns  |\n| `AGENT_BROWSER_ACTION_POLICY`       | Path to action policy JSON file          |\n| `AGENT_BROWSER_CONFIRM_ACTIONS`     | Action categories requiring confirmation |\n| `AGENT_BROWSER_CONFIRM_INTERACTIVE` | Enable interactive confirmation prompts  |\n\nSee [Security documentation](https://agent-browser.dev/security) for details.\n\n## Snapshot Options\n\nThe `snapshot` command supports filtering to reduce output size:\n\n```bash\nagent-browser snapshot                    # Full accessibility tree\nagent-browser snapshot -i                 # Interactive elements only (buttons, inputs, links)\nagent-browser snapshot -i -C              # Include cursor-interactive elements (divs with onclick, etc.)\nagent-browser snapshot -c                 # Compact (remove empty structural elements)\nagent-browser snapshot -d 3               # Limit depth to 3 levels\nagent-browser snapshot -s \"#main\"         # Scope to CSS selector\nagent-browser snapshot -i -c -d 5         # Combine options\n```\n\n| Option                 | Description                                                             |\n| ---------------------- | ----------------------------------------------------------------------- |\n| `-i, --interactive`    | Only show interactive elements (buttons, links, inputs)                 |\n| `-C, --cursor`         | Include cursor-interactive elements (cursor:pointer, onclick, tabindex) |\n| `-c, --compact`        | Remove empty structural elements                                        |\n| `-d, --depth <n>`      | Limit tree depth                                                        |\n| `-s, --selector <sel>` | Scope to CSS selector                                                   |\n\nThe `-C` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.\n\n## Annotated Screenshots\n\nThe `--annotate` flag overlays numbered labels on interactive elements in the screenshot. Each label `[N]` corresponds to ref `@eN`, so the same refs work for both visual and text-based workflows.\n\nAnnotated screenshots are supported on the CDP-backed browser path (Chrome/Lightpanda). The Safari/WebDriver backend does not yet support `--annotate`.\n\n```bash\nagent-browser screenshot --annotate\n# -> Screenshot saved to /tmp/screenshot-2026-02-17T12-00-00-abc123.png\n#    [1] @e1 button \"Submit\"\n#    [2] @e2 link \"Home\"\n#    [3] @e3 textbox \"Email\"\n```\n\nAfter an annotated screenshot, refs are cached so you can immediately interact with elements:\n\n```bash\nagent-browser screenshot --annotate ./page.png\nagent-browser click @e2     # Click the \"Home\" link labeled [2]\n```\n\nThis is useful for multimodal AI models that can reason about visual layout, unlabeled icon buttons, canvas elements, or visual state that the text accessibility tree cannot capture.\n\n## Options\n\n| Option | Description |\n|--------|-------------|\n| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |\n| `--session-name <name>` | Auto-save/restore session state (or `AGENT_BROWSER_SESSION_NAME` env) |\n| `--profile <path>` | Persistent browser profile directory (or `AGENT_BROWSER_PROFILE` env) |\n| `--state <path>` | Load storage state from JSON file (or `AGENT_BROWSER_STATE` env) |\n| `--headers <json>` | Set HTTP headers scoped to the URL's origin |\n| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |\n| `--extension <path>` | Load browser extension (repeatable; or `AGENT_BROWSER_EXTENSIONS` env) |\n| `--args <args>` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |\n| `--user-agent <ua>` | Custom User-Agent string (or `AGENT_BROWSER_USER_AGENT` env) |\n| `--proxy <url>` | Proxy server URL with optional auth (or `AGENT_BROWSER_PROXY` env) |\n| `--proxy-bypass <hosts>` | Hosts to bypass proxy (or `AGENT_BROWSER_PROXY_BYPASS` env) |\n| `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) |\n| `--allow-file-access` | Allow file:// URLs to access local files (Chromium only) |\n| `-p, --provider <name>` | Cloud browser provider (or `AGENT_BROWSER_PROVIDER` env) |\n| `--device <name>` | iOS device name, e.g. \"iPhone 15 Pro\" (or `AGENT_BROWSER_IOS_DEVICE` env) |\n| `--json` | JSON output (for agents) |\n| `--annotate` | Annotated screenshot with numbered element labels (or `AGENT_BROWSER_ANNOTATE` env) |\n| `--screenshot-dir <path>` | Default screenshot output directory (or `AGENT_BROWSER_SCREENSHOT_DIR` env) |\n| `--screenshot-quality <n>` | JPEG quality 0-100 (or `AGENT_BROWSER_SCREENSHOT_QUALITY` env) |\n| `--screenshot-format <fmt>` | Screenshot format: `png`, `jpeg` (or `AGENT_BROWSER_SCREENSHOT_FORMAT` env) |\n| `--headed` | Show browser window (not headless) (or `AGENT_BROWSER_HEADED` env) |\n| `--cdp <port\\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |\n| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |\n| `--color-scheme <scheme>` | Color scheme: `dark`, `light`, `no-preference` (or `AGENT_BROWSER_COLOR_SCHEME` env) |\n| `--download-path <path>` | Default download directory (or `AGENT_BROWSER_DOWNLOAD_PATH` env) |\n| `--content-boundaries` | Wrap page output in boundary markers for LLM safety (or `AGENT_BROWSER_CONTENT_BOUNDARIES` env) |\n| `--max-output <chars>` | Truncate page output to N characters (or `AGENT_BROWSER_MAX_OUTPUT` env) |\n| `--allowed-domains <list>` | Comma-separated allowed domain patterns (or `AGENT_BROWSER_ALLOWED_DOMAINS` env) |\n| `--action-policy <path>` | Path to action policy JSON file (or `AGENT_BROWSER_ACTION_POLICY` env) |\n| `--confirm-actions <list>` | Action categories requiring confirmation (or `AGENT_BROWSER_CONFIRM_ACTIONS` env) |\n| `--confirm-interactive` | Interactive confirmation prompts; auto-denies if stdin is not a TTY (or `AGENT_BROWSER_CONFIRM_INTERACTIVE` env) |\n| `--engine <name>` | Browser engine: `chrome` (default), `lightpanda` (or `AGENT_BROWSER_ENGINE` env) |\n| `--config <path>` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |\n| `--debug` | Debug output |\n\n## Configuration\n\nCreate an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.\n\n**Locations (lowest to highest priority):**\n\n1. `~/.agent-browser/config.json` -- user-level defaults\n2. `./agent-browser.json` -- project-level overrides (in working directory)\n3. `AGENT_BROWSER_*` environment variables override config file values\n4. CLI flags override everything\n\n**Example `agent-browser.json`:**\n\n```json\n{\n  \"headed\": true,\n  \"proxy\": \"http://localhost:8080\",\n  \"profile\": \"./browser-data\",\n  \"userAgent\": \"my-agent/1.0\",\n  \"ignoreHttpsErrors\": true\n}\n```\n\nUse `--config <path>` or `AGENT_BROWSER_CONFIG` to load a specific config file instead of the defaults:\n\n```bash\nagent-browser --config ./ci-config.json open example.com\nAGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com\n```\n\nAll options from the table above can be set in the config file using camelCase keys (e.g., `--executable-path` becomes `\"executablePath\"`, `--proxy-bypass` becomes `\"proxyBypass\"`). Unknown keys are ignored for forward compatibility.\n\nBoolean flags accept an optional `true`/`false` value to override config settings. For example, `--headed false` disables `\"headed\": true` from config. A bare `--headed` is equivalent to `--headed true`.\n\nAuto-discovered config files that are missing are silently ignored. If `--config <path>` points to a missing or invalid file, agent-browser exits with an error. Extensions from user and project configs are merged (concatenated), not replaced.\n\n> **Tip:** If your project-level `agent-browser.json` contains environment-specific values (paths, proxies), consider adding it to `.gitignore`.\n\n## Default Timeout\n\nThe default timeout for standard operations (clicks, waits, fills, etc.) is 25 seconds. This is intentionally below the CLI's 30-second IPC read timeout so that the daemon returns a proper error instead of the CLI timing out with EAGAIN.\n\nOverride the default timeout via environment variable:\n\n```bash\n# Set a longer timeout for slow pages (in milliseconds)\nexport AGENT_BROWSER_DEFAULT_TIMEOUT=45000\n```\n\n> **Note:** Setting this above 30000 (30s) may cause EAGAIN errors on slow operations because the CLI's read timeout will expire before the daemon responds. The CLI retries transient errors automatically, but response times will increase.\n\n| Variable                        | Description                              |\n| ------------------------------- | ---------------------------------------- |\n| `AGENT_BROWSER_DEFAULT_TIMEOUT` | Default operation timeout in ms (default: 25000) |\n\n## Selectors\n\n### Refs (Recommended for AI)\n\nRefs provide deterministic element selection from snapshots:\n\n```bash\n# 1. Get snapshot with refs\nagent-browser snapshot\n# Output:\n# - heading \"Example Domain\" [ref=e1] [level=1]\n# - button \"Submit\" [ref=e2]\n# - textbox \"Email\" [ref=e3]\n# - link \"Learn more\" [ref=e4]\n\n# 2. Use refs to interact\nagent-browser click @e2                   # Click the button\nagent-browser fill @e3 \"test@example.com\" # Fill the textbox\nagent-browser get text @e1                # Get heading text\nagent-browser hover @e4                   # Hover the link\n```\n\n**Why use refs?**\n\n- **Deterministic**: Ref points to exact element from snapshot\n- **Fast**: No DOM re-query needed\n- **AI-friendly**: Snapshot + ref workflow is optimal for LLMs\n\n### CSS Selectors\n\n```bash\nagent-browser click \"#id\"\nagent-browser click \".class\"\nagent-browser click \"div > button\"\n```\n\n### Text & XPath\n\n```bash\nagent-browser click \"text=Submit\"\nagent-browser click \"xpath=//button\"\n```\n\n### Semantic Locators\n\n```bash\nagent-browser find role button click --name \"Submit\"\nagent-browser find label \"Email\" fill \"test@test.com\"\n```\n\n## Agent Mode\n\nUse `--json` for machine-readable output:\n\n```bash\nagent-browser snapshot --json\n# Returns: {\"success\":true,\"data\":{\"snapshot\":\"...\",\"refs\":{\"e1\":{\"role\":\"heading\",\"name\":\"Title\"},...}}}\n\nagent-browser get text @e1 --json\nagent-browser is visible @e2 --json\n```\n\n### Optimal AI Workflow\n\n```bash\n# 1. Navigate and get snapshot\nagent-browser open example.com\nagent-browser snapshot -i --json   # AI parses tree and refs\n\n# 2. AI identifies target refs from snapshot\n# 3. Execute actions using refs\nagent-browser click @e2\nagent-browser fill @e3 \"input text\"\n\n# 4. Get new snapshot if page changed\nagent-browser snapshot -i --json\n```\n\n### Command Chaining\n\nCommands can be chained with `&&` in a single shell invocation. The browser persists via a background daemon, so chaining is safe and more efficient:\n\n```bash\n# Open, wait for load, and snapshot in one call\nagent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i\n\n# Chain multiple interactions\nagent-browser fill @e1 \"user@example.com\" && agent-browser fill @e2 \"pass\" && agent-browser click @e3\n\n# Navigate and screenshot\nagent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png\n```\n\nUse `&&` when you don't need intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs before interacting).\n\n## Headed Mode\n\nShow the browser window for debugging:\n\n```bash\nagent-browser open example.com --headed\n```\n\nThis opens a visible browser window instead of running headless.\n\n> **Note:** Browser extensions work in both headed and headless mode (Chrome's `--headless=new`).\n\n## Authenticated Sessions\n\nUse `--headers` to set HTTP headers for a specific origin, enabling authentication without login flows:\n\n```bash\n# Headers are scoped to api.example.com only\nagent-browser open api.example.com --headers '{\"Authorization\": \"Bearer <token>\"}'\n\n# Requests to api.example.com include the auth header\nagent-browser snapshot -i --json\nagent-browser click @e2\n\n# Navigate to another domain - headers are NOT sent (safe!)\nagent-browser open other-site.com\n```\n\nThis is useful for:\n\n- **Skipping login flows** - Authenticate via headers instead of UI\n- **Switching users** - Start new sessions with different auth tokens\n- **API testing** - Access protected endpoints directly\n- **Security** - Headers are scoped to the origin, not leaked to other domains\n\nTo set headers for multiple origins, use `--headers` with each `open` command:\n\n```bash\nagent-browser open api.example.com --headers '{\"Authorization\": \"Bearer token1\"}'\nagent-browser open api.acme.com --headers '{\"Authorization\": \"Bearer token2\"}'\n```\n\nFor global headers (all domains), use `set headers`:\n\n```bash\nagent-browser set headers '{\"X-Custom-Header\": \"value\"}'\n```\n\n## Custom Browser Executable\n\nUse a custom browser executable instead of the bundled Chromium. This is useful for:\n\n- **Serverless deployment**: Use lightweight Chromium builds like `@sparticuz/chromium` (~50MB vs ~684MB)\n- **System browsers**: Use an existing Chrome/Chromium installation\n- **Custom builds**: Use modified browser builds\n\n### CLI Usage\n\n```bash\n# Via flag\nagent-browser --executable-path /path/to/chromium open example.com\n\n# Via environment variable\nAGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com\n```\n\n### Serverless (Vercel)\n\nRun agent-browser + Chrome in an ephemeral Vercel Sandbox microVM. No external server needed:\n\n```typescript\nimport { Sandbox } from \"@vercel/sandbox\";\n\nconst sandbox = await Sandbox.create({ runtime: \"node24\" });\nawait sandbox.runCommand(\"agent-browser\", [\"open\", \"https://example.com\"]);\nconst result = await sandbox.runCommand(\"agent-browser\", [\"screenshot\", \"--json\"]);\nawait sandbox.stop();\n```\n\nSee the [environments example](examples/environments/) for a working demo with a UI and deploy-to-Vercel button.\n\n### Serverless (AWS Lambda)\n\n```typescript\nimport chromium from '@sparticuz/chromium';\nimport { execSync } from 'child_process';\n\nexport async function handler() {\n  const executablePath = await chromium.executablePath();\n  const result = execSync(\n    `AGENT_BROWSER_EXECUTABLE_PATH=${executablePath} agent-browser open https://example.com && agent-browser snapshot -i --json`,\n    { encoding: 'utf-8' }\n  );\n  return JSON.parse(result);\n}\n```\n\n## Local Files\n\nOpen and interact with local files (PDFs, HTML, etc.) using `file://` URLs:\n\n```bash\n# Enable file access (required for JavaScript to access local files)\nagent-browser --allow-file-access open file:///path/to/document.pdf\nagent-browser --allow-file-access open file:///path/to/page.html\n\n# Take screenshot of a local PDF\nagent-browser --allow-file-access open file:///Users/me/report.pdf\nagent-browser screenshot report.png\n```\n\nThe `--allow-file-access` flag adds Chromium flags (`--allow-file-access-from-files`, `--allow-file-access`) that allow `file://` URLs to:\n\n- Load and render local files\n- Access other local files via JavaScript (XHR, fetch)\n- Load local resources (images, scripts, stylesheets)\n\n**Note:** This flag only works with Chromium. For security, it's disabled by default.\n\n## CDP Mode\n\nConnect to an existing browser via Chrome DevTools Protocol:\n\n```bash\n# Start Chrome with: google-chrome --remote-debugging-port=9222\n\n# Connect once, then run commands without --cdp\nagent-browser connect 9222\nagent-browser snapshot\nagent-browser tab\nagent-browser close\n\n# Or pass --cdp on each command\nagent-browser --cdp 9222 snapshot\n\n# Connect to remote browser via WebSocket URL\nagent-browser --cdp \"wss://your-browser-service.com/cdp?token=...\" snapshot\n```\n\nThe `--cdp` flag accepts either:\n\n- A port number (e.g., `9222`) for local connections via `http://localhost:{port}`\n- A full WebSocket URL (e.g., `wss://...` or `ws://...`) for remote browser services\n\nThis enables control of:\n\n- Electron apps\n- Chrome/Chromium instances with remote debugging\n- WebView2 applications\n- Any browser exposing a CDP endpoint\n\n### Auto-Connect\n\nUse `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port:\n\n```bash\n# Auto-discover running Chrome with remote debugging\nagent-browser --auto-connect open example.com\nagent-browser --auto-connect snapshot\n\n# Or via environment variable\nAGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot\n```\n\nAuto-connect discovers Chrome by:\n\n1. Reading Chrome's `DevToolsActivePort` file from the default user data directory\n2. Falling back to probing common debugging ports (9222, 9229)\n3. If HTTP-based discovery (`/json/version`, `/json/list`) fails, falling back to a direct WebSocket connection\n\nThis is useful when:\n\n- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port)\n- You want a zero-configuration connection to your existing browser\n- You don't want to track which port Chrome is using\n\n## Streaming (Browser Preview)\n\nStream the browser viewport via WebSocket for live preview or \"pair browsing\" where a human can watch and interact alongside an AI agent.\n\n### Enable Streaming\n\nSet the `AGENT_BROWSER_STREAM_PORT` environment variable:\n\n```bash\nAGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com\n```\n\nThis starts a WebSocket server on the specified port that streams the browser viewport and accepts input events.\n\n### WebSocket Protocol\n\nConnect to `ws://localhost:9223` to receive frames and send input:\n\n**Receive frames:**\n\n```json\n{\n  \"type\": \"frame\",\n  \"data\": \"<base64-encoded-jpeg>\",\n  \"metadata\": {\n    \"deviceWidth\": 1280,\n    \"deviceHeight\": 720,\n    \"pageScaleFactor\": 1,\n    \"offsetTop\": 0,\n    \"scrollOffsetX\": 0,\n    \"scrollOffsetY\": 0\n  }\n}\n```\n\n**Send mouse events:**\n\n```json\n{\n  \"type\": \"input_mouse\",\n  \"eventType\": \"mousePressed\",\n  \"x\": 100,\n  \"y\": 200,\n  \"button\": \"left\",\n  \"clickCount\": 1\n}\n```\n\n**Send keyboard events:**\n\n```json\n{\n  \"type\": \"input_keyboard\",\n  \"eventType\": \"keyDown\",\n  \"key\": \"Enter\",\n  \"code\": \"Enter\"\n}\n```\n\n**Send touch events:**\n\n```json\n{\n  \"type\": \"input_touch\",\n  \"eventType\": \"touchStart\",\n  \"touchPoints\": [{ \"x\": 100, \"y\": 200 }]\n}\n```\n\n## Architecture\n\nagent-browser uses a client-daemon architecture:\n\n1. **Rust CLI** - Parses commands, communicates with daemon\n2. **Rust Daemon** - Pure Rust daemon using direct CDP, no Node.js required\n\nThe daemon starts automatically on first command and persists between commands for fast subsequent operations. To auto-shutdown the daemon after a period of inactivity, set `AGENT_BROWSER_IDLE_TIMEOUT_MS` (value in milliseconds). When set, the daemon closes the browser and exits after receiving no commands for the specified duration.\n\n**Browser Engine:** Uses Chrome (from Chrome for Testing) by default. The `--engine` flag selects between `chrome` and `lightpanda`. Supported browsers: Chromium/Chrome (via CDP) and Safari (via WebDriver for iOS).\n\n## Platforms\n\n| Platform    | Binary      |\n| ----------- | ----------- |\n| macOS ARM64 | Native Rust |\n| macOS x64   | Native Rust |\n| Linux ARM64 | Native Rust |\n| Linux x64   | Native Rust |\n| Windows x64 | Native Rust |\n\n## Usage with AI Agents\n\n### Just ask the agent\n\nThe simplest approach -- just tell your agent to use it:\n\n```\nUse agent-browser to test the login flow. Run agent-browser --help to see available commands.\n```\n\nThe `--help` output is comprehensive and most agents can figure it out from there.\n\n### AI Coding Assistants (recommended)\n\nAdd the skill to your AI coding assistant for richer context:\n\n```bash\nnpx skills add vercel-labs/agent-browser\n```\n\nThis works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf. The skill is fetched from the repository, so it stays up to date automatically -- do not copy `SKILL.md` from `node_modules` as it will become stale.\n\n### Claude Code\n\nInstall as a Claude Code skill:\n\n```bash\nnpx skills add vercel-labs/agent-browser\n```\n\nThis adds the skill to `.claude/skills/agent-browser/SKILL.md` in your project. The skill teaches Claude Code the full agent-browser workflow, including the snapshot-ref interaction pattern, session management, and timeout handling.\n\n### AGENTS.md / CLAUDE.md\n\nFor more consistent results, add to your project or global instructions file:\n\n```markdown\n## Browser Automation\n\nUse `agent-browser` for web automation. Run `agent-browser --help` for all commands.\n\nCore workflow:\n\n1. `agent-browser open <url>` - Navigate to page\n2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2)\n3. `agent-browser click @e1` / `fill @e2 \"text\"` - Interact using refs\n4. Re-snapshot after page changes\n```\n\n## Integrations\n\n### iOS Simulator\n\nControl real Mobile Safari in the iOS Simulator for authentic mobile web testing. Requires macOS with Xcode.\n\n**Setup:**\n\n```bash\n# Install Appium and XCUITest driver\nnpm install -g appium\nappium driver install xcuitest\n```\n\n**Usage:**\n\n```bash\n# List available iOS simulators\nagent-browser device list\n\n# Launch Safari on a specific device\nagent-browser -p ios --device \"iPhone 16 Pro\" open https://example.com\n\n# Same commands as desktop\nagent-browser -p ios snapshot -i\nagent-browser -p ios tap @e1\nagent-browser -p ios fill @e2 \"text\"\nagent-browser -p ios screenshot mobile.png\n\n# Mobile-specific commands\nagent-browser -p ios swipe up\nagent-browser -p ios swipe down 500\n\n# Close session\nagent-browser -p ios close\n```\n\nOr use environment variables:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=ios\nexport AGENT_BROWSER_IOS_DEVICE=\"iPhone 16 Pro\"\nagent-browser open https://example.com\n```\n\n| Variable                   | Description                                     |\n| -------------------------- | ----------------------------------------------- |\n| `AGENT_BROWSER_PROVIDER`   | Set to `ios` to enable iOS mode                 |\n| `AGENT_BROWSER_IOS_DEVICE` | Device name (e.g., \"iPhone 16 Pro\", \"iPad Pro\") |\n| `AGENT_BROWSER_IOS_UDID`   | Device UDID (alternative to device name)        |\n\n**Supported devices:** All iOS Simulators available in Xcode (iPhones, iPads), plus real iOS devices.\n\n**Note:** The iOS provider boots the simulator, starts Appium, and controls Safari. First launch takes ~30-60 seconds; subsequent commands are fast.\n\n#### Real Device Support\n\nAppium also supports real iOS devices connected via USB. This requires additional one-time setup:\n\n**1. Get your device UDID:**\n\n```bash\nxcrun xctrace list devices\n# or\nsystem_profiler SPUSBDataType | grep -A 5 \"iPhone\\|iPad\"\n```\n\n**2. Sign WebDriverAgent (one-time):**\n\n```bash\n# Open the WebDriverAgent Xcode project\ncd ~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent\nopen WebDriverAgent.xcodeproj\n```\n\nIn Xcode:\n\n- Select the `WebDriverAgentRunner` target\n- Go to Signing & Capabilities\n- Select your Team (requires Apple Developer account, free tier works)\n- Let Xcode manage signing automatically\n\n**3. Use with agent-browser:**\n\n```bash\n# Connect device via USB, then:\nagent-browser -p ios --device \"<DEVICE_UDID>\" open https://example.com\n\n# Or use the device name if unique\nagent-browser -p ios --device \"John's iPhone\" open https://example.com\n```\n\n**Real device notes:**\n\n- First run installs WebDriverAgent to the device (may require Trust prompt)\n- Device must be unlocked and connected via USB\n- Slightly slower initial connection than simulator\n- Tests against real Safari performance and behavior\n\n### Browserless\n\n[Browserless](https://browserless.io) provides cloud browser infrastructure with a Sessions API. Use it when running agent-browser in environments where a local browser isn't available.\n\nTo enable Browserless, use the `-p` flag:\n\n```bash\nexport BROWSERLESS_API_KEY=\"your-api-token\"\nagent-browser -p browserless open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=browserless\nexport BROWSERLESS_API_KEY=\"your-api-token\"\nagent-browser open https://example.com\n```\n\nOptional configuration via environment variables:\n\n| Variable                   | Description                                      | Default                                 |\n| -------------------------- | ------------------------------------------------ | --------------------------------------- |\n| `BROWSERLESS_API_URL`      | Base API URL (for custom regions or self-hosted) | `https://production-sfo.browserless.io` |\n| `BROWSERLESS_BROWSER_TYPE` | Type of browser to use (chromium or chrome)      | chromium                                |\n| `BROWSERLESS_TTL`          | Session TTL in milliseconds                      | `300000`                                |\n| `BROWSERLESS_STEALTH`      | Enable stealth mode (`true`/`false`)             | `true`                                  |\n\nWhen enabled, agent-browser connects to a Browserless cloud session instead of launching a local browser. All commands work identically.\n\nGet your API token from the [Browserless Dashboard](https://browserless.io).\n\n### Browserbase\n\n[Browserbase](https://browserbase.com) provides remote browser infrastructure to make deployment of agentic browsing agents easy. Use it when running the agent-browser CLI in an environment where a local browser isn't feasible.\n\nTo enable Browserbase, use the `-p` flag:\n\n```bash\nexport BROWSERBASE_API_KEY=\"your-api-key\"\nagent-browser -p browserbase open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=browserbase\nexport BROWSERBASE_API_KEY=\"your-api-key\"\nagent-browser open https://example.com\n```\n\nWhen enabled, agent-browser connects to a Browserbase session instead of launching a local browser. All commands work identically.\n\nGet your API key from the [Browserbase Dashboard](https://browserbase.com/overview).\n\n### Browser Use\n\n[Browser Use](https://browser-use.com) provides cloud browser infrastructure for AI agents. Use it when running agent-browser in environments where a local browser isn't available (serverless, CI/CD, etc.).\n\nTo enable Browser Use, use the `-p` flag:\n\n```bash\nexport BROWSER_USE_API_KEY=\"your-api-key\"\nagent-browser -p browseruse open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=browseruse\nexport BROWSER_USE_API_KEY=\"your-api-key\"\nagent-browser open https://example.com\n```\n\nWhen enabled, agent-browser connects to a Browser Use cloud session instead of launching a local browser. All commands work identically.\n\nGet your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.\n\n### Kernel\n\n[Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles.\n\nTo enable Kernel, use the `-p` flag:\n\n```bash\nexport KERNEL_API_KEY=\"your-api-key\"\nagent-browser -p kernel open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=kernel\nexport KERNEL_API_KEY=\"your-api-key\"\nagent-browser open https://example.com\n```\n\nOptional configuration via environment variables:\n\n| Variable                 | Description                                                                      | Default |\n| ------------------------ | -------------------------------------------------------------------------------- | ------- |\n| `KERNEL_HEADLESS`        | Run browser in headless mode (`true`/`false`)                                    | `false` |\n| `KERNEL_STEALTH`         | Enable stealth mode to avoid bot detection (`true`/`false`)                      | `true`  |\n| `KERNEL_TIMEOUT_SECONDS` | Session timeout in seconds                                                       | `300`   |\n| `KERNEL_PROFILE_NAME`    | Browser profile name for persistent cookies/logins (created if it doesn't exist) | (none)  |\n\nWhen enabled, agent-browser connects to a Kernel cloud session instead of launching a local browser. All commands work identically.\n\n**Profile Persistence:** When `KERNEL_PROFILE_NAME` is set, the profile will be created if it doesn't already exist. Cookies, logins, and session data are automatically saved back to the profile when the browser session ends, making them available for future sessions.\n\nGet your API key from the [Kernel Dashboard](https://dashboard.onkernel.com).\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "benchmarks/.gitignore",
    "content": "node_modules/\nresults.json\n"
  },
  {
    "path": "benchmarks/README.md",
    "content": "# agent-browser Daemon Benchmarks\n\nCompares command latency and system metrics between the **Node.js daemon** (published npm version) and the **Rust native daemon** (built from source), running inside a [Vercel Sandbox](https://vercel.com/docs/sandbox) microVM.\n\n## What it measures\n\n**Command latency** -- per-scenario timing with warmup, multiple iterations, and stddev:\n\n- `navigate` -- page load round-trip\n- `snapshot` -- accessibility tree generation\n- `screenshot` -- viewport capture\n- `evaluate` -- JavaScript execution\n- `click` -- element interaction\n- `fill` -- form input\n- `agent-loop` -- snapshot/click/snapshot cycle (typical AI agent pattern)\n- `full-workflow` -- realistic 7-command sequence\n\n**System metrics** -- collected while the daemon is running:\n\n- Cold start time (daemon spawn + browser launch)\n- Binary size and total distribution size (including browser download)\n- Daemon RSS and peak RSS (separated from browser process memory)\n- Browser RSS (Chrome processes, same for both daemons)\n- Daemon CPU time\n- Process counts\n\n## Prerequisites\n\n- Node.js 18+\n- pnpm\n- Vercel Sandbox credentials (token, team ID, project ID)\n\n## Setup\n\n```bash\ncd benchmarks\npnpm install\ncp .env.example .env\n```\n\nFill in your Vercel Sandbox credentials in `.env`:\n\n```\nSANDBOX_VERCEL_TOKEN=your_token\nSANDBOX_VERCEL_TEAM_ID=your_team_id\nSANDBOX_VERCEL_PROJECT_ID=your_project_id\n```\n\n## Usage\n\n```bash\npnpm bench                             # 10 iterations, 1 warmup, 8 vCPUs\npnpm bench -- --iterations 20          # more iterations for tighter stats\npnpm bench -- --warmup 2               # extra warmup iterations\npnpm bench -- --json                   # write results.json\npnpm bench -- --branch main            # build native from a different branch\npnpm bench -- --vcpus 16               # more vCPUs (faster Rust build)\n```\n\n## How it works\n\n1. Creates a Vercel Sandbox (Amazon Linux, configurable vCPUs)\n2. Installs Chromium system dependencies\n3. **Phase 1 -- Node.js daemon**: installs `agent-browser` from npm (last version with the Node daemon), runs all scenarios, collects metrics\n4. **Phase 2 -- Rust native daemon**: installs Rust toolchain, clones the repo, runs `cargo build --release`, replaces the binary, runs the same scenarios, collects metrics\n5. Prints comparison tables and optionally writes `results.json`\n\n## Interpreting results\n\n**Command latency** is dominated by Chrome (CDP round-trips), not the daemon. Both daemons are thin relays between the CLI and Chrome, so per-command speedups are typically small. The stddev column helps distinguish real differences from noise.\n\n**Where the native daemon wins** is in cold start (no Node.js runtime to boot), daemon memory (single Rust binary vs V8 heap), and distribution size (no Playwright dependency).\n\nThe **daemon RSS** metric isolates the daemon process memory from Chrome. This is the apples-to-apples comparison -- both daemons talk to the same Chrome, but Node.js adds ~140 MB of V8 overhead while the Rust daemon uses ~7 MB.\n\n**Distribution size** includes the daemon plus its browser download. The Node version includes the npm package + Playwright's bundled Chromium. The Rust version is just the binary + Chrome for Testing.\n"
  },
  {
    "path": "benchmarks/bench.ts",
    "content": "/**\n * Node.js Daemon vs Rust Native Daemon benchmark.\n *\n * Compares the last published npm version (Node.js daemon) against the\n * Rust-only build from a given branch, running real agent-browser commands\n * inside a Vercel Sandbox.\n *\n * Captures:\n *   - Command latency (per-scenario, with warmup + measured iterations + stddev)\n *   - Cold start time (first launch to daemon ready)\n *   - Daemon memory (RSS, peak RSS) separated from browser memory\n *   - Daemon CPU time\n *   - Process tree (daemon + browser children)\n *   - Binary and distribution size on disk\n *\n * Usage:\n *   pnpm bench                        # default: 10 iterations, 1 warmup\n *   pnpm bench -- --iterations 20     # override iterations\n *   pnpm bench -- --warmup 2          # override warmup count\n *   pnpm bench -- --json              # write results.json\n *   pnpm bench -- --branch my-branch  # override native branch (default: ctate/native-2)\n *   pnpm bench -- --vcpus 8           # sandbox vCPUs (default: 8, higher = faster Rust build)\n */\n\nimport { Sandbox } from \"@vercel/sandbox\";\nimport { readFileSync, writeFileSync } from \"fs\";\nimport { scenarios, type Scenario } from \"./scenarios.js\";\n\n// ---------------------------------------------------------------------------\n// Env\n// ---------------------------------------------------------------------------\n\nfunction loadEnv() {\n  try {\n    const content = readFileSync(\".env\", \"utf-8\");\n    for (const line of content.split(\"\\n\")) {\n      const trimmed = line.trim();\n      if (!trimmed || trimmed.startsWith(\"#\")) continue;\n      const eq = trimmed.indexOf(\"=\");\n      if (eq === -1) continue;\n      const key = trimmed.slice(0, eq);\n      let val = trimmed.slice(eq + 1);\n      if (\n        (val.startsWith('\"') && val.endsWith('\"')) ||\n        (val.startsWith(\"'\") && val.endsWith(\"'\"))\n      ) {\n        val = val.slice(1, -1);\n      }\n      process.env[key] = val;\n    }\n  } catch {}\n}\nloadEnv();\n\nconst credentials = {\n  token: process.env.SANDBOX_VERCEL_TOKEN!,\n  teamId: process.env.SANDBOX_VERCEL_TEAM_ID!,\n  projectId: process.env.SANDBOX_VERCEL_PROJECT_ID!,\n};\n\nif (!credentials.token || !credentials.teamId || !credentials.projectId) {\n  console.error(\n    \"Missing credentials. Set SANDBOX_VERCEL_TOKEN, SANDBOX_VERCEL_TEAM_ID, SANDBOX_VERCEL_PROJECT_ID in .env\",\n  );\n  process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// CLI args\n// ---------------------------------------------------------------------------\n\nfunction parseArgs() {\n  const args = process.argv.slice(2);\n  let iterations = 10;\n  let warmup = 1;\n  let json = false;\n  let branch = \"ctate/native-2\";\n  let vcpus = 8;\n\n  for (let i = 0; i < args.length; i++) {\n    if (args[i] === \"--iterations\" && args[i + 1]) {\n      iterations = parseInt(args[++i], 10);\n    } else if (args[i] === \"--warmup\" && args[i + 1]) {\n      warmup = parseInt(args[++i], 10);\n    } else if (args[i] === \"--json\") {\n      json = true;\n    } else if (args[i] === \"--branch\" && args[i + 1]) {\n      branch = args[++i];\n    } else if (args[i] === \"--vcpus\" && args[i + 1]) {\n      vcpus = parseInt(args[++i], 10);\n    }\n  }\n\n  return { iterations, warmup, json, branch, vcpus };\n}\n\nconst config = parseArgs();\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst TIMEOUT_MS = 30 * 60 * 1000;\nconst REPO_URL = \"https://github.com/vercel-labs/agent-browser.git\";\n\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\",\n  \"nspr\",\n  \"libxkbcommon\",\n  \"atk\",\n  \"at-spi2-atk\",\n  \"at-spi2-core\",\n  \"libXcomposite\",\n  \"libXdamage\",\n  \"libXrandr\",\n  \"libXfixes\",\n  \"libXcursor\",\n  \"libXi\",\n  \"libXtst\",\n  \"libXScrnSaver\",\n  \"libXext\",\n  \"mesa-libgbm\",\n  \"libdrm\",\n  \"mesa-libGL\",\n  \"mesa-libEGL\",\n  \"cups-libs\",\n  \"alsa-lib\",\n  \"pango\",\n  \"cairo\",\n  \"gtk3\",\n  \"dbus-libs\",\n];\n\n// ---------------------------------------------------------------------------\n// Sandbox helpers\n// ---------------------------------------------------------------------------\n\ntype SandboxInstance = InstanceType<typeof Sandbox>;\n\nasync function run(\n  sandbox: SandboxInstance,\n  cmd: string,\n  args: string[],\n): Promise<string> {\n  const result = await sandbox.runCommand(cmd, args);\n  const stdout = await result.stdout();\n  const stderr = await result.stderr();\n  if (result.exitCode !== 0) {\n    throw new Error(\n      `Command failed (exit ${result.exitCode}): ${cmd} ${args.join(\" \")}\\n${stderr || stdout}`,\n    );\n  }\n  return stdout;\n}\n\nasync function shell(sandbox: SandboxInstance, script: string): Promise<string> {\n  return run(sandbox, \"sh\", [\"-c\", script]);\n}\n\nasync function shellSafe(sandbox: SandboxInstance, script: string): Promise<string> {\n  const result = await sandbox.runCommand(\"sh\", [\"-c\", script]);\n  return (await result.stdout()).trim();\n}\n\n// ---------------------------------------------------------------------------\n// Stats\n// ---------------------------------------------------------------------------\n\ninterface Stats {\n  avgMs: number;\n  stddevMs: number;\n  minMs: number;\n  maxMs: number;\n  p50Ms: number;\n  samples: number[];\n}\n\nfunction computeStats(samples: number[]): Stats {\n  const sorted = [...samples].sort((a, b) => a - b);\n  const sum = sorted.reduce((a, b) => a + b, 0);\n  const avg = sum / sorted.length;\n  const variance =\n    sorted.reduce((acc, v) => acc + (v - avg) ** 2, 0) / sorted.length;\n  return {\n    avgMs: Math.round(avg),\n    stddevMs: Math.round(Math.sqrt(variance)),\n    minMs: sorted[0],\n    maxMs: sorted[sorted.length - 1],\n    p50Ms: sorted[Math.floor(sorted.length / 2)],\n    samples: sorted,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Metrics collection\n// ---------------------------------------------------------------------------\n\ninterface ProcessMetrics {\n  pid: number;\n  rssKb: number;\n  vszKb: number;\n  cpuPercent: number;\n  memPercent: number;\n  cpuTimeSec: number;\n  command: string;\n}\n\ninterface DaemonMetrics {\n  coldStartMs: number;\n  binarySizeBytes: number;\n  distributionSizeBytes: number;\n  daemonProcesses: ProcessMetrics[];\n  browserProcesses: ProcessMetrics[];\n  daemonRssKb: number;\n  browserRssKb: number;\n  daemonPeakRssKb: number;\n  daemonCpuTimeSec: number;\n  totalCpuTimeSec: number;\n}\n\nasync function findDaemonPids(\n  sandbox: SandboxInstance,\n  _session: string,\n): Promise<number[]> {\n  // The daemon process name is \"agent-browser\" but session/daemon flags are\n  // env vars, not command-line args, so we can't grep them from `ps`.\n  // Instead, find all agent-browser processes that look like long-running daemons\n  // (not short-lived CLI invocations -- those exit immediately).\n  const raw = await shellSafe(\n    sandbox,\n    `pgrep -x agent-browser 2>/dev/null || true`,\n  );\n  if (!raw) {\n    // Fallback: broader match on process name\n    const fallback = await shellSafe(\n      sandbox,\n      `pgrep -f 'agent-browser' 2>/dev/null | head -5 || true`,\n    );\n    if (!fallback) return [];\n    return fallback.split(\"\\n\").map(Number).filter(Boolean);\n  }\n  return raw.split(\"\\n\").map(Number).filter(Boolean);\n}\n\nasync function collectProcessMetrics(\n  sandbox: SandboxInstance,\n  pid: number,\n): Promise<ProcessMetrics | null> {\n  const raw = await shellSafe(\n    sandbox,\n    `ps -p ${pid} -o pid=,rss=,vsz=,%cpu=,%mem=,cputime=,comm= 2>/dev/null || true`,\n  );\n  if (!raw) return null;\n\n  const parts = raw.trim().split(/\\s+/);\n  if (parts.length < 7) return null;\n\n  // Parse cputime \"HH:MM:SS\" or \"MM:SS\" to seconds\n  const timeParts = parts[5].split(\":\").map(Number);\n  let cpuTimeSec = 0;\n  if (timeParts.length === 3) {\n    cpuTimeSec = timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2];\n  } else if (timeParts.length === 2) {\n    cpuTimeSec = timeParts[0] * 60 + timeParts[1];\n  }\n\n  return {\n    pid: Number(parts[0]),\n    rssKb: Number(parts[1]),\n    vszKb: Number(parts[2]),\n    cpuPercent: Number(parts[3]),\n    memPercent: Number(parts[4]),\n    cpuTimeSec,\n    command: parts.slice(6).join(\" \"),\n  };\n}\n\nasync function getPeakRssKb(\n  sandbox: SandboxInstance,\n  pid: number,\n): Promise<number> {\n  const raw = await shellSafe(\n    sandbox,\n    `cat /proc/${pid}/status 2>/dev/null | grep VmHWM | awk '{print $2}' || echo 0`,\n  );\n  return Number(raw) || 0;\n}\n\nasync function getChildPids(\n  sandbox: SandboxInstance,\n  pid: number,\n): Promise<number[]> {\n  const raw = await shellSafe(\n    sandbox,\n    `pgrep -P ${pid} 2>/dev/null || true`,\n  );\n  if (!raw) return [];\n  return raw.split(\"\\n\").map(Number).filter(Boolean);\n}\n\nasync function getAllDescendantPids(\n  sandbox: SandboxInstance,\n  pid: number,\n): Promise<number[]> {\n  const all: number[] = [];\n  const queue = [pid];\n  while (queue.length > 0) {\n    const current = queue.shift()!;\n    all.push(current);\n    const children = await getChildPids(sandbox, current);\n    queue.push(...children);\n  }\n  return all;\n}\n\nasync function collectDaemonMetrics(\n  sandbox: SandboxInstance,\n  session: string,\n  coldStartMs: number,\n  binarySizeBytes: number,\n  distributionSizeBytes: number,\n): Promise<DaemonMetrics> {\n  // Find daemon PIDs -- the agent-browser process itself\n  const daemonPids = await findDaemonPids(sandbox, session);\n\n  // Also find the full process tree (daemon + Chrome children)\n  let allPids: number[] = [];\n  for (const pid of daemonPids) {\n    const descendants = await getAllDescendantPids(sandbox, pid);\n    allPids.push(...descendants);\n  }\n  allPids = [...new Set(allPids)];\n\n  // If no daemon PIDs found via pgrep, fall back to grabbing all\n  // agent-browser and chrome processes for metrics\n  if (allPids.length === 0) {\n    const fallback = await shellSafe(\n      sandbox,\n      `ps -eo pid,comm | grep -E 'agent-browser|chrome' | grep -v grep | awk '{print $1}' || true`,\n    );\n    if (fallback) {\n      allPids = fallback.split(\"\\n\").map(Number).filter(Boolean);\n    }\n  }\n\n  const daemonProcs: ProcessMetrics[] = [];\n  const browserProcs: ProcessMetrics[] = [];\n  let daemonPeakRssKb = 0;\n\n  for (const pid of allPids) {\n    const metrics = await collectProcessMetrics(sandbox, pid);\n    if (!metrics) continue;\n\n    const isBrowser = /chrome|chromium/i.test(metrics.command);\n    if (isBrowser) {\n      browserProcs.push(metrics);\n    } else {\n      daemonProcs.push(metrics);\n      const peak = await getPeakRssKb(sandbox, pid);\n      daemonPeakRssKb = Math.max(daemonPeakRssKb, peak);\n    }\n  }\n\n  const daemonRssKb = daemonProcs.reduce((sum, p) => sum + p.rssKb, 0);\n  const browserRssKb = browserProcs.reduce((sum, p) => sum + p.rssKb, 0);\n  const daemonCpuTimeSec = daemonProcs.reduce((sum, p) => sum + p.cpuTimeSec, 0);\n  const allProcs = [...daemonProcs, ...browserProcs];\n  const totalCpuTimeSec = allProcs.reduce((sum, p) => sum + p.cpuTimeSec, 0);\n\n  return {\n    coldStartMs,\n    binarySizeBytes,\n    distributionSizeBytes,\n    daemonProcesses: daemonProcs,\n    browserProcesses: browserProcs,\n    daemonRssKb,\n    browserRssKb,\n    daemonPeakRssKb,\n    daemonCpuTimeSec,\n    totalCpuTimeSec,\n  };\n}\n\nasync function getBinarySize(\n  sandbox: SandboxInstance,\n): Promise<number> {\n  // Follow symlinks to get the real binary/script size\n  const raw = await shellSafe(\n    sandbox,\n    `stat -L -c %s \"$(readlink -f \"$(which agent-browser)\")\" 2>/dev/null || echo 0`,\n  );\n  return Number(raw) || 0;\n}\n\nasync function getDistributionSize(\n  sandbox: SandboxInstance,\n  mode: DaemonMode,\n): Promise<number> {\n  if (mode === \"node\") {\n    // Total size of the npm package + Playwright browser\n    const npmPkg = await shellSafe(\n      sandbox,\n      `du -sb \"$(npm root -g)/agent-browser\" 2>/dev/null | awk '{print $1}' || echo 0`,\n    );\n    const pwBrowser = await shellSafe(\n      sandbox,\n      `du -sb \"$HOME/.cache/ms-playwright\" 2>/dev/null | awk '{print $1}' || echo 0`,\n    );\n    return (Number(npmPkg) || 0) + (Number(pwBrowser) || 0);\n  } else {\n    // Rust binary + Chrome for Testing (checks multiple possible cache paths)\n    const binary = await shellSafe(\n      sandbox,\n      `stat -L -c %s \"$(readlink -f \"$(which agent-browser)\")\" 2>/dev/null || echo 0`,\n    );\n    const chrome = await shellSafe(\n      sandbox,\n      [\n        `size=0`,\n        `for d in \"$HOME/.cache/agent-browser\" \"$HOME/.cache/ms-playwright\" \"$HOME/.agent-browser/chrome\"; do`,\n        `  if [ -d \"$d\" ]; then size=$(du -sb \"$d\" 2>/dev/null | awk '{print $1}'); break; fi`,\n        `done`,\n        `echo $size`,\n      ].join(\"; \"),\n    );\n    return (Number(binary) || 0) + (Number(chrome) || 0);\n  }\n}\n\nfunction formatBytes(bytes: number): string {\n  if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n  if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n  return `${bytes} B`;\n}\n\nfunction formatKb(kb: number): string {\n  if (kb >= 1024) return `${(kb / 1024).toFixed(1)} MB`;\n  return `${kb} KB`;\n}\n\n// ---------------------------------------------------------------------------\n// Scenario runner\n// ---------------------------------------------------------------------------\n\ntype DaemonMode = \"node\" | \"native\";\n\nfunction daemonEnv(mode: DaemonMode): Record<string, string> {\n  return { AGENT_BROWSER_SESSION: `bench-${mode}` };\n}\n\nasync function agentBrowser(\n  sandbox: SandboxInstance,\n  args: string[],\n  mode: DaemonMode,\n): Promise<void> {\n  const result = await sandbox.runCommand({\n    cmd: \"agent-browser\",\n    args,\n    env: daemonEnv(mode),\n  });\n  if (result.exitCode !== 0) {\n    const stderr = await result.stderr();\n    const stdout = await result.stdout();\n    throw new Error(\n      `agent-browser ${args.join(\" \")} failed (exit ${result.exitCode}): ${stderr || stdout}`,\n    );\n  }\n}\n\nasync function timedAgentBrowser(\n  sandbox: SandboxInstance,\n  args: string[],\n  mode: DaemonMode,\n): Promise<number> {\n  const start = Date.now();\n  const result = await sandbox.runCommand({\n    cmd: \"agent-browser\",\n    args,\n    env: daemonEnv(mode),\n  });\n  const elapsed = Date.now() - start;\n  if (result.exitCode !== 0) {\n    const stderr = await result.stderr();\n    const stdout = await result.stdout();\n    throw new Error(\n      `agent-browser ${args.join(\" \")} failed (exit ${result.exitCode}): ${stderr || stdout}`,\n    );\n  }\n  return elapsed;\n}\n\ninterface ScenarioResult {\n  name: string;\n  description: string;\n  stats: Stats;\n  error?: string;\n}\n\nasync function runScenario(\n  sandbox: SandboxInstance,\n  scenario: Scenario,\n  mode: DaemonMode,\n  iterations: number,\n  warmup: number,\n): Promise<ScenarioResult> {\n  try {\n    if (scenario.setup) {\n      for (const cmd of scenario.setup) {\n        await agentBrowser(sandbox, cmd, mode);\n      }\n    }\n\n    for (let w = 0; w < warmup; w++) {\n      for (const cmd of scenario.commands) {\n        await agentBrowser(sandbox, cmd, mode);\n      }\n    }\n\n    const samples: number[] = [];\n    for (let i = 0; i < iterations; i++) {\n      let totalMs = 0;\n      for (const cmd of scenario.commands) {\n        totalMs += await timedAgentBrowser(sandbox, cmd, mode);\n      }\n      samples.push(totalMs);\n    }\n\n    if (scenario.teardown) {\n      for (const cmd of scenario.teardown) {\n        await agentBrowser(sandbox, cmd, mode);\n      }\n    }\n\n    return {\n      name: scenario.name,\n      description: scenario.description,\n      stats: computeStats(samples),\n    };\n  } catch (err: unknown) {\n    const message = err instanceof Error ? err.message : String(err);\n    return {\n      name: scenario.name,\n      description: scenario.description,\n      stats: { avgMs: -1, stddevMs: -1, minMs: -1, maxMs: -1, p50Ms: -1, samples: [] },\n      error: message,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Benchmark phases\n// ---------------------------------------------------------------------------\n\ninterface DaemonResults {\n  mode: DaemonMode;\n  label: string;\n  scenarios: ScenarioResult[];\n  metrics: DaemonMetrics;\n}\n\nasync function benchmarkDaemon(\n  sandbox: SandboxInstance,\n  mode: DaemonMode,\n  label: string,\n): Promise<DaemonResults> {\n  console.log(`\\n--- ${label} ---`);\n\n  // Measure sizes before launch\n  const binarySizeBytes = await getBinarySize(sandbox);\n  const distributionSizeBytes = await getDistributionSize(sandbox, mode);\n\n  // Cold start: time the first launch (daemon spawn + browser launch)\n  const coldStartBegin = Date.now();\n  await agentBrowser(sandbox, [\"open\", \"about:blank\"], mode);\n  const coldStartMs = Date.now() - coldStartBegin;\n  console.log(`  Cold start: ${coldStartMs}ms`);\n  console.log(`  Binary size: ${formatBytes(binarySizeBytes)}`);\n  console.log(`  Distribution size: ${formatBytes(distributionSizeBytes)}`);\n\n  // Run all scenarios\n  const results: ScenarioResult[] = [];\n  for (const scenario of scenarios) {\n    process.stdout.write(`  ${scenario.name} `);\n    const result = await runScenario(\n      sandbox,\n      scenario,\n      mode,\n      config.iterations,\n      config.warmup,\n    );\n    if (result.error) {\n      console.log(`FAILED: ${result.error.slice(0, 120)}`);\n    } else {\n      const dots = \".\".repeat(Math.max(1, 30 - scenario.name.length));\n      const s = result.stats;\n      console.log(\n        `${dots} ${s.avgMs}ms avg +/-${s.stddevMs}ms (p50: ${s.p50Ms}ms, min: ${s.minMs}ms, max: ${s.maxMs}ms)`,\n      );\n    }\n    results.push(result);\n  }\n\n  // Collect system metrics after scenarios (daemon is still running)\n  const session = `bench-${mode}`;\n  const metrics = await collectDaemonMetrics(\n    sandbox,\n    session,\n    coldStartMs,\n    binarySizeBytes,\n    distributionSizeBytes,\n  );\n\n  // Also grab a full process snapshot for context\n  const psOutput = await shellSafe(\n    sandbox,\n    `ps aux --sort=-rss | head -20`,\n  );\n  console.log(`\\n  Process snapshot (top by RSS):`);\n  for (const line of psOutput.split(\"\\n\").slice(0, 10)) {\n    console.log(`    ${line}`);\n  }\n\n  console.log(`\\n  Daemon processes (${metrics.daemonProcesses.length}):`);\n  console.log(`    RSS: ${formatKb(metrics.daemonRssKb)} (peak: ${formatKb(metrics.daemonPeakRssKb)})`);\n  console.log(`    CPU time: ${metrics.daemonCpuTimeSec.toFixed(1)}s`);\n  for (const p of metrics.daemonProcesses) {\n    console.log(`      PID ${p.pid}: ${p.command} (RSS: ${formatKb(p.rssKb)}, CPU: ${p.cpuPercent}%)`);\n  }\n  console.log(`  Browser processes (${metrics.browserProcesses.length}):`);\n  console.log(`    RSS: ${formatKb(metrics.browserRssKb)}`);\n  for (const p of metrics.browserProcesses) {\n    console.log(`      PID ${p.pid}: ${p.command} (RSS: ${formatKb(p.rssKb)}, CPU: ${p.cpuPercent}%)`);\n  }\n\n  await agentBrowser(sandbox, [\"close\"], mode);\n  console.log(`  Browser closed.`);\n\n  return { mode, label, scenarios: results, metrics };\n}\n\n// ---------------------------------------------------------------------------\n// Install helpers\n// ---------------------------------------------------------------------------\n\nasync function installChromiumDeps(sandbox: SandboxInstance) {\n  console.log(\"Installing Chromium system dependencies...\");\n  await shell(\n    sandbox,\n    `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n  );\n}\n\nasync function installNodeDaemon(sandbox: SandboxInstance) {\n  console.log(\"Installing agent-browser from npm (Node.js daemon)...\");\n  await run(sandbox, \"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n  await run(sandbox, \"npx\", [\"agent-browser\", \"install\"]);\n  const version = await shell(sandbox, \"agent-browser --version 2>&1 || true\");\n  console.log(`  version: ${version.trim()}`);\n}\n\nasync function installNativeDaemon(sandbox: SandboxInstance, branch: string) {\n  console.log(`\\nBuilding native daemon from ${branch}...`);\n\n  console.log(\"  Installing build tools and Rust toolchain...\");\n  const rustStart = Date.now();\n  await shell(\n    sandbox,\n    \"sudo dnf install -y gcc gcc-c++ make perl-core openssl-devel 2>&1\",\n  );\n  await shell(\n    sandbox,\n    \"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 2>&1\",\n  );\n  console.log(`  Rust + build tools installed (${Math.round((Date.now() - rustStart) / 1000)}s)`);\n\n  console.log(`  Cloning repo (branch: ${branch})...`);\n  const cloneStart = Date.now();\n  await shell(\n    sandbox,\n    `git clone --depth 1 --branch ${branch} ${REPO_URL} /tmp/agent-browser 2>&1`,\n  );\n  console.log(`  Cloned (${Math.round((Date.now() - cloneStart) / 1000)}s)`);\n\n  console.log(\"  Building release binary (cargo build --release)...\");\n  const buildStart = Date.now();\n  await shell(\n    sandbox,\n    \"source $HOME/.cargo/env && cd /tmp/agent-browser/cli && cargo build --release 2>&1\",\n  );\n  console.log(`  Built (${Math.round((Date.now() - buildStart) / 1000)}s)`);\n\n  const npmBinPath = (await shell(sandbox, \"which agent-browser\")).trim();\n  console.log(`  Replacing ${npmBinPath} with native build...`);\n  await shell(\n    sandbox,\n    `sudo cp /tmp/agent-browser/cli/target/release/agent-browser ${npmBinPath}`,\n  );\n\n  const version = await shell(sandbox, \"agent-browser --version 2>&1 || true\");\n  console.log(`  version: ${version.trim()}`);\n}\n\n// ---------------------------------------------------------------------------\n// Output\n// ---------------------------------------------------------------------------\n\nfunction printResults(node: DaemonResults, native: DaemonResults) {\n  console.log(\"\\n\\n========== COMMAND LATENCY ==========\\n\");\n\n  const header =\n    \"Scenario\".padEnd(20) + \"| Node avg +/-sd  | Rust avg +/-sd  | Speedup\";\n  const sep = \"-\".repeat(20) + \"|-----------------|-----------------|--------\";\n  console.log(header);\n  console.log(sep);\n\n  for (let i = 0; i < node.scenarios.length; i++) {\n    const n = node.scenarios[i];\n    const r = native.scenarios[i];\n    const name = n.name.padEnd(20);\n\n    if (n.error || r.error) {\n      const nodeVal = n.error ? \"FAILED\".padEnd(15) : `${n.stats.avgMs}ms`.padEnd(15);\n      const rustVal = r.error ? \"FAILED\".padEnd(15) : `${r.stats.avgMs}ms`.padEnd(15);\n      console.log(`${name}| ${nodeVal} | ${rustVal} |    --`);\n      continue;\n    }\n\n    const nodeVal = `${n.stats.avgMs} +/-${n.stats.stddevMs}ms`.padEnd(15);\n    const rustVal = `${r.stats.avgMs} +/-${r.stats.stddevMs}ms`.padEnd(15);\n    const speedup =\n      r.stats.avgMs > 0\n        ? (n.stats.avgMs / r.stats.avgMs).toFixed(2) + \"x\"\n        : \"--\";\n    console.log(`${name}| ${nodeVal} | ${rustVal} | ${speedup.padStart(6)}`);\n  }\n\n  console.log(\"\\n\\n========== SYSTEM METRICS ==========\\n\");\n\n  const nm = node.metrics;\n  const rm = native.metrics;\n\n  function ratio(a: number, b: number): string {\n    if (b <= 0) return \"--\";\n    return (a / b).toFixed(2) + \"x\";\n  }\n\n  const metricRows: [string, string, string, string][] = [\n    [\n      \"Cold start\",\n      `${nm.coldStartMs}ms`,\n      `${rm.coldStartMs}ms`,\n      ratio(nm.coldStartMs, rm.coldStartMs),\n    ],\n    [\n      \"Binary size\",\n      formatBytes(nm.binarySizeBytes),\n      formatBytes(rm.binarySizeBytes),\n      ratio(nm.binarySizeBytes, rm.binarySizeBytes),\n    ],\n    [\n      \"Distribution size\",\n      formatBytes(nm.distributionSizeBytes),\n      formatBytes(rm.distributionSizeBytes),\n      ratio(nm.distributionSizeBytes, rm.distributionSizeBytes),\n    ],\n    [\n      \"Daemon RSS\",\n      formatKb(nm.daemonRssKb),\n      formatKb(rm.daemonRssKb),\n      ratio(nm.daemonRssKb, rm.daemonRssKb),\n    ],\n    [\n      \"Daemon peak RSS\",\n      formatKb(nm.daemonPeakRssKb),\n      formatKb(rm.daemonPeakRssKb),\n      ratio(nm.daemonPeakRssKb, rm.daemonPeakRssKb),\n    ],\n    [\n      \"Browser RSS\",\n      formatKb(nm.browserRssKb),\n      formatKb(rm.browserRssKb),\n      ratio(nm.browserRssKb, rm.browserRssKb),\n    ],\n    [\n      \"Daemon CPU time\",\n      `${nm.daemonCpuTimeSec.toFixed(1)}s`,\n      `${rm.daemonCpuTimeSec.toFixed(1)}s`,\n      ratio(nm.daemonCpuTimeSec, rm.daemonCpuTimeSec),\n    ],\n    [\n      \"Daemon processes\",\n      String(nm.daemonProcesses.length),\n      String(rm.daemonProcesses.length),\n      \"--\",\n    ],\n    [\n      \"Browser processes\",\n      String(nm.browserProcesses.length),\n      String(rm.browserProcesses.length),\n      \"--\",\n    ],\n  ];\n\n  const mHeader =\n    \"Metric\".padEnd(20) + \"| Node\".padEnd(14) + \"| Rust\".padEnd(14) + \"| Ratio\";\n  const mSep = \"-\".repeat(20) + \"|\" + \"-\".repeat(13) + \"|\" + \"-\".repeat(13) + \"|--------\";\n  console.log(mHeader);\n  console.log(mSep);\n  for (const [metric, nodeVal, rustVal, ratio] of metricRows) {\n    console.log(\n      `${metric.padEnd(20)}| ${nodeVal.padEnd(12)}| ${rustVal.padEnd(12)}| ${ratio}`,\n    );\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n  console.log(\"agent-browser Daemon Benchmark (Node.js vs Rust Native)\");\n  console.log(`Branch: ${config.branch}`);\n  console.log(`Iterations: ${config.iterations} (+ ${config.warmup} warmup)`);\n  console.log(`vCPUs: ${config.vcpus}\\n`);\n\n  console.log(\"Creating sandbox...\");\n  const sandbox = await Sandbox.create({\n    ...credentials,\n    timeout: TIMEOUT_MS,\n    runtime: \"node22\",\n    networkPolicy: \"allow-all\" as const,\n    resources: { vcpus: config.vcpus },\n  });\n  console.log(`Sandbox: ${sandbox.sandboxId}`);\n\n  try {\n    await installChromiumDeps(sandbox);\n\n    // Phase 1: Node.js daemon (from published npm package)\n    await installNodeDaemon(sandbox);\n    const nodeResults = await benchmarkDaemon(\n      sandbox,\n      \"node\",\n      \"Node.js Daemon (npm)\",\n    );\n\n    // Phase 2: Rust native daemon (built from branch)\n    await installNativeDaemon(sandbox, config.branch);\n    const nativeResults = await benchmarkDaemon(\n      sandbox,\n      \"native\",\n      `Rust Native Daemon (${config.branch})`,\n    );\n\n    printResults(nodeResults, nativeResults);\n\n    if (config.json) {\n      const output = {\n        timestamp: new Date().toISOString(),\n        branch: config.branch,\n        vcpus: config.vcpus,\n        iterations: config.iterations,\n        warmup: config.warmup,\n        node: {\n          scenarios: nodeResults.scenarios.map((s) => ({\n            name: s.name,\n            description: s.description,\n            ...s.stats,\n            error: s.error,\n          })),\n          metrics: nodeResults.metrics,\n        },\n        native: {\n          scenarios: nativeResults.scenarios.map((s) => ({\n            name: s.name,\n            description: s.description,\n            ...s.stats,\n            error: s.error,\n          })),\n          metrics: nativeResults.metrics,\n        },\n      };\n      writeFileSync(\"results.json\", JSON.stringify(output, null, 2));\n      console.log(\"\\nResults written to results.json\");\n    }\n  } catch (err: unknown) {\n    const message = err instanceof Error ? err.message : String(err);\n    console.error(`\\nFatal error: ${message}`);\n    process.exit(1);\n  } finally {\n    try {\n      await sandbox.stop();\n      console.log(\"\\nSandbox stopped.\");\n    } catch {\n      console.warn(\"Warning: failed to stop sandbox.\");\n    }\n  }\n}\n\nmain();\n"
  },
  {
    "path": "benchmarks/package.json",
    "content": "{\n  \"name\": \"agent-browser-benchmarks\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"bench\": \"tsx bench.ts\"\n  },\n  \"dependencies\": {\n    \"@vercel/sandbox\": \"^1.8.0\",\n    \"tsx\": \"^4.19.0\"\n  }\n}\n"
  },
  {
    "path": "benchmarks/scenarios.ts",
    "content": "/**\n * Benchmark scenarios for comparing Node.js daemon vs Rust native daemon.\n *\n * Each scenario defines CLI commands run via `sandbox.runCommand(\"agent-browser\", args)`.\n * Setup/teardown commands run once and are not timed.\n * The `commands` array is timed over N iterations.\n */\n\nexport interface Scenario {\n  name: string;\n  description: string;\n  setup?: string[][];\n  commands: string[][];\n  teardown?: string[][];\n}\n\nconst FORM_HTML = [\n  \"<html><head><title>Bench</title></head><body>\",\n  \"<h1>Benchmark Page</h1>\",\n  \"<input id='name' type='text' placeholder='Name'>\",\n  \"<input id='email' type='email' placeholder='Email'>\",\n  \"<select id='color'><option value='red'>Red</option><option value='blue'>Blue</option></select>\",\n  \"<input id='agree' type='checkbox'>\",\n  \"<textarea id='bio' placeholder='Bio'></textarea>\",\n  \"<button id='submit'>Submit</button>\",\n  \"<p id='status'>Ready</p>\",\n  \"<a id='link' href='javascript:void(0)' onclick=\\\"document.getElementById('status').textContent='Clicked'\\\">Click me</a>\",\n  \"<ul>\",\n  ...Array.from({ length: 20 }, (_, i) => `<li class='item'>Item ${i + 1}</li>`),\n  \"</ul>\",\n  \"</body></html>\",\n].join(\"\");\n\nconst INJECT_FORM_SCRIPT = `document.open(); document.write(${JSON.stringify(FORM_HTML)}); document.close(); 'ok'`;\n\nconst SETUP_PAGE: string[][] = [\n  [\"open\", \"about:blank\"],\n  [\"eval\", INJECT_FORM_SCRIPT],\n];\n\nexport const scenarios: Scenario[] = [\n  {\n    name: \"navigate\",\n    description: \"Page navigation (about:blank round-trip)\",\n    commands: [[\"open\", \"about:blank\"]],\n  },\n  {\n    name: \"snapshot\",\n    description: \"DOM snapshot (accessibility tree)\",\n    setup: SETUP_PAGE,\n    commands: [[\"snapshot\"]],\n  },\n  {\n    name: \"screenshot\",\n    description: \"Screenshot capture\",\n    setup: SETUP_PAGE,\n    commands: [[\"screenshot\"]],\n  },\n  {\n    name: \"evaluate\",\n    description: \"JavaScript evaluation\",\n    setup: SETUP_PAGE,\n    commands: [\n      [\n        \"eval\",\n        \"document.title + ' ' + document.querySelectorAll('li').length\",\n      ],\n    ],\n  },\n  {\n    name: \"click\",\n    description: \"Element click interaction\",\n    setup: SETUP_PAGE,\n    commands: [[\"click\", \"#link\"]],\n  },\n  {\n    name: \"fill\",\n    description: \"Form field fill\",\n    setup: SETUP_PAGE,\n    commands: [[\"fill\", \"#name\", \"Benchmark User\"]],\n  },\n  {\n    name: \"agent-loop\",\n    description: \"AI agent loop: snapshot -> click -> snapshot (typical agent cycle)\",\n    setup: SETUP_PAGE,\n    commands: [[\"snapshot\"], [\"click\", \"#link\"], [\"snapshot\"]],\n  },\n  {\n    name: \"full-workflow\",\n    description:\n      \"Realistic workflow: navigate, inject form, snapshot, click, fill, evaluate, screenshot\",\n    commands: [\n      [\"open\", \"about:blank\"],\n      [\"eval\", INJECT_FORM_SCRIPT],\n      [\"snapshot\"],\n      [\"click\", \"#link\"],\n      [\"fill\", \"#name\", \"Agent User\"],\n      [\n        \"eval\",\n        \"document.getElementById('name').value\",\n      ],\n      [\"screenshot\"],\n    ],\n  },\n];\n"
  },
  {
    "path": "benchmarks/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"dist\",\n    \"declaration\": true\n  },\n  \"include\": [\"*.ts\"]\n}\n"
  },
  {
    "path": "bin/agent-browser.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Cross-platform CLI wrapper for agent-browser\n * \n * This wrapper enables npx support on Windows where shell scripts don't work.\n * For global installs, postinstall.js patches the shims to invoke the native\n * binary directly (zero overhead).\n */\n\nimport { spawn, execSync } from 'child_process';\nimport { existsSync, accessSync, chmodSync, constants } from 'fs';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\nimport { platform, arch } from 'os';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Detect if the system uses musl libc (e.g. Alpine Linux)\nfunction isMusl() {\n  if (platform() !== 'linux') return false;\n  try {\n    const result = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });\n    return result.toLowerCase().includes('musl');\n  } catch {\n    return existsSync('/lib/ld-musl-x86_64.so.1') || existsSync('/lib/ld-musl-aarch64.so.1');\n  }\n}\n\n// Map Node.js platform/arch to binary naming convention\nfunction getBinaryName() {\n  const os = platform();\n  const cpuArch = arch();\n\n  let osKey;\n  switch (os) {\n    case 'darwin':\n      osKey = 'darwin';\n      break;\n    case 'linux':\n      osKey = isMusl() ? 'linux-musl' : 'linux';\n      break;\n    case 'win32':\n      osKey = 'win32';\n      break;\n    default:\n      return null;\n  }\n\n  let archKey;\n  switch (cpuArch) {\n    case 'x64':\n    case 'x86_64':\n      archKey = 'x64';\n      break;\n    case 'arm64':\n    case 'aarch64':\n      archKey = 'arm64';\n      break;\n    default:\n      return null;\n  }\n\n  const ext = os === 'win32' ? '.exe' : '';\n  return `agent-browser-${osKey}-${archKey}${ext}`;\n}\n\nfunction main() {\n  const binaryName = getBinaryName();\n\n  if (!binaryName) {\n    console.error(`Error: Unsupported platform: ${platform()}-${arch()}`);\n    process.exit(1);\n  }\n\n  const binaryPath = join(__dirname, binaryName);\n\n  if (!existsSync(binaryPath)) {\n    console.error(`Error: No binary found for ${platform()}-${arch()}`);\n    console.error(`Expected: ${binaryPath}`);\n    console.error('');\n    console.error('Run \"npm run build:native\" to build for your platform,');\n    console.error('or reinstall the package to trigger the postinstall download.');\n    process.exit(1);\n  }\n\n  // Ensure binary is executable (fixes EACCES on macOS/Linux when postinstall didn't run,\n  // e.g., when using bun which blocks lifecycle scripts by default)\n  if (platform() !== 'win32') {\n    try {\n      accessSync(binaryPath, constants.X_OK);\n    } catch {\n      // Binary exists but isn't executable - fix it\n      try {\n        chmodSync(binaryPath, 0o755);\n      } catch (chmodErr) {\n        console.error(`Error: Cannot make binary executable: ${chmodErr.message}`);\n        console.error('Try running: chmod +x ' + binaryPath);\n        process.exit(1);\n      }\n    }\n  }\n\n  // Spawn the native binary with inherited stdio\n  const child = spawn(binaryPath, process.argv.slice(2), {\n    stdio: 'inherit',\n    windowsHide: false,\n  });\n\n  child.on('error', (err) => {\n    console.error(`Error executing binary: ${err.message}`);\n    process.exit(1);\n  });\n\n  child.on('close', (code) => {\n    process.exit(code ?? 0);\n  });\n}\n\nmain();\n"
  },
  {
    "path": "cli/Cargo.toml",
    "content": "[package]\nname = \"agent-browser\"\nversion = \"0.21.2\"\nedition = \"2021\"\ndescription = \"Fast browser automation CLI for AI agents\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/vercel-labs/agent-browser\"\nhomepage = \"https://agent-browser.dev\"\nreadme = \"../README.md\"\nkeywords = [\"browser\", \"automation\", \"ai\", \"cdp\", \"chrome\"]\ncategories = [\"command-line-utilities\", \"web-programming\"]\n\n[dependencies]\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\ndirs = \"5.0\"\nbase64 = \"0.22\"\ngetrandom = \"0.2\"\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"net\", \"io-util\", \"time\", \"sync\", \"signal\", \"process\"] }\ntokio-tungstenite = { version = \"0.24\", features = [\"rustls-tls-webpki-roots\"] }\nfutures-util = \"0.3\"\nurl = \"2\"\nuuid = { version = \"1\", features = [\"v4\"] }\nimage = \"0.25\"\nreqwest = { version = \"0.12\", default-features = false, features = [\"json\", \"rustls-tls-webpki-roots\"] }\nsha2 = \"0.10\"\naes-gcm = \"0.10\"\nasync-trait = \"0.1\"\nsocket2 = \"0.6\"\nsimilar = \"2\"\nzip = { version = \"8.2.0\", default-features = false, features = [\"deflate\"] }\ntime = { version = \"0.3\", features = [\"formatting\"] }\n\n[target.'cfg(unix)'.dependencies]\nlibc = \"0.2\"\n\n[target.'cfg(windows)'.dependencies]\nwindows-sys = { version = \"0.52\", features = [\"Win32_System_Threading\", \"Win32_Foundation\"] }\n\n[build-dependencies]\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\n\n[profile.release]\nopt-level = 3\nlto = true\ncodegen-units = 1\nstrip = true\n\n[profile.ci]\ninherits = \"release\"\nlto = \"thin\"\ncodegen-units = 16\n"
  },
  {
    "path": "cli/build.rs",
    "content": "use std::collections::HashSet;\nuse std::env;\nuse std::fs;\nuse std::path::Path;\n\nfn main() {\n    let protocol_dir = Path::new(\"cdp-protocol\");\n    let out_dir = env::var(\"OUT_DIR\").unwrap();\n    let out_path = Path::new(&out_dir).join(\"cdp_generated.rs\");\n\n    let browser_path = protocol_dir.join(\"browser_protocol.json\");\n    let js_path = protocol_dir.join(\"js_protocol.json\");\n\n    if !browser_path.exists() && !js_path.exists() {\n        fs::write(\n            &out_path,\n            \"// No protocol JSON files found in cdp-protocol/\\n\",\n        )\n        .unwrap();\n        return;\n    }\n\n    let mut all_domains: Vec<Domain> = Vec::new();\n\n    for path in [&browser_path, &js_path] {\n        if !path.exists() {\n            continue;\n        }\n        println!(\"cargo:rerun-if-changed={}\", path.display());\n        let content = fs::read_to_string(path).unwrap();\n        let protocol: ProtocolSpec = match serde_json::from_str(&content) {\n            Ok(p) => p,\n            Err(e) => {\n                eprintln!(\"cargo:warning=Failed to parse {}: {}\", path.display(), e);\n                continue;\n            }\n        };\n        all_domains.extend(protocol.domains);\n    }\n\n    // Collect all known type IDs per domain for cross-domain resolution\n    let mut domain_types: std::collections::HashMap<String, HashSet<String>> =\n        std::collections::HashMap::new();\n    for domain in &all_domains {\n        let mut types = HashSet::new();\n        for td in &domain.types {\n            types.insert(td.id.clone());\n        }\n        domain_types.insert(domain.domain.clone(), types);\n    }\n\n    // Known recursive struct fields that need Box wrapping\n    let recursive_fields: HashSet<(&str, &str, &str)> = [\n        (\"DOM\", \"Node\", \"contentDocument\"),\n        (\"DOM\", \"Node\", \"templateContent\"),\n        (\"DOM\", \"Node\", \"importedDocument\"),\n        (\"Accessibility\", \"AXNode\", \"sources\"),\n        (\"Runtime\", \"StackTrace\", \"parent\"),\n    ]\n    .into_iter()\n    .collect();\n\n    let mut output = String::new();\n    output.push_str(\"use serde::{Deserialize, Serialize};\\n\\n\");\n\n    for domain in &all_domains {\n        generate_domain(domain, &domain_types, &recursive_fields, &mut output);\n    }\n\n    fs::write(&out_path, &output).unwrap();\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize)]\nstruct ProtocolSpec {\n    domains: Vec<Domain>,\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize, Clone)]\nstruct Domain {\n    domain: String,\n    #[serde(default)]\n    types: Vec<TypeDef>,\n    #[serde(default)]\n    commands: Vec<Command>,\n    #[serde(default)]\n    events: Vec<Event>,\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize, Clone)]\nstruct TypeDef {\n    id: String,\n    #[serde(rename = \"type\", default)]\n    type_kind: String,\n    #[serde(default)]\n    properties: Vec<Property>,\n    #[serde(rename = \"enum\", default)]\n    enum_values: Vec<String>,\n    #[serde(default)]\n    description: Option<String>,\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize, Clone)]\nstruct Command {\n    name: String,\n    #[serde(default)]\n    parameters: Vec<Property>,\n    #[serde(default)]\n    returns: Vec<Property>,\n    #[serde(default)]\n    description: Option<String>,\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize, Clone)]\nstruct Event {\n    name: String,\n    #[serde(default)]\n    parameters: Vec<Property>,\n    #[serde(default)]\n    description: Option<String>,\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize, Clone)]\nstruct Property {\n    name: String,\n    #[serde(rename = \"type\", default)]\n    type_kind: Option<String>,\n    #[serde(rename = \"$ref\", default)]\n    ref_type: Option<String>,\n    #[serde(default)]\n    optional: bool,\n    #[serde(default)]\n    description: Option<String>,\n    #[serde(default)]\n    items: Option<Box<ItemType>>,\n    #[serde(rename = \"enum\", default)]\n    enum_values: Vec<String>,\n}\n\n#[allow(dead_code)]\n#[derive(serde::Deserialize, Clone)]\nstruct ItemType {\n    #[serde(rename = \"type\", default)]\n    type_kind: Option<String>,\n    #[serde(rename = \"$ref\", default)]\n    ref_type: Option<String>,\n}\n\nfn to_pascal_case(s: &str) -> String {\n    let mut result = String::new();\n    let mut capitalize = true;\n    for c in s.chars() {\n        if c == '_' || c == '-' || c == '.' {\n            capitalize = true;\n        } else if capitalize {\n            result.push(c.to_ascii_uppercase());\n            capitalize = false;\n        } else {\n            result.push(c);\n        }\n    }\n    result\n}\n\nfn to_snake_case(s: &str) -> String {\n    let mut result = String::new();\n    let chars: Vec<char> = s.chars().collect();\n    for (i, &c) in chars.iter().enumerate() {\n        if c.is_uppercase() && i > 0 {\n            // Only insert underscore at transitions from lowercase to uppercase,\n            // or when an uppercase sequence ends (e.g. \"DOM\" -> \"dom\", not \"d_o_m\")\n            let prev_upper = chars[i - 1].is_uppercase();\n            let next_lower = chars.get(i + 1).is_some_and(|n| n.is_lowercase());\n            if !prev_upper || next_lower {\n                result.push('_');\n            }\n        }\n        result.push(c.to_ascii_lowercase());\n    }\n    result\n}\n\n/// Resolve a $ref type reference. Cross-domain refs like \"Page.FrameId\" become\n/// `super::cdp_page::FrameId`. Same-domain refs are used directly.\nfn resolve_ref(\n    r: &str,\n    current_domain: &str,\n    domain_types: &std::collections::HashMap<String, HashSet<String>>,\n) -> String {\n    let parts: Vec<&str> = r.split('.').collect();\n    if parts.len() == 2 {\n        let ref_domain = parts[0];\n        let ref_type = parts[1];\n        if ref_domain == current_domain {\n            to_pascal_case(ref_type)\n        } else {\n            // Check if this type actually exists in the referenced domain\n            if domain_types\n                .get(ref_domain)\n                .is_some_and(|t| t.contains(ref_type))\n            {\n                format!(\n                    \"super::cdp_{}::{}\",\n                    to_snake_case(ref_domain),\n                    to_pascal_case(ref_type)\n                )\n            } else {\n                // Fall back to serde_json::Value for unknown cross-domain refs\n                \"serde_json::Value\".to_string()\n            }\n        }\n    } else {\n        to_pascal_case(r)\n    }\n}\n\nfn map_type_in_domain(\n    prop: &Property,\n    current_domain: &str,\n    domain_types: &std::collections::HashMap<String, HashSet<String>>,\n) -> String {\n    if let Some(ref r) = prop.ref_type {\n        let type_name = resolve_ref(r, current_domain, domain_types);\n        if prop.optional {\n            format!(\"Option<{}>\", type_name)\n        } else {\n            type_name\n        }\n    } else if let Some(ref t) = prop.type_kind {\n        let base = match t.as_str() {\n            \"string\" => \"String\".to_string(),\n            \"integer\" => \"i64\".to_string(),\n            \"number\" => \"f64\".to_string(),\n            \"boolean\" => \"bool\".to_string(),\n            \"object\" => \"serde_json::Value\".to_string(),\n            \"any\" => \"serde_json::Value\".to_string(),\n            \"array\" => {\n                if let Some(ref items) = prop.items {\n                    let inner = if let Some(ref r) = items.ref_type {\n                        resolve_ref(r, current_domain, domain_types)\n                    } else {\n                        match items.type_kind.as_deref().unwrap_or(\"any\") {\n                            \"string\" => \"String\".to_string(),\n                            \"integer\" => \"i64\".to_string(),\n                            \"number\" => \"f64\".to_string(),\n                            \"boolean\" => \"bool\".to_string(),\n                            _ => \"serde_json::Value\".to_string(),\n                        }\n                    };\n                    format!(\"Vec<{}>\", inner)\n                } else {\n                    \"Vec<serde_json::Value>\".to_string()\n                }\n            }\n            _ => \"serde_json::Value\".to_string(),\n        };\n        if prop.optional {\n            format!(\"Option<{}>\", base)\n        } else {\n            base\n        }\n    } else if prop.optional {\n        \"Option<serde_json::Value>\".to_string()\n    } else {\n        \"serde_json::Value\".to_string()\n    }\n}\n\nfn is_rust_keyword(s: &str) -> bool {\n    matches!(\n        s,\n        \"type\"\n            | \"self\"\n            | \"Self\"\n            | \"super\"\n            | \"move\"\n            | \"ref\"\n            | \"fn\"\n            | \"mod\"\n            | \"use\"\n            | \"pub\"\n            | \"let\"\n            | \"mut\"\n            | \"const\"\n            | \"static\"\n            | \"if\"\n            | \"else\"\n            | \"for\"\n            | \"while\"\n            | \"loop\"\n            | \"match\"\n            | \"return\"\n            | \"break\"\n            | \"continue\"\n            | \"as\"\n            | \"in\"\n            | \"impl\"\n            | \"trait\"\n            | \"struct\"\n            | \"enum\"\n            | \"where\"\n            | \"async\"\n            | \"await\"\n            | \"dyn\"\n            | \"box\"\n            | \"yield\"\n            | \"override\"\n            | \"crate\"\n            | \"extern\"\n    )\n}\n\nfn generate_domain(\n    domain: &Domain,\n    domain_types: &std::collections::HashMap<String, HashSet<String>>,\n    recursive_fields: &HashSet<(&str, &str, &str)>,\n    output: &mut String,\n) {\n    let mod_name = to_snake_case(&domain.domain);\n    output.push_str(&format!(\n        \"#[allow(dead_code, non_snake_case, non_camel_case_types, clippy::enum_variant_names)]\\npub mod cdp_{} {{\\n\",\n        mod_name\n    ));\n    output.push_str(\"    use super::*;\\n\\n\");\n\n    for type_def in &domain.types {\n        if !type_def.enum_values.is_empty() {\n            // Deduplicate enum variants (some CDP enums have duplicated PascalCase forms)\n            let mut seen_variants = HashSet::new();\n            output.push_str(\"    #[derive(Debug, Clone, Serialize, Deserialize)]\\n\");\n            output.push_str(&format!(\"    pub enum {} {{\\n\", type_def.id));\n            for val in &type_def.enum_values {\n                let mut variant = to_pascal_case(val);\n                if variant == \"Self\" {\n                    variant = \"SelfValue\".to_string();\n                }\n                if variant.chars().next().is_some_and(|c| c.is_ascii_digit()) {\n                    variant = format!(\"V{}\", variant);\n                }\n                if seen_variants.insert(variant.clone()) {\n                    output.push_str(&format!(\n                        \"        #[serde(rename = \\\"{}\\\")]\\n        {},\\n\",\n                        val, variant\n                    ));\n                }\n            }\n            output.push_str(\"    }\\n\\n\");\n        } else if type_def.type_kind == \"object\" && !type_def.properties.is_empty() {\n            output.push_str(\n                \"    #[derive(Debug, Clone, Serialize, Deserialize)]\\n    #[serde(rename_all = \\\"camelCase\\\")]\\n\",\n            );\n            output.push_str(&format!(\"    pub struct {} {{\\n\", type_def.id));\n            for prop in &type_def.properties {\n                let field_name = to_snake_case(&prop.name);\n                let field_name = if is_rust_keyword(&field_name) {\n                    format!(\"r#{}\", field_name)\n                } else {\n                    field_name\n                };\n                let mut rust_type = map_type_in_domain(prop, &domain.domain, domain_types);\n\n                // Wrap recursive fields in Box\n                if recursive_fields.contains(&(\n                    domain.domain.as_str(),\n                    type_def.id.as_str(),\n                    prop.name.as_str(),\n                )) {\n                    if rust_type.starts_with(\"Option<\") {\n                        let inner = &rust_type[7..rust_type.len() - 1];\n                        rust_type = format!(\"Option<Box<{}>>\", inner);\n                    } else {\n                        rust_type = format!(\"Box<{}>\", rust_type);\n                    }\n                }\n\n                if prop.optional {\n                    output\n                        .push_str(\"        #[serde(skip_serializing_if = \\\"Option::is_none\\\")]\\n\");\n                }\n                output.push_str(&format!(\"        pub {}: {},\\n\", field_name, rust_type));\n            }\n            output.push_str(\"    }\\n\\n\");\n        } else if type_def.type_kind == \"object\" && type_def.properties.is_empty() {\n            output.push_str(&format!(\n                \"    pub type {} = serde_json::Value;\\n\\n\",\n                type_def.id\n            ));\n        } else if type_def.type_kind == \"array\" {\n            output.push_str(&format!(\n                \"    pub type {} = Vec<serde_json::Value>;\\n\\n\",\n                type_def.id\n            ));\n        } else if type_def.type_kind == \"string\" && type_def.enum_values.is_empty() {\n            output.push_str(&format!(\"    pub type {} = String;\\n\\n\", type_def.id));\n        } else if type_def.type_kind == \"integer\" {\n            output.push_str(&format!(\"    pub type {} = i64;\\n\\n\", type_def.id));\n        } else if type_def.type_kind == \"number\" {\n            output.push_str(&format!(\"    pub type {} = f64;\\n\\n\", type_def.id));\n        }\n    }\n\n    for cmd in &domain.commands {\n        let pascal_name = to_pascal_case(&cmd.name);\n\n        if !cmd.parameters.is_empty() {\n            output.push_str(\n                \"    #[derive(Debug, Clone, Serialize, Deserialize)]\\n    #[serde(rename_all = \\\"camelCase\\\")]\\n\",\n            );\n            output.push_str(&format!(\"    pub struct {}Params {{\\n\", pascal_name));\n            for param in &cmd.parameters {\n                let field_name = to_snake_case(&param.name);\n                let field_name = if is_rust_keyword(&field_name) {\n                    format!(\"r#{}\", field_name)\n                } else {\n                    field_name\n                };\n                let rust_type = map_type_in_domain(param, &domain.domain, domain_types);\n                if param.optional {\n                    output\n                        .push_str(\"        #[serde(skip_serializing_if = \\\"Option::is_none\\\")]\\n\");\n                }\n                output.push_str(&format!(\"        pub {}: {},\\n\", field_name, rust_type));\n            }\n            output.push_str(\"    }\\n\\n\");\n        }\n\n        if !cmd.returns.is_empty() {\n            output.push_str(\n                \"    #[derive(Debug, Clone, Serialize, Deserialize)]\\n    #[serde(rename_all = \\\"camelCase\\\")]\\n\",\n            );\n            output.push_str(&format!(\"    pub struct {}Result {{\\n\", pascal_name));\n            for ret in &cmd.returns {\n                let field_name = to_snake_case(&ret.name);\n                let field_name = if is_rust_keyword(&field_name) {\n                    format!(\"r#{}\", field_name)\n                } else {\n                    field_name\n                };\n                let rust_type = map_type_in_domain(ret, &domain.domain, domain_types);\n                if ret.optional {\n                    output\n                        .push_str(\"        #[serde(skip_serializing_if = \\\"Option::is_none\\\")]\\n\");\n                }\n                output.push_str(&format!(\"        pub {}: {},\\n\", field_name, rust_type));\n            }\n            output.push_str(\"    }\\n\\n\");\n        }\n    }\n\n    for event in &domain.events {\n        if !event.parameters.is_empty() {\n            let pascal_name = to_pascal_case(&event.name);\n            output.push_str(\n                \"    #[derive(Debug, Clone, Serialize, Deserialize)]\\n    #[serde(rename_all = \\\"camelCase\\\")]\\n\",\n            );\n            output.push_str(&format!(\"    pub struct {}Event {{\\n\", pascal_name));\n            for param in &event.parameters {\n                let field_name = to_snake_case(&param.name);\n                let field_name = if is_rust_keyword(&field_name) {\n                    format!(\"r#{}\", field_name)\n                } else {\n                    field_name\n                };\n                let rust_type = map_type_in_domain(param, &domain.domain, domain_types);\n                if param.optional {\n                    output\n                        .push_str(\"        #[serde(skip_serializing_if = \\\"Option::is_none\\\")]\\n\");\n                }\n                output.push_str(&format!(\"        pub {}: {},\\n\", field_name, rust_type));\n            }\n            output.push_str(\"    }\\n\\n\");\n        }\n    }\n\n    output.push_str(\"}\\n\\n\");\n}\n"
  },
  {
    "path": "cli/cdp-protocol/browser_protocol.json",
    "content": "{\n    \"version\": {\n        \"major\": \"1\",\n        \"minor\": \"3\"\n    },\n    \"domains\": [\n        {\n            \"domain\": \"Accessibility\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"DOM\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"AXNodeId\",\n                    \"description\": \"Unique accessibility node identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"AXValueType\",\n                    \"description\": \"Enum of possible property types.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"boolean\",\n                        \"tristate\",\n                        \"booleanOrUndefined\",\n                        \"idref\",\n                        \"idrefList\",\n                        \"integer\",\n                        \"node\",\n                        \"nodeList\",\n                        \"number\",\n                        \"string\",\n                        \"computedString\",\n                        \"token\",\n                        \"tokenList\",\n                        \"domRelation\",\n                        \"role\",\n                        \"internalRole\",\n                        \"valueUndefined\"\n                    ]\n                },\n                {\n                    \"id\": \"AXValueSourceType\",\n                    \"description\": \"Enum of possible property sources.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"attribute\",\n                        \"implicit\",\n                        \"style\",\n                        \"contents\",\n                        \"placeholder\",\n                        \"relatedElement\"\n                    ]\n                },\n                {\n                    \"id\": \"AXValueNativeSourceType\",\n                    \"description\": \"Enum of possible native property sources (as a subtype of a particular AXValueSourceType).\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"description\",\n                        \"figcaption\",\n                        \"label\",\n                        \"labelfor\",\n                        \"labelwrapped\",\n                        \"legend\",\n                        \"rubyannotation\",\n                        \"tablecaption\",\n                        \"title\",\n                        \"other\"\n                    ]\n                },\n                {\n                    \"id\": \"AXValueSource\",\n                    \"description\": \"A single source for a computed AX property.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"What type of source this is.\",\n                            \"$ref\": \"AXValueSourceType\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The value of this property source.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"attribute\",\n                            \"description\": \"The name of the relevant attribute, if any.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"attributeValue\",\n                            \"description\": \"The value of the relevant attribute, if any.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"superseded\",\n                            \"description\": \"Whether this source is superseded by a higher priority source.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"nativeSource\",\n                            \"description\": \"The native markup source for this value, e.g. a `<label>` element.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValueNativeSourceType\"\n                        },\n                        {\n                            \"name\": \"nativeSourceValue\",\n                            \"description\": \"The value, such as a node or node list, of the native source.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"invalid\",\n                            \"description\": \"Whether the value for this property is invalid.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"invalidReason\",\n                            \"description\": \"Reason for the value being invalid, if it is.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AXRelatedNode\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"backendDOMNodeId\",\n                            \"description\": \"The BackendNodeId of the related DOM node.\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"idref\",\n                            \"description\": \"The IDRef value provided, if any.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"The text alternative of this node in the current context.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AXProperty\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The name of this property.\",\n                            \"$ref\": \"AXPropertyName\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The value of this property.\",\n                            \"$ref\": \"AXValue\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AXValue\",\n                    \"description\": \"A single computed AX property.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"The type of this value.\",\n                            \"$ref\": \"AXValueType\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The computed value of this property.\",\n                            \"optional\": true,\n                            \"type\": \"any\"\n                        },\n                        {\n                            \"name\": \"relatedNodes\",\n                            \"description\": \"One or more related nodes, if applicable.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXRelatedNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"sources\",\n                            \"description\": \"The sources which contributed to the computation of this property.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXValueSource\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AXPropertyName\",\n                    \"description\": \"Values of AXProperty name:\\n- from 'busy' to 'roledescription': states which apply to every AX node\\n- from 'live' to 'root': attributes which apply to nodes in live regions\\n- from 'autocomplete' to 'valuetext': attributes which apply to widgets\\n- from 'checked' to 'selected': states which apply to widgets\\n- from 'activedescendant' to 'owns': relationships between elements other than parent/child/sibling\\n- from 'activeFullscreenElement' to 'uninteresting': reasons why this noode is hidden\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"actions\",\n                        \"busy\",\n                        \"disabled\",\n                        \"editable\",\n                        \"focusable\",\n                        \"focused\",\n                        \"hidden\",\n                        \"hiddenRoot\",\n                        \"invalid\",\n                        \"keyshortcuts\",\n                        \"settable\",\n                        \"roledescription\",\n                        \"live\",\n                        \"atomic\",\n                        \"relevant\",\n                        \"root\",\n                        \"autocomplete\",\n                        \"hasPopup\",\n                        \"level\",\n                        \"multiselectable\",\n                        \"orientation\",\n                        \"multiline\",\n                        \"readonly\",\n                        \"required\",\n                        \"valuemin\",\n                        \"valuemax\",\n                        \"valuetext\",\n                        \"checked\",\n                        \"expanded\",\n                        \"modal\",\n                        \"pressed\",\n                        \"selected\",\n                        \"activedescendant\",\n                        \"controls\",\n                        \"describedby\",\n                        \"details\",\n                        \"errormessage\",\n                        \"flowto\",\n                        \"labelledby\",\n                        \"owns\",\n                        \"url\",\n                        \"activeFullscreenElement\",\n                        \"activeModalDialog\",\n                        \"activeAriaModalDialog\",\n                        \"ariaHiddenElement\",\n                        \"ariaHiddenSubtree\",\n                        \"emptyAlt\",\n                        \"emptyText\",\n                        \"inertElement\",\n                        \"inertSubtree\",\n                        \"labelContainer\",\n                        \"labelFor\",\n                        \"notRendered\",\n                        \"notVisible\",\n                        \"presentationalRole\",\n                        \"probablyPresentational\",\n                        \"inactiveCarouselTabContent\",\n                        \"uninteresting\"\n                    ]\n                },\n                {\n                    \"id\": \"AXNode\",\n                    \"description\": \"A node in the accessibility tree.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Unique identifier for this node.\",\n                            \"$ref\": \"AXNodeId\"\n                        },\n                        {\n                            \"name\": \"ignored\",\n                            \"description\": \"Whether this node is ignored for accessibility\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"ignoredReasons\",\n                            \"description\": \"Collection of reasons why this node is hidden.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXProperty\"\n                            }\n                        },\n                        {\n                            \"name\": \"role\",\n                            \"description\": \"This `Node`'s role, whether explicit or implicit.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"chromeRole\",\n                            \"description\": \"This `Node`'s Chrome raw role.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The accessible name for this `Node`.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"description\",\n                            \"description\": \"The accessible description for this `Node`.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The value for this `Node`.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXValue\"\n                        },\n                        {\n                            \"name\": \"properties\",\n                            \"description\": \"All other properties\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXProperty\"\n                            }\n                        },\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"ID for this node's parent.\",\n                            \"optional\": true,\n                            \"$ref\": \"AXNodeId\"\n                        },\n                        {\n                            \"name\": \"childIds\",\n                            \"description\": \"IDs for each of this node's child nodes.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNodeId\"\n                            }\n                        },\n                        {\n                            \"name\": \"backendDOMNodeId\",\n                            \"description\": \"The backend ID for the associated DOM node, if any.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The frame ID for the frame associated with this nodes document.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables the accessibility domain.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled.\"\n                },\n                {\n                    \"name\": \"getPartialAXTree\",\n                    \"description\": \"Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to get the partial accessibility tree for.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node to get the partial accessibility tree for.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper to get the partial accessibility tree for.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"fetchRelatives\",\n                            \"description\": \"Whether to fetch this node's ancestors, siblings and children. Defaults to true.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\\nchildren, if requested.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getFullAXTree\",\n                    \"description\": \"Fetches the entire accessibility tree for the root Document\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"The maximum depth at which descendants of the root node should be retrieved.\\nIf omitted, the full tree is returned.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The frame for whose document the AX tree should be retrieved.\\nIf omitted, the root frame is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getRootAXNode\",\n                    \"description\": \"Fetches the root node.\\nRequires `enable()` to have been called previously.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The frame in whose document the node resides.\\nIf omitted, the root frame is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"node\",\n                            \"$ref\": \"AXNode\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAXNodeAndAncestors\",\n                    \"description\": \"Fetches a node and all ancestors up to and including the root.\\nRequires `enable()` to have been called previously.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to get.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node to get.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper to get.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getChildAXNodes\",\n                    \"description\": \"Fetches a particular accessibility node by AXNodeId.\\nRequires `enable()` to have been called previously.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"AXNodeId\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The frame in whose document the node resides.\\nIf omitted, the root frame is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"queryAXTree\",\n                    \"description\": \"Query a DOM node's accessibility subtree for accessible name and role.\\nThis command computes the name and role for all nodes in the subtree, including those that are\\nignored for accessibility, and returns those that match the specified name and role. If no DOM\\nnode is specified, or the DOM node does not exist, the command returns an error. If neither\\n`accessibleName` or `role` is specified, it returns all the accessibility nodes in the subtree.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node for the root to query.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node for the root to query.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper for the root to query.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"accessibleName\",\n                            \"description\": \"Find nodes with this computed name.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"role\",\n                            \"description\": \"Find nodes with this computed role.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"A list of `Accessibility.AXNode` matching the specified attributes,\\nincluding nodes that are ignored for accessibility.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNode\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"loadComplete\",\n                    \"description\": \"The loadComplete event mirrors the load complete event sent by the browser to assistive\\ntechnology when the web page has finished loading.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"root\",\n                            \"description\": \"New document root node.\",\n                            \"$ref\": \"AXNode\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nodesUpdated\",\n                    \"description\": \"The nodesUpdated event is sent every time a previously requested node has changed the in tree.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"Updated node data.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AXNode\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Animation\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Runtime\",\n                \"DOM\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"Animation\",\n                    \"description\": \"Animation instance.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"`Animation`'s id.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"`Animation`'s name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"pausedState\",\n                            \"description\": \"`Animation`'s internal paused state.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"playState\",\n                            \"description\": \"`Animation`'s play state.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"playbackRate\",\n                            \"description\": \"`Animation`'s playback rate.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"startTime\",\n                            \"description\": \"`Animation`'s start time.\\nMilliseconds for time based animations and\\npercentage [0 - 100] for scroll driven animations\\n(i.e. when viewOrScrollTimeline exists).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"currentTime\",\n                            \"description\": \"`Animation`'s current time.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Animation type of `Animation`.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"CSSTransition\",\n                                \"CSSAnimation\",\n                                \"WebAnimation\"\n                            ]\n                        },\n                        {\n                            \"name\": \"source\",\n                            \"description\": \"`Animation`'s source animation node.\",\n                            \"optional\": true,\n                            \"$ref\": \"AnimationEffect\"\n                        },\n                        {\n                            \"name\": \"cssId\",\n                            \"description\": \"A unique ID for `Animation` representing the sources that triggered this CSS\\nanimation/transition.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"viewOrScrollTimeline\",\n                            \"description\": \"View or scroll timeline\",\n                            \"optional\": true,\n                            \"$ref\": \"ViewOrScrollTimeline\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ViewOrScrollTimeline\",\n                    \"description\": \"Timeline instance\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sourceNodeId\",\n                            \"description\": \"Scroll container node\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"startOffset\",\n                            \"description\": \"Represents the starting scroll position of the timeline\\nas a length offset in pixels from scroll origin.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"endOffset\",\n                            \"description\": \"Represents the ending scroll position of the timeline\\nas a length offset in pixels from scroll origin.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"subjectNodeId\",\n                            \"description\": \"The element whose principal box's visibility in the\\nscrollport defined the progress of the timeline.\\nDoes not exist for animations with ScrollTimeline\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"axis\",\n                            \"description\": \"Orientation of the scroll\",\n                            \"$ref\": \"DOM.ScrollOrientation\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AnimationEffect\",\n                    \"description\": \"AnimationEffect instance\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"delay\",\n                            \"description\": \"`AnimationEffect`'s delay.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"endDelay\",\n                            \"description\": \"`AnimationEffect`'s end delay.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"iterationStart\",\n                            \"description\": \"`AnimationEffect`'s iteration start.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"iterations\",\n                            \"description\": \"`AnimationEffect`'s iterations. Omitted if the value is infinite.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"duration\",\n                            \"description\": \"`AnimationEffect`'s iteration duration.\\nMilliseconds for time based animations and\\npercentage [0 - 100] for scroll driven animations\\n(i.e. when viewOrScrollTimeline exists).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"direction\",\n                            \"description\": \"`AnimationEffect`'s playback direction.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fill\",\n                            \"description\": \"`AnimationEffect`'s fill mode.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"`AnimationEffect`'s target node.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"keyframesRule\",\n                            \"description\": \"`AnimationEffect`'s keyframes.\",\n                            \"optional\": true,\n                            \"$ref\": \"KeyframesRule\"\n                        },\n                        {\n                            \"name\": \"easing\",\n                            \"description\": \"`AnimationEffect`'s timing function.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"KeyframesRule\",\n                    \"description\": \"Keyframes Rule\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"CSS keyframed animation's name.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyframes\",\n                            \"description\": \"List of animation keyframes.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"KeyframeStyle\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"KeyframeStyle\",\n                    \"description\": \"Keyframe Style\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"offset\",\n                            \"description\": \"Keyframe's time offset.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"easing\",\n                            \"description\": \"`AnimationEffect`'s timing function.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables animation domain notifications.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables animation domain notifications.\"\n                },\n                {\n                    \"name\": \"getCurrentTime\",\n                    \"description\": \"Returns the current time of the an animation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Id of animation.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"currentTime\",\n                            \"description\": \"Current time of the page.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getPlaybackRate\",\n                    \"description\": \"Gets the playback rate of the document timeline.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"playbackRate\",\n                            \"description\": \"Playback rate for animations on page.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"releaseAnimations\",\n                    \"description\": \"Releases a set of animations to no longer be manipulated.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animations\",\n                            \"description\": \"List of animation ids to seek.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resolveAnimation\",\n                    \"description\": \"Gets the remote object of the Animation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animationId\",\n                            \"description\": \"Animation id.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"remoteObject\",\n                            \"description\": \"Corresponding remote object.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"seekAnimations\",\n                    \"description\": \"Seek a set of animations to a particular time within each animation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animations\",\n                            \"description\": \"List of animation ids to seek.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"currentTime\",\n                            \"description\": \"Set the current time of each animation.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPaused\",\n                    \"description\": \"Sets the paused state of a set of animations.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animations\",\n                            \"description\": \"Animations to set the pause state of.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"paused\",\n                            \"description\": \"Paused state to set to.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPlaybackRate\",\n                    \"description\": \"Sets the playback rate of the document timeline.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"playbackRate\",\n                            \"description\": \"Playback rate for animations on page\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setTiming\",\n                    \"description\": \"Sets the timing of an animation node.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animationId\",\n                            \"description\": \"Animation id.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"duration\",\n                            \"description\": \"Duration of the animation.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"delay\",\n                            \"description\": \"Delay of the animation.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"animationCanceled\",\n                    \"description\": \"Event for when an animation has been cancelled.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Id of the animation that was cancelled.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"animationCreated\",\n                    \"description\": \"Event for each animation that has been created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Id of the animation that was created.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"animationStarted\",\n                    \"description\": \"Event for animation that has been started.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animation\",\n                            \"description\": \"Animation that was started.\",\n                            \"$ref\": \"Animation\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"animationUpdated\",\n                    \"description\": \"Event for animation that has been updated.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"animation\",\n                            \"description\": \"Animation that was updated.\",\n                            \"$ref\": \"Animation\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Audits\",\n            \"description\": \"Audits domain allows investigation of page violations and possible improvements.\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Network\",\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"AffectedCookie\",\n                    \"description\": \"Information about a cookie that is affected by an inspector issue.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The following three properties uniquely identify a cookie\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domain\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AffectedRequest\",\n                    \"description\": \"Information about a request that is affected by an inspector issue.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"The unique request id.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.RequestId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AffectedFrame\",\n                    \"description\": \"Information about the frame affected by an inspector issue.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CookieExclusionReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ExcludeSameSiteUnspecifiedTreatedAsLax\",\n                        \"ExcludeSameSiteNoneInsecure\",\n                        \"ExcludeSameSiteLax\",\n                        \"ExcludeSameSiteStrict\",\n                        \"ExcludeDomainNonASCII\",\n                        \"ExcludeThirdPartyCookieBlockedInFirstPartySet\",\n                        \"ExcludeThirdPartyPhaseout\",\n                        \"ExcludePortMismatch\",\n                        \"ExcludeSchemeMismatch\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieWarningReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"WarnSameSiteUnspecifiedCrossSiteContext\",\n                        \"WarnSameSiteNoneInsecure\",\n                        \"WarnSameSiteUnspecifiedLaxAllowUnsafe\",\n                        \"WarnSameSiteStrictLaxDowngradeStrict\",\n                        \"WarnSameSiteStrictCrossDowngradeStrict\",\n                        \"WarnSameSiteStrictCrossDowngradeLax\",\n                        \"WarnSameSiteLaxCrossDowngradeStrict\",\n                        \"WarnSameSiteLaxCrossDowngradeLax\",\n                        \"WarnAttributeValueExceedsMaxSize\",\n                        \"WarnDomainNonASCII\",\n                        \"WarnThirdPartyPhaseout\",\n                        \"WarnCrossSiteRedirectDowngradeChangesInclusion\",\n                        \"WarnDeprecationTrialMetadata\",\n                        \"WarnThirdPartyCookieHeuristic\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieOperation\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SetCookie\",\n                        \"ReadCookie\"\n                    ]\n                },\n                {\n                    \"id\": \"InsightType\",\n                    \"description\": \"Represents the category of insight that a cookie issue falls under.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"GitHubResource\",\n                        \"GracePeriod\",\n                        \"Heuristics\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieIssueInsight\",\n                    \"description\": \"Information about the suggested solution to a cookie issue.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"InsightType\"\n                        },\n                        {\n                            \"name\": \"tableEntryUrl\",\n                            \"description\": \"Link to table entry in third-party cookie migration readiness list.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CookieIssueDetails\",\n                    \"description\": \"This information is currently necessary, as the front-end has a difficult\\ntime finding a specific cookie. With this, we can convey specific error\\ninformation without the cookie.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"cookie\",\n                            \"description\": \"If AffectedCookie is not set then rawCookieLine contains the raw\\nSet-Cookie header string. This hints at a problem where the\\ncookie line is syntactically or semantically malformed in a way\\nthat no valid cookie could be created.\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedCookie\"\n                        },\n                        {\n                            \"name\": \"rawCookieLine\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cookieWarningReasons\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CookieWarningReason\"\n                            }\n                        },\n                        {\n                            \"name\": \"cookieExclusionReasons\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CookieExclusionReason\"\n                            }\n                        },\n                        {\n                            \"name\": \"operation\",\n                            \"description\": \"Optionally identifies the site-for-cookies and the cookie url, which\\nmay be used by the front-end as additional context.\",\n                            \"$ref\": \"CookieOperation\"\n                        },\n                        {\n                            \"name\": \"siteForCookies\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cookieUrl\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedRequest\"\n                        },\n                        {\n                            \"name\": \"insight\",\n                            \"description\": \"The recommended solution to the issue.\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieIssueInsight\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PerformanceIssueType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"DocumentCookie\"\n                    ]\n                },\n                {\n                    \"id\": \"PerformanceIssueDetails\",\n                    \"description\": \"Details for a performance issue.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"performanceIssueType\",\n                            \"$ref\": \"PerformanceIssueType\"\n                        },\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceCodeLocation\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"MixedContentResolutionStatus\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"MixedContentBlocked\",\n                        \"MixedContentAutomaticallyUpgraded\",\n                        \"MixedContentWarning\"\n                    ]\n                },\n                {\n                    \"id\": \"MixedContentResourceType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"AttributionSrc\",\n                        \"Audio\",\n                        \"Beacon\",\n                        \"CSPReport\",\n                        \"Download\",\n                        \"EventSource\",\n                        \"Favicon\",\n                        \"Font\",\n                        \"Form\",\n                        \"Frame\",\n                        \"Image\",\n                        \"Import\",\n                        \"JSON\",\n                        \"Manifest\",\n                        \"Ping\",\n                        \"PluginData\",\n                        \"PluginResource\",\n                        \"Prefetch\",\n                        \"Resource\",\n                        \"Script\",\n                        \"ServiceWorker\",\n                        \"SharedWorker\",\n                        \"SpeculationRules\",\n                        \"Stylesheet\",\n                        \"Track\",\n                        \"Video\",\n                        \"Worker\",\n                        \"XMLHttpRequest\",\n                        \"XSLT\"\n                    ]\n                },\n                {\n                    \"id\": \"MixedContentIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"resourceType\",\n                            \"description\": \"The type of resource causing the mixed content issue (css, js, iframe,\\nform,...). Marked as optional because it is mapped to from\\nblink::mojom::RequestContextType, which will be replaced\\nby network::mojom::RequestDestination\",\n                            \"optional\": true,\n                            \"$ref\": \"MixedContentResourceType\"\n                        },\n                        {\n                            \"name\": \"resolutionStatus\",\n                            \"description\": \"The way the mixed content issue is being resolved.\",\n                            \"$ref\": \"MixedContentResolutionStatus\"\n                        },\n                        {\n                            \"name\": \"insecureURL\",\n                            \"description\": \"The unsafe http url causing the mixed content issue.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mainResourceURL\",\n                            \"description\": \"The url responsible for the call to an unsafe url.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"The mixed content request.\\nDoes not always exist (e.g. for unsafe form submission urls).\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedRequest\"\n                        },\n                        {\n                            \"name\": \"frame\",\n                            \"description\": \"Optional because not every mixed content issue is necessarily linked to a frame.\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedFrame\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BlockedByResponseReason\",\n                    \"description\": \"Enum indicating the reason a response has been blocked. These reasons are\\nrefinements of the net error BLOCKED_BY_RESPONSE.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"CoepFrameResourceNeedsCoepHeader\",\n                        \"CoopSandboxedIFrameCannotNavigateToCoopPage\",\n                        \"CorpNotSameOrigin\",\n                        \"CorpNotSameOriginAfterDefaultedToSameOriginByCoep\",\n                        \"CorpNotSameOriginAfterDefaultedToSameOriginByDip\",\n                        \"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip\",\n                        \"CorpNotSameSite\",\n                        \"SRIMessageSignatureMismatch\"\n                    ]\n                },\n                {\n                    \"id\": \"BlockedByResponseIssueDetails\",\n                    \"description\": \"Details for a request that has been blocked with the BLOCKED_BY_RESPONSE\\ncode. Currently only used for COEP/COOP, but may be extended to include\\nsome CSP errors in the future.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"AffectedRequest\"\n                        },\n                        {\n                            \"name\": \"parentFrame\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedFrame\"\n                        },\n                        {\n                            \"name\": \"blockedFrame\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedFrame\"\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"$ref\": \"BlockedByResponseReason\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"HeavyAdResolutionStatus\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"HeavyAdBlocked\",\n                        \"HeavyAdWarning\"\n                    ]\n                },\n                {\n                    \"id\": \"HeavyAdReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"NetworkTotalLimit\",\n                        \"CpuTotalLimit\",\n                        \"CpuPeakLimit\"\n                    ]\n                },\n                {\n                    \"id\": \"HeavyAdIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"resolution\",\n                            \"description\": \"The resolution status, either blocking the content or warning.\",\n                            \"$ref\": \"HeavyAdResolutionStatus\"\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"The reason the ad was blocked, total network or cpu or peak cpu.\",\n                            \"$ref\": \"HeavyAdReason\"\n                        },\n                        {\n                            \"name\": \"frame\",\n                            \"description\": \"The frame that was blocked.\",\n                            \"$ref\": \"AffectedFrame\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContentSecurityPolicyViolationType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"kInlineViolation\",\n                        \"kEvalViolation\",\n                        \"kURLViolation\",\n                        \"kSRIViolation\",\n                        \"kTrustedTypesSinkViolation\",\n                        \"kTrustedTypesPolicyViolation\",\n                        \"kWasmEvalViolation\"\n                    ]\n                },\n                {\n                    \"id\": \"SourceCodeLocation\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContentSecurityPolicyIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"blockedURL\",\n                            \"description\": \"The url not included in allowed sources.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"violatedDirective\",\n                            \"description\": \"Specific directive that is violated, causing the CSP issue.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isReportOnly\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"contentSecurityPolicyViolationType\",\n                            \"$ref\": \"ContentSecurityPolicyViolationType\"\n                        },\n                        {\n                            \"name\": \"frameAncestor\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedFrame\"\n                        },\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"violatingNodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedArrayBufferIssueType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"TransferIssue\",\n                        \"CreationIssue\"\n                    ]\n                },\n                {\n                    \"id\": \"SharedArrayBufferIssueDetails\",\n                    \"description\": \"Details for a issue arising from an SAB being instantiated in, or\\ntransferred to a context that is not cross-origin isolated.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"isWarning\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"SharedArrayBufferIssueType\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LowTextContrastIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"violatingNodeId\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"violatingNodeSelector\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contrastRatio\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"thresholdAA\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"thresholdAAA\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"fontSize\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontWeight\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CorsIssueDetails\",\n                    \"description\": \"Details for a CORS related issue, e.g. a warning or error related to\\nCORS RFC1918 enforcement.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"corsErrorStatus\",\n                            \"$ref\": \"Network.CorsErrorStatus\"\n                        },\n                        {\n                            \"name\": \"isWarning\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"AffectedRequest\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"initiatorOrigin\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"resourceIPAddressSpace\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.IPAddressSpace\"\n                        },\n                        {\n                            \"name\": \"clientSecurityState\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.ClientSecurityState\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingIssueType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"PermissionPolicyDisabled\",\n                        \"UntrustworthyReportingOrigin\",\n                        \"InsecureContext\",\n                        \"InvalidHeader\",\n                        \"InvalidRegisterTriggerHeader\",\n                        \"SourceAndTriggerHeaders\",\n                        \"SourceIgnored\",\n                        \"TriggerIgnored\",\n                        \"OsSourceIgnored\",\n                        \"OsTriggerIgnored\",\n                        \"InvalidRegisterOsSourceHeader\",\n                        \"InvalidRegisterOsTriggerHeader\",\n                        \"WebAndOsHeaders\",\n                        \"NoWebOrOsSupport\",\n                        \"NavigationRegistrationWithoutTransientUserActivation\",\n                        \"InvalidInfoHeader\",\n                        \"NoRegisterSourceHeader\",\n                        \"NoRegisterTriggerHeader\",\n                        \"NoRegisterOsSourceHeader\",\n                        \"NoRegisterOsTriggerHeader\",\n                        \"NavigationRegistrationUniqueScopeAlreadySet\"\n                    ]\n                },\n                {\n                    \"id\": \"SharedDictionaryError\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"UseErrorCrossOriginNoCorsRequest\",\n                        \"UseErrorDictionaryLoadFailure\",\n                        \"UseErrorMatchingDictionaryNotUsed\",\n                        \"UseErrorUnexpectedContentDictionaryHeader\",\n                        \"WriteErrorCossOriginNoCorsRequest\",\n                        \"WriteErrorDisallowedBySettings\",\n                        \"WriteErrorExpiredResponse\",\n                        \"WriteErrorFeatureDisabled\",\n                        \"WriteErrorInsufficientResources\",\n                        \"WriteErrorInvalidMatchField\",\n                        \"WriteErrorInvalidStructuredHeader\",\n                        \"WriteErrorInvalidTTLField\",\n                        \"WriteErrorNavigationRequest\",\n                        \"WriteErrorNoMatchField\",\n                        \"WriteErrorNonIntegerTTLField\",\n                        \"WriteErrorNonListMatchDestField\",\n                        \"WriteErrorNonSecureContext\",\n                        \"WriteErrorNonStringIdField\",\n                        \"WriteErrorNonStringInMatchDestList\",\n                        \"WriteErrorNonStringMatchField\",\n                        \"WriteErrorNonTokenTypeField\",\n                        \"WriteErrorRequestAborted\",\n                        \"WriteErrorShuttingDown\",\n                        \"WriteErrorTooLongIdField\",\n                        \"WriteErrorUnsupportedType\"\n                    ]\n                },\n                {\n                    \"id\": \"SRIMessageSignatureError\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"MissingSignatureHeader\",\n                        \"MissingSignatureInputHeader\",\n                        \"InvalidSignatureHeader\",\n                        \"InvalidSignatureInputHeader\",\n                        \"SignatureHeaderValueIsNotByteSequence\",\n                        \"SignatureHeaderValueIsParameterized\",\n                        \"SignatureHeaderValueIsIncorrectLength\",\n                        \"SignatureInputHeaderMissingLabel\",\n                        \"SignatureInputHeaderValueNotInnerList\",\n                        \"SignatureInputHeaderValueMissingComponents\",\n                        \"SignatureInputHeaderInvalidComponentType\",\n                        \"SignatureInputHeaderInvalidComponentName\",\n                        \"SignatureInputHeaderInvalidHeaderComponentParameter\",\n                        \"SignatureInputHeaderInvalidDerivedComponentParameter\",\n                        \"SignatureInputHeaderKeyIdLength\",\n                        \"SignatureInputHeaderInvalidParameter\",\n                        \"SignatureInputHeaderMissingRequiredParameters\",\n                        \"ValidationFailedSignatureExpired\",\n                        \"ValidationFailedInvalidLength\",\n                        \"ValidationFailedSignatureMismatch\",\n                        \"ValidationFailedIntegrityMismatch\"\n                    ]\n                },\n                {\n                    \"id\": \"UnencodedDigestError\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"MalformedDictionary\",\n                        \"UnknownAlgorithm\",\n                        \"IncorrectDigestType\",\n                        \"IncorrectDigestLength\"\n                    ]\n                },\n                {\n                    \"id\": \"ConnectionAllowlistError\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"InvalidHeader\",\n                        \"MoreThanOneList\",\n                        \"ItemNotInnerList\",\n                        \"InvalidAllowlistItemType\",\n                        \"ReportingEndpointNotToken\",\n                        \"InvalidUrlPattern\"\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingIssueDetails\",\n                    \"description\": \"Details for issues around \\\"Attribution Reporting API\\\" usage.\\nExplainer: https://github.com/WICG/attribution-reporting-api\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"violationType\",\n                            \"$ref\": \"AttributionReportingIssueType\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedRequest\"\n                        },\n                        {\n                            \"name\": \"violatingNodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"invalidParameter\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"QuirksModeIssueDetails\",\n                    \"description\": \"Details for issues about documents in Quirks Mode\\nor Limited Quirks Mode that affects page layouting.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"isLimitedQuirksMode\",\n                            \"description\": \"If false, it means the document's mode is \\\"quirks\\\"\\ninstead of \\\"limited-quirks\\\".\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"documentNodeId\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"$ref\": \"Network.LoaderId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"NavigatorUserAgentIssueDetails\",\n                    \"deprecated\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceCodeLocation\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedDictionaryIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sharedDictionaryError\",\n                            \"$ref\": \"SharedDictionaryError\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"AffectedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SRIMessageSignatureIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"error\",\n                            \"$ref\": \"SRIMessageSignatureError\"\n                        },\n                        {\n                            \"name\": \"signatureBase\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"integrityAssertions\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"AffectedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"UnencodedDigestIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"error\",\n                            \"$ref\": \"UnencodedDigestError\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"AffectedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ConnectionAllowlistIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"error\",\n                            \"$ref\": \"ConnectionAllowlistError\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"AffectedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"GenericIssueErrorType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"FormLabelForNameError\",\n                        \"FormDuplicateIdForInputError\",\n                        \"FormInputWithNoLabelError\",\n                        \"FormAutocompleteAttributeEmptyError\",\n                        \"FormEmptyIdAndNameAttributesForInputError\",\n                        \"FormAriaLabelledByToNonExistingIdError\",\n                        \"FormInputAssignedAutocompleteValueToIdOrNameAttributeError\",\n                        \"FormLabelHasNeitherForNorNestedInputError\",\n                        \"FormLabelForMatchesNonExistingIdError\",\n                        \"FormInputHasWrongButWellIntendedAutocompleteValueError\",\n                        \"ResponseWasBlockedByORB\",\n                        \"NavigationEntryMarkedSkippable\",\n                        \"AutofillAndManualTextPolicyControlledFeaturesInfo\",\n                        \"AutofillPolicyControlledFeatureInfo\",\n                        \"ManualTextPolicyControlledFeatureInfo\"\n                    ]\n                },\n                {\n                    \"id\": \"GenericIssueDetails\",\n                    \"description\": \"Depending on the concrete errorType, different properties are set.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"errorType\",\n                            \"description\": \"Issues with the same errorType are aggregated in the frontend.\",\n                            \"$ref\": \"GenericIssueErrorType\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"violatingNodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"violatingNodeAttribute\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeprecationIssueDetails\",\n                    \"description\": \"This issue tracks information needed to print a deprecation message.\\nhttps://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/third_party/blink/renderer/core/frame/deprecation/README.md\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"affectedFrame\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedFrame\"\n                        },\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"One of the deprecation names from third_party/blink/renderer/core/frame/deprecation/deprecation.json5\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BounceTrackingIssueDetails\",\n                    \"description\": \"This issue warns about sites in the redirect chain of a finished navigation\\nthat may be flagged as trackers and have their state cleared if they don't\\nreceive a user interaction. Note that in this context 'site' means eTLD+1.\\nFor example, if the URL `https://example.test:80/bounce` was in the\\nredirect chain, the site reported would be `example.test`.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"trackingSites\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CookieDeprecationMetadataIssueDetails\",\n                    \"description\": \"This issue warns about third-party sites that are accessing cookies on the\\ncurrent page, and have been permitted due to having a global metadata grant.\\nNote that in this context 'site' means eTLD+1. For example, if the URL\\n`https://example.test:80/web_page` was accessing cookies, the site reported\\nwould be `example.test`.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"allowedSites\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"optOutPercentage\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"isOptOutTopLevel\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"operation\",\n                            \"$ref\": \"CookieOperation\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ClientHintIssueReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"MetaTagAllowListInvalidOrigin\",\n                        \"MetaTagModifiedHTML\"\n                    ]\n                },\n                {\n                    \"id\": \"FederatedAuthRequestIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"federatedAuthRequestIssueReason\",\n                            \"$ref\": \"FederatedAuthRequestIssueReason\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FederatedAuthRequestIssueReason\",\n                    \"description\": \"Represents the failure reason when a federated authentication reason fails.\\nShould be updated alongside RequestIdTokenStatus in\\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom to include\\nall cases except for success.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ShouldEmbargo\",\n                        \"TooManyRequests\",\n                        \"WellKnownHttpNotFound\",\n                        \"WellKnownNoResponse\",\n                        \"WellKnownInvalidResponse\",\n                        \"WellKnownListEmpty\",\n                        \"WellKnownInvalidContentType\",\n                        \"ConfigNotInWellKnown\",\n                        \"WellKnownTooBig\",\n                        \"ConfigHttpNotFound\",\n                        \"ConfigNoResponse\",\n                        \"ConfigInvalidResponse\",\n                        \"ConfigInvalidContentType\",\n                        \"IdpNotPotentiallyTrustworthy\",\n                        \"DisabledInSettings\",\n                        \"DisabledInFlags\",\n                        \"ErrorFetchingSignin\",\n                        \"InvalidSigninResponse\",\n                        \"AccountsHttpNotFound\",\n                        \"AccountsNoResponse\",\n                        \"AccountsInvalidResponse\",\n                        \"AccountsListEmpty\",\n                        \"AccountsInvalidContentType\",\n                        \"IdTokenHttpNotFound\",\n                        \"IdTokenNoResponse\",\n                        \"IdTokenInvalidResponse\",\n                        \"IdTokenIdpErrorResponse\",\n                        \"IdTokenCrossSiteIdpErrorResponse\",\n                        \"IdTokenInvalidRequest\",\n                        \"IdTokenInvalidContentType\",\n                        \"ErrorIdToken\",\n                        \"Canceled\",\n                        \"RpPageNotVisible\",\n                        \"SilentMediationFailure\",\n                        \"NotSignedInWithIdp\",\n                        \"MissingTransientUserActivation\",\n                        \"ReplacedByActiveMode\",\n                        \"RelyingPartyOriginIsOpaque\",\n                        \"TypeNotMatching\",\n                        \"UiDismissedNoEmbargo\",\n                        \"CorsError\",\n                        \"SuppressedBySegmentationPlatform\"\n                    ]\n                },\n                {\n                    \"id\": \"FederatedAuthUserInfoRequestIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"federatedAuthUserInfoRequestIssueReason\",\n                            \"$ref\": \"FederatedAuthUserInfoRequestIssueReason\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FederatedAuthUserInfoRequestIssueReason\",\n                    \"description\": \"Represents the failure reason when a getUserInfo() call fails.\\nShould be updated alongside FederatedAuthUserInfoRequestResult in\\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"NotSameOrigin\",\n                        \"NotIframe\",\n                        \"NotPotentiallyTrustworthy\",\n                        \"NoApiPermission\",\n                        \"NotSignedInWithIdp\",\n                        \"NoAccountSharingPermission\",\n                        \"InvalidConfigOrWellKnown\",\n                        \"InvalidAccountsResponse\",\n                        \"NoReturningUserFromFetchedAccounts\"\n                    ]\n                },\n                {\n                    \"id\": \"ClientHintIssueDetails\",\n                    \"description\": \"This issue tracks client hints related issues. It's used to deprecate old\\nfeatures, encourage the use of new ones, and provide general guidance.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"clientHintIssueReason\",\n                            \"$ref\": \"ClientHintIssueReason\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FailedRequestInfo\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The URL that failed to load.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"failureMessage\",\n                            \"description\": \"The failure message for the failed request.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PartitioningBlobURLInfo\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"BlockedCrossPartitionFetching\",\n                        \"EnforceNoopenerForNavigation\"\n                    ]\n                },\n                {\n                    \"id\": \"PartitioningBlobURLIssueDetails\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The BlobURL that failed to load.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"partitioningBlobURLInfo\",\n                            \"description\": \"Additional information about the Partitioning Blob URL issue.\",\n                            \"$ref\": \"PartitioningBlobURLInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ElementAccessibilityIssueReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"DisallowedSelectChild\",\n                        \"DisallowedOptGroupChild\",\n                        \"NonPhrasingContentOptionChild\",\n                        \"InteractiveContentOptionChild\",\n                        \"InteractiveContentLegendChild\",\n                        \"InteractiveContentSummaryDescendant\"\n                    ]\n                },\n                {\n                    \"id\": \"ElementAccessibilityIssueDetails\",\n                    \"description\": \"This issue warns about errors in the select or summary element content model.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"elementAccessibilityIssueReason\",\n                            \"$ref\": \"ElementAccessibilityIssueReason\"\n                        },\n                        {\n                            \"name\": \"hasDisallowedAttributes\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StyleSheetLoadingIssueReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"LateImportRule\",\n                        \"RequestFailed\"\n                    ]\n                },\n                {\n                    \"id\": \"StylesheetLoadingIssueDetails\",\n                    \"description\": \"This issue warns when a referenced stylesheet couldn't be loaded.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"description\": \"Source code position that referenced the failing stylesheet.\",\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"styleSheetLoadingIssueReason\",\n                            \"description\": \"Reason why the stylesheet couldn't be loaded.\",\n                            \"$ref\": \"StyleSheetLoadingIssueReason\"\n                        },\n                        {\n                            \"name\": \"failedRequestInfo\",\n                            \"description\": \"Contains additional info when the failure was due to a request.\",\n                            \"optional\": true,\n                            \"$ref\": \"FailedRequestInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PropertyRuleIssueReason\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"InvalidSyntax\",\n                        \"InvalidInitialValue\",\n                        \"InvalidInherits\",\n                        \"InvalidName\"\n                    ]\n                },\n                {\n                    \"id\": \"PropertyRuleIssueDetails\",\n                    \"description\": \"This issue warns about errors in property rules that lead to property\\nregistrations being ignored.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"description\": \"Source code position of the property rule.\",\n                            \"$ref\": \"SourceCodeLocation\"\n                        },\n                        {\n                            \"name\": \"propertyRuleIssueReason\",\n                            \"description\": \"Reason why the property rule was discarded.\",\n                            \"$ref\": \"PropertyRuleIssueReason\"\n                        },\n                        {\n                            \"name\": \"propertyValue\",\n                            \"description\": \"The value of the property rule property that failed to parse\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"UserReidentificationIssueType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"BlockedFrameNavigation\",\n                        \"BlockedSubresource\",\n                        \"NoisedCanvasReadback\"\n                    ]\n                },\n                {\n                    \"id\": \"UserReidentificationIssueDetails\",\n                    \"description\": \"This issue warns about uses of APIs that may be considered misuse to\\nre-identify users.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"UserReidentificationIssueType\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"Applies to BlockedFrameNavigation and BlockedSubresource issue types.\",\n                            \"optional\": true,\n                            \"$ref\": \"AffectedRequest\"\n                        },\n                        {\n                            \"name\": \"sourceCodeLocation\",\n                            \"description\": \"Applies to NoisedCanvasReadback issue type.\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceCodeLocation\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PermissionElementIssueType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"InvalidType\",\n                        \"FencedFrameDisallowed\",\n                        \"CspFrameAncestorsMissing\",\n                        \"PermissionsPolicyBlocked\",\n                        \"PaddingRightUnsupported\",\n                        \"PaddingBottomUnsupported\",\n                        \"InsetBoxShadowUnsupported\",\n                        \"RequestInProgress\",\n                        \"UntrustedEvent\",\n                        \"RegistrationFailed\",\n                        \"TypeNotSupported\",\n                        \"InvalidTypeActivation\",\n                        \"SecurityChecksFailed\",\n                        \"ActivationDisabled\",\n                        \"GeolocationDeprecated\",\n                        \"InvalidDisplayStyle\",\n                        \"NonOpaqueColor\",\n                        \"LowContrast\",\n                        \"FontSizeTooSmall\",\n                        \"FontSizeTooLarge\",\n                        \"InvalidSizeValue\"\n                    ]\n                },\n                {\n                    \"id\": \"PermissionElementIssueDetails\",\n                    \"description\": \"This issue warns about improper usage of the <permission> element.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"issueType\",\n                            \"$ref\": \"PermissionElementIssueType\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"The value of the type attribute.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The node ID of the <permission> element.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"isWarning\",\n                            \"description\": \"True if the issue is a warning, false if it is an error.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"permissionName\",\n                            \"description\": \"Fields for message construction:\\nUsed for messages that reference a specific permission name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"occluderNodeInfo\",\n                            \"description\": \"Used for messages about occlusion\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"occluderParentNodeInfo\",\n                            \"description\": \"Used for messages about occluder's parent\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"disableReason\",\n                            \"description\": \"Used for messages about activation disabled reason\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AdScriptIdentifier\",\n                    \"description\": \"Metadata about the ad script that was on the stack that caused the current\\nscript in the `AdAncestry` to be considered ad related.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"The script's v8 identifier.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"debuggerId\",\n                            \"description\": \"v8's debugging id for the v8::Context.\",\n                            \"$ref\": \"Runtime.UniqueDebuggerId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The script's url (or generated name based on id if inline script).\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AdAncestry\",\n                    \"description\": \"Providence about how an ad script was determined to be such. It is an ad\\nbecause its url matched a filterlist rule, or because some other ad script\\nwas on the stack when this script was loaded.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"adAncestryChain\",\n                            \"description\": \"The ad-script in the stack when the offending script was loaded. This is\\nrecursive down to the root script that was tagged due to the filterlist\\nrule.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AdScriptIdentifier\"\n                            }\n                        },\n                        {\n                            \"name\": \"rootScriptFilterlistRule\",\n                            \"description\": \"The filterlist rule that caused the root (last) script in\\n`adAncestry` to be ad-tagged.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SelectivePermissionsInterventionIssueDetails\",\n                    \"description\": \"The issue warns about blocked calls to privacy sensitive APIs via the\\nSelective Permissions Intervention.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"apiName\",\n                            \"description\": \"Which API was intervened on.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"adAncestry\",\n                            \"description\": \"Why the ad script using the API is considered an ad.\",\n                            \"$ref\": \"AdAncestry\"\n                        },\n                        {\n                            \"name\": \"stackTrace\",\n                            \"description\": \"The stack trace at the time of the intervention.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InspectorIssueCode\",\n                    \"description\": \"A unique identifier for the type of issue. Each type may use one of the\\noptional fields in InspectorIssueDetails to convey more specific\\ninformation about the kind of issue.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"CookieIssue\",\n                        \"MixedContentIssue\",\n                        \"BlockedByResponseIssue\",\n                        \"HeavyAdIssue\",\n                        \"ContentSecurityPolicyIssue\",\n                        \"SharedArrayBufferIssue\",\n                        \"LowTextContrastIssue\",\n                        \"CorsIssue\",\n                        \"AttributionReportingIssue\",\n                        \"QuirksModeIssue\",\n                        \"PartitioningBlobURLIssue\",\n                        \"NavigatorUserAgentIssue\",\n                        \"GenericIssue\",\n                        \"DeprecationIssue\",\n                        \"ClientHintIssue\",\n                        \"FederatedAuthRequestIssue\",\n                        \"BounceTrackingIssue\",\n                        \"CookieDeprecationMetadataIssue\",\n                        \"StylesheetLoadingIssue\",\n                        \"FederatedAuthUserInfoRequestIssue\",\n                        \"PropertyRuleIssue\",\n                        \"SharedDictionaryIssue\",\n                        \"ElementAccessibilityIssue\",\n                        \"SRIMessageSignatureIssue\",\n                        \"UnencodedDigestIssue\",\n                        \"ConnectionAllowlistIssue\",\n                        \"UserReidentificationIssue\",\n                        \"PermissionElementIssue\",\n                        \"PerformanceIssue\",\n                        \"SelectivePermissionsInterventionIssue\"\n                    ]\n                },\n                {\n                    \"id\": \"InspectorIssueDetails\",\n                    \"description\": \"This struct holds a list of optional fields with additional information\\nspecific to the kind of issue. When adding a new issue code, please also\\nadd a new optional field to this type.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"cookieIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieIssueDetails\"\n                        },\n                        {\n                            \"name\": \"mixedContentIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"MixedContentIssueDetails\"\n                        },\n                        {\n                            \"name\": \"blockedByResponseIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"BlockedByResponseIssueDetails\"\n                        },\n                        {\n                            \"name\": \"heavyAdIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"HeavyAdIssueDetails\"\n                        },\n                        {\n                            \"name\": \"contentSecurityPolicyIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"ContentSecurityPolicyIssueDetails\"\n                        },\n                        {\n                            \"name\": \"sharedArrayBufferIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"SharedArrayBufferIssueDetails\"\n                        },\n                        {\n                            \"name\": \"lowTextContrastIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"LowTextContrastIssueDetails\"\n                        },\n                        {\n                            \"name\": \"corsIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"CorsIssueDetails\"\n                        },\n                        {\n                            \"name\": \"attributionReportingIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"AttributionReportingIssueDetails\"\n                        },\n                        {\n                            \"name\": \"quirksModeIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"QuirksModeIssueDetails\"\n                        },\n                        {\n                            \"name\": \"partitioningBlobURLIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"PartitioningBlobURLIssueDetails\"\n                        },\n                        {\n                            \"name\": \"navigatorUserAgentIssueDetails\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"NavigatorUserAgentIssueDetails\"\n                        },\n                        {\n                            \"name\": \"genericIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"GenericIssueDetails\"\n                        },\n                        {\n                            \"name\": \"deprecationIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"DeprecationIssueDetails\"\n                        },\n                        {\n                            \"name\": \"clientHintIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"ClientHintIssueDetails\"\n                        },\n                        {\n                            \"name\": \"federatedAuthRequestIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"FederatedAuthRequestIssueDetails\"\n                        },\n                        {\n                            \"name\": \"bounceTrackingIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"BounceTrackingIssueDetails\"\n                        },\n                        {\n                            \"name\": \"cookieDeprecationMetadataIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieDeprecationMetadataIssueDetails\"\n                        },\n                        {\n                            \"name\": \"stylesheetLoadingIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"StylesheetLoadingIssueDetails\"\n                        },\n                        {\n                            \"name\": \"propertyRuleIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"PropertyRuleIssueDetails\"\n                        },\n                        {\n                            \"name\": \"federatedAuthUserInfoRequestIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"FederatedAuthUserInfoRequestIssueDetails\"\n                        },\n                        {\n                            \"name\": \"sharedDictionaryIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"SharedDictionaryIssueDetails\"\n                        },\n                        {\n                            \"name\": \"elementAccessibilityIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"ElementAccessibilityIssueDetails\"\n                        },\n                        {\n                            \"name\": \"sriMessageSignatureIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"SRIMessageSignatureIssueDetails\"\n                        },\n                        {\n                            \"name\": \"unencodedDigestIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"UnencodedDigestIssueDetails\"\n                        },\n                        {\n                            \"name\": \"connectionAllowlistIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"ConnectionAllowlistIssueDetails\"\n                        },\n                        {\n                            \"name\": \"userReidentificationIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"UserReidentificationIssueDetails\"\n                        },\n                        {\n                            \"name\": \"permissionElementIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"PermissionElementIssueDetails\"\n                        },\n                        {\n                            \"name\": \"performanceIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"PerformanceIssueDetails\"\n                        },\n                        {\n                            \"name\": \"selectivePermissionsInterventionIssueDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"SelectivePermissionsInterventionIssueDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"IssueId\",\n                    \"description\": \"A unique id for a DevTools inspector issue. Allows other entities (e.g.\\nexceptions, CDP message, console messages, etc.) to reference an issue.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"InspectorIssue\",\n                    \"description\": \"An inspector issue reported from the back-end.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"code\",\n                            \"$ref\": \"InspectorIssueCode\"\n                        },\n                        {\n                            \"name\": \"details\",\n                            \"$ref\": \"InspectorIssueDetails\"\n                        },\n                        {\n                            \"name\": \"issueId\",\n                            \"description\": \"A unique id for this issue. May be omitted if no other entity (e.g.\\nexception, CDP message, etc.) is referencing this issue.\",\n                            \"optional\": true,\n                            \"$ref\": \"IssueId\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getEncodedResponse\",\n                    \"description\": \"Returns the response body and size if it were re-encoded with the specified settings. Only\\napplies to images.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier of the network request to get content for.\",\n                            \"$ref\": \"Network.RequestId\"\n                        },\n                        {\n                            \"name\": \"encoding\",\n                            \"description\": \"The encoding to use.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"webp\",\n                                \"jpeg\",\n                                \"png\"\n                            ]\n                        },\n                        {\n                            \"name\": \"quality\",\n                            \"description\": \"The quality of the encoding (0-1). (defaults to 1)\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sizeOnly\",\n                            \"description\": \"Whether to only return the size information (defaults to false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"body\",\n                            \"description\": \"The encoded body as a base64 string. Omitted if sizeOnly is true. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"originalSize\",\n                            \"description\": \"Size before re-encoding.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"encodedSize\",\n                            \"description\": \"Size after re-encoding.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables issues domain, prevents further issues from being reported to the client.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables issues domain, sends the issues collected so far to the client by means of the\\n`issueAdded` event.\"\n                },\n                {\n                    \"name\": \"checkContrast\",\n                    \"description\": \"Runs the contrast check for the target page. Found issues are reported\\nusing Audits.issueAdded event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"reportAAA\",\n                            \"description\": \"Whether to report WCAG AAA level issues. Default is false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"checkFormsIssues\",\n                    \"description\": \"Runs the form issues check for the target page. Found issues are reported\\nusing Audits.issueAdded event.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"formIssues\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"GenericIssueDetails\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"issueAdded\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"issue\",\n                            \"$ref\": \"InspectorIssue\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Autofill\",\n            \"description\": \"Defines commands and events for Autofill.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"CreditCard\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"number\",\n                            \"description\": \"16-digit credit card number.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name of the credit card owner.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"expiryMonth\",\n                            \"description\": \"2-digit expiry month.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"expiryYear\",\n                            \"description\": \"4-digit expiry year.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cvc\",\n                            \"description\": \"3-digit card verification code.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AddressField\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"address field name, for example GIVEN_NAME.\\nThe full list of supported field names:\\nhttps://source.chromium.org/chromium/chromium/src/+/main:components/autofill/core/browser/field_types.cc;l=38\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"address field value, for example Jon Doe.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AddressFields\",\n                    \"description\": \"A list of address fields.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"fields\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AddressField\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Address\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"fields\",\n                            \"description\": \"fields and values defining an address.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AddressField\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AddressUI\",\n                    \"description\": \"Defines how an address can be displayed like in chrome://settings/addresses.\\nAddress UI is a two dimensional array, each inner array is an \\\"address information line\\\", and when rendered in a UI surface should be displayed as such.\\nThe following address UI for instance:\\n[[{name: \\\"GIVE_NAME\\\", value: \\\"Jon\\\"}, {name: \\\"FAMILY_NAME\\\", value: \\\"Doe\\\"}], [{name: \\\"CITY\\\", value: \\\"Munich\\\"}, {name: \\\"ZIP\\\", value: \\\"81456\\\"}]]\\nshould allow the receiver to render:\\nJon Doe\\nMunich 81456\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"addressFields\",\n                            \"description\": \"A two dimension array containing the representation of values from an address profile.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AddressFields\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FillingStrategy\",\n                    \"description\": \"Specified whether a filled field was done so by using the html autocomplete attribute or autofill heuristics.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"autocompleteAttribute\",\n                        \"autofillInferred\"\n                    ]\n                },\n                {\n                    \"id\": \"FilledField\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"htmlType\",\n                            \"description\": \"The type of the field, e.g text, password etc.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"the html id\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"the html name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"the field value\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"autofillType\",\n                            \"description\": \"The actual field type, e.g FAMILY_NAME\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fillingStrategy\",\n                            \"description\": \"The filling strategy\",\n                            \"$ref\": \"FillingStrategy\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The frame the field belongs to\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"fieldId\",\n                            \"description\": \"The form field's DOM node\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"addressFormFilled\",\n                    \"description\": \"Emitted when an address form is filled.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"filledFields\",\n                            \"description\": \"Information about the fields that were filled\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FilledField\"\n                            }\n                        },\n                        {\n                            \"name\": \"addressUi\",\n                            \"description\": \"An UI representation of the address used to fill the form.\\nConsists of a 2D array where each child represents an address/profile line.\",\n                            \"$ref\": \"AddressUI\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"trigger\",\n                    \"description\": \"Trigger autofill on a form identified by the fieldId.\\nIf the field and related form cannot be autofilled, returns an error.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"fieldId\",\n                            \"description\": \"Identifies a field that serves as an anchor for autofill.\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Identifies the frame that field belongs to.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"card\",\n                            \"description\": \"Credit card information to fill out the form. Credit card data is not saved.  Mutually exclusive with `address`.\",\n                            \"optional\": true,\n                            \"$ref\": \"CreditCard\"\n                        },\n                        {\n                            \"name\": \"address\",\n                            \"description\": \"Address to fill out the form. Address data is not saved. Mutually exclusive with `card`.\",\n                            \"optional\": true,\n                            \"$ref\": \"Address\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAddresses\",\n                    \"description\": \"Set addresses so that developers can verify their forms implementation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"addresses\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Address\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables autofill domain notifications.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables autofill domain notifications.\"\n                }\n            ]\n        },\n        {\n            \"domain\": \"BackgroundService\",\n            \"description\": \"Defines events for background web platform features.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"ServiceName\",\n                    \"description\": \"The Background Service that will be associated with the commands/events.\\nEvery Background Service operates independently, but they share the same\\nAPI.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"backgroundFetch\",\n                        \"backgroundSync\",\n                        \"pushMessaging\",\n                        \"notifications\",\n                        \"paymentHandler\",\n                        \"periodicBackgroundSync\"\n                    ]\n                },\n                {\n                    \"id\": \"EventMetadata\",\n                    \"description\": \"A key-value pair for additional event information to pass along.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BackgroundServiceEvent\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp of the event (in seconds).\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"The origin this event belongs to.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"serviceWorkerRegistrationId\",\n                            \"description\": \"The Service Worker ID that initiated the event.\",\n                            \"$ref\": \"ServiceWorker.RegistrationID\"\n                        },\n                        {\n                            \"name\": \"service\",\n                            \"description\": \"The Background Service this event belongs to.\",\n                            \"$ref\": \"ServiceName\"\n                        },\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"A description of the event.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"instanceId\",\n                            \"description\": \"An identifier that groups related events together.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"eventMetadata\",\n                            \"description\": \"A list of event-specific information.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"EventMetadata\"\n                            }\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key this event belongs to.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"startObserving\",\n                    \"description\": \"Enables event updates for the service.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"service\",\n                            \"$ref\": \"ServiceName\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopObserving\",\n                    \"description\": \"Disables event updates for the service.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"service\",\n                            \"$ref\": \"ServiceName\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setRecording\",\n                    \"description\": \"Set the recording state for the service.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"shouldRecord\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"service\",\n                            \"$ref\": \"ServiceName\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearEvents\",\n                    \"description\": \"Clears all stored data for the service.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"service\",\n                            \"$ref\": \"ServiceName\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"recordingStateChanged\",\n                    \"description\": \"Called when the recording state for the service has been updated.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"isRecording\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"service\",\n                            \"$ref\": \"ServiceName\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"backgroundServiceEventReceived\",\n                    \"description\": \"Called with all existing backgroundServiceEvents when enabled, and all new\\nevents afterwards if enabled and recording.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"backgroundServiceEvent\",\n                            \"$ref\": \"BackgroundServiceEvent\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"BluetoothEmulation\",\n            \"description\": \"This domain allows configuring virtual Bluetooth devices to test\\nthe web-bluetooth API.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"CentralState\",\n                    \"description\": \"Indicates the various states of Central.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"absent\",\n                        \"powered-off\",\n                        \"powered-on\"\n                    ]\n                },\n                {\n                    \"id\": \"GATTOperationType\",\n                    \"description\": \"Indicates the various types of GATT event.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"connection\",\n                        \"discovery\"\n                    ]\n                },\n                {\n                    \"id\": \"CharacteristicWriteType\",\n                    \"description\": \"Indicates the various types of characteristic write.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"write-default-deprecated\",\n                        \"write-with-response\",\n                        \"write-without-response\"\n                    ]\n                },\n                {\n                    \"id\": \"CharacteristicOperationType\",\n                    \"description\": \"Indicates the various types of characteristic operation.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"read\",\n                        \"write\",\n                        \"subscribe-to-notifications\",\n                        \"unsubscribe-from-notifications\"\n                    ]\n                },\n                {\n                    \"id\": \"DescriptorOperationType\",\n                    \"description\": \"Indicates the various types of descriptor operation.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"read\",\n                        \"write\"\n                    ]\n                },\n                {\n                    \"id\": \"ManufacturerData\",\n                    \"description\": \"Stores the manufacturer data\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"description\": \"Company identifier\\nhttps://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml\\nhttps://usb.org/developers\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Manufacturer-specific data (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScanRecord\",\n                    \"description\": \"Stores the byte data of the advertisement packet sent by a Bluetooth device.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"uuids\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"appearance\",\n                            \"description\": \"Stores the external appearance description of the device.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"txPower\",\n                            \"description\": \"Stores the transmission power of a broadcasting device.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"manufacturerData\",\n                            \"description\": \"Key is the company identifier and the value is an array of bytes of\\nmanufacturer specific data.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ManufacturerData\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScanEntry\",\n                    \"description\": \"Stores the advertisement packet information that is sent by a Bluetooth device.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"deviceAddress\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"rssi\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"scanRecord\",\n                            \"$ref\": \"ScanRecord\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CharacteristicProperties\",\n                    \"description\": \"Describes the properties of a characteristic. This follows Bluetooth Core\\nSpecification BT 4.2 Vol 3 Part G 3.3.1. Characteristic Properties.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"broadcast\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"read\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"writeWithoutResponse\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"write\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"notify\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"indicate\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"authenticatedSignedWrites\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"extendedProperties\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enable the BluetoothEmulation domain.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"state\",\n                            \"description\": \"State of the simulated central.\",\n                            \"$ref\": \"CentralState\"\n                        },\n                        {\n                            \"name\": \"leSupported\",\n                            \"description\": \"If the simulated central supports low-energy.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSimulatedCentralState\",\n                    \"description\": \"Set the state of the simulated central.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"state\",\n                            \"description\": \"State of the simulated central.\",\n                            \"$ref\": \"CentralState\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disable the BluetoothEmulation domain.\"\n                },\n                {\n                    \"name\": \"simulatePreconnectedPeripheral\",\n                    \"description\": \"Simulates a peripheral with |address|, |name| and |knownServiceUuids|\\nthat has already been connected to the system.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"address\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"manufacturerData\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ManufacturerData\"\n                            }\n                        },\n                        {\n                            \"name\": \"knownServiceUuids\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"simulateAdvertisement\",\n                    \"description\": \"Simulates an advertisement packet described in |entry| being received by\\nthe central.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"entry\",\n                            \"$ref\": \"ScanEntry\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"simulateGATTOperationResponse\",\n                    \"description\": \"Simulates the response code from the peripheral with |address| for a\\nGATT operation of |type|. The |code| value follows the HCI Error Codes from\\nBluetooth Core Specification Vol 2 Part D 1.3 List Of Error Codes.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"address\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"GATTOperationType\"\n                        },\n                        {\n                            \"name\": \"code\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"simulateCharacteristicOperationResponse\",\n                    \"description\": \"Simulates the response from the characteristic with |characteristicId| for a\\ncharacteristic operation of |type|. The |code| value follows the Error\\nCodes from Bluetooth Core Specification Vol 3 Part F 3.4.1.1 Error Response.\\nThe |data| is expected to exist when simulating a successful read operation\\nresponse.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"characteristicId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"CharacteristicOperationType\"\n                        },\n                        {\n                            \"name\": \"code\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"simulateDescriptorOperationResponse\",\n                    \"description\": \"Simulates the response from the descriptor with |descriptorId| for a\\ndescriptor operation of |type|. The |code| value follows the Error\\nCodes from Bluetooth Core Specification Vol 3 Part F 3.4.1.1 Error Response.\\nThe |data| is expected to exist when simulating a successful read operation\\nresponse.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"descriptorId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"DescriptorOperationType\"\n                        },\n                        {\n                            \"name\": \"code\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addService\",\n                    \"description\": \"Adds a service with |serviceUuid| to the peripheral with |address|.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"address\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"serviceUuid\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"serviceId\",\n                            \"description\": \"An identifier that uniquely represents this service.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeService\",\n                    \"description\": \"Removes the service respresented by |serviceId| from the simulated central.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"serviceId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addCharacteristic\",\n                    \"description\": \"Adds a characteristic with |characteristicUuid| and |properties| to the\\nservice represented by |serviceId|.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"serviceId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"characteristicUuid\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"properties\",\n                            \"$ref\": \"CharacteristicProperties\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"characteristicId\",\n                            \"description\": \"An identifier that uniquely represents this characteristic.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeCharacteristic\",\n                    \"description\": \"Removes the characteristic respresented by |characteristicId| from the\\nsimulated central.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"characteristicId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addDescriptor\",\n                    \"description\": \"Adds a descriptor with |descriptorUuid| to the characteristic respresented\\nby |characteristicId|.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"characteristicId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"descriptorUuid\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"descriptorId\",\n                            \"description\": \"An identifier that uniquely represents this descriptor.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeDescriptor\",\n                    \"description\": \"Removes the descriptor with |descriptorId| from the simulated central.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"descriptorId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"simulateGATTDisconnection\",\n                    \"description\": \"Simulates a GATT disconnection from the peripheral with |address|.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"address\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"gattOperationReceived\",\n                    \"description\": \"Event for when a GATT operation of |type| to the peripheral with |address|\\nhappened.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"address\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"GATTOperationType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"characteristicOperationReceived\",\n                    \"description\": \"Event for when a characteristic operation of |type| to the characteristic\\nrespresented by |characteristicId| happened. |data| and |writeType| is\\nexpected to exist when |type| is write.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"characteristicId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"CharacteristicOperationType\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"writeType\",\n                            \"optional\": true,\n                            \"$ref\": \"CharacteristicWriteType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"descriptorOperationReceived\",\n                    \"description\": \"Event for when a descriptor operation of |type| to the descriptor\\nrespresented by |descriptorId| happened. |data| is expected to exist when\\n|type| is write.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"descriptorId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"DescriptorOperationType\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Browser\",\n            \"description\": \"The Browser domain defines methods and events for browser managing.\",\n            \"types\": [\n                {\n                    \"id\": \"BrowserContextID\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"WindowID\",\n                    \"experimental\": true,\n                    \"type\": \"integer\"\n                },\n                {\n                    \"id\": \"WindowState\",\n                    \"description\": \"The state of the browser window.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"normal\",\n                        \"minimized\",\n                        \"maximized\",\n                        \"fullscreen\"\n                    ]\n                },\n                {\n                    \"id\": \"Bounds\",\n                    \"description\": \"Browser window bounds information\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"left\",\n                            \"description\": \"The offset from the left edge of the screen to the window in pixels.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"top\",\n                            \"description\": \"The offset from the top edge of the screen to the window in pixels.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"The window width in pixels.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"The window height in pixels.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"windowState\",\n                            \"description\": \"The window state. Default to normal.\",\n                            \"optional\": true,\n                            \"$ref\": \"WindowState\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PermissionType\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ar\",\n                        \"audioCapture\",\n                        \"automaticFullscreen\",\n                        \"backgroundFetch\",\n                        \"backgroundSync\",\n                        \"cameraPanTiltZoom\",\n                        \"capturedSurfaceControl\",\n                        \"clipboardReadWrite\",\n                        \"clipboardSanitizedWrite\",\n                        \"displayCapture\",\n                        \"durableStorage\",\n                        \"geolocation\",\n                        \"handTracking\",\n                        \"idleDetection\",\n                        \"keyboardLock\",\n                        \"localFonts\",\n                        \"localNetwork\",\n                        \"localNetworkAccess\",\n                        \"loopbackNetwork\",\n                        \"midi\",\n                        \"midiSysex\",\n                        \"nfc\",\n                        \"notifications\",\n                        \"paymentHandler\",\n                        \"periodicBackgroundSync\",\n                        \"pointerLock\",\n                        \"protectedMediaIdentifier\",\n                        \"sensors\",\n                        \"smartCard\",\n                        \"speakerSelection\",\n                        \"storageAccess\",\n                        \"topLevelStorageAccess\",\n                        \"videoCapture\",\n                        \"vr\",\n                        \"wakeLockScreen\",\n                        \"wakeLockSystem\",\n                        \"webAppInstallation\",\n                        \"webPrinting\",\n                        \"windowManagement\"\n                    ]\n                },\n                {\n                    \"id\": \"PermissionSetting\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"granted\",\n                        \"denied\",\n                        \"prompt\"\n                    ]\n                },\n                {\n                    \"id\": \"PermissionDescriptor\",\n                    \"description\": \"Definition of PermissionDescriptor defined in the Permissions API:\\nhttps://w3c.github.io/permissions/#dom-permissiondescriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name of permission.\\nSee https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sysex\",\n                            \"description\": \"For \\\"midi\\\" permission, may also specify sysex control.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"userVisibleOnly\",\n                            \"description\": \"For \\\"push\\\" permission, may specify userVisibleOnly.\\nNote that userVisibleOnly = true is the only currently supported type.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"allowWithoutSanitization\",\n                            \"description\": \"For \\\"clipboard\\\" permission, may specify allowWithoutSanitization.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"allowWithoutGesture\",\n                            \"description\": \"For \\\"fullscreen\\\" permission, must specify allowWithoutGesture:true.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"panTiltZoom\",\n                            \"description\": \"For \\\"camera\\\" permission, may specify panTiltZoom.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BrowserCommandId\",\n                    \"description\": \"Browser command ids used by executeBrowserCommand.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"openTabSearch\",\n                        \"closeTabSearch\",\n                        \"openGlic\"\n                    ]\n                },\n                {\n                    \"id\": \"Bucket\",\n                    \"description\": \"Chrome histogram bucket.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"low\",\n                            \"description\": \"Minimum value (inclusive).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"high\",\n                            \"description\": \"Maximum value (exclusive).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"count\",\n                            \"description\": \"Number of samples.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Histogram\",\n                    \"description\": \"Chrome histogram.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sum\",\n                            \"description\": \"Sum of sample values.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"count\",\n                            \"description\": \"Total number of samples.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"buckets\",\n                            \"description\": \"Buckets.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Bucket\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PrivacySandboxAPI\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"BiddingAndAuctionServices\",\n                        \"TrustedKeyValue\"\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"setPermission\",\n                    \"description\": \"Set permission settings for given embedding and embedded origins.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"permission\",\n                            \"description\": \"Descriptor of permission to override.\",\n                            \"$ref\": \"PermissionDescriptor\"\n                        },\n                        {\n                            \"name\": \"setting\",\n                            \"description\": \"Setting of the permission.\",\n                            \"$ref\": \"PermissionSetting\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Embedding origin the permission applies to, all origins if not specified.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"embeddedOrigin\",\n                            \"description\": \"Embedded origin the permission applies to. It is ignored unless the embedding origin is\\npresent and valid. If the embedding origin is provided but the embedded origin isn't, the\\nembedding origin is used as the embedded origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"Context to override. When omitted, default browser context is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"grantPermissions\",\n                    \"description\": \"Grant specific permissions to the given origin and reject all others. Deprecated. Use\\nsetPermission instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"permissions\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PermissionType\"\n                            }\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin the permission applies to, all origins if not specified.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"BrowserContext to override permissions. When omitted, default browser context is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resetPermissions\",\n                    \"description\": \"Reset all permission management for all origins.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"BrowserContext to reset permissions. When omitted, default browser context is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDownloadBehavior\",\n                    \"description\": \"Set the behavior when downloading a file.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"behavior\",\n                            \"description\": \"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny). |allowAndName| allows download and names files according to\\ntheir download guids.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"deny\",\n                                \"allow\",\n                                \"allowAndName\",\n                                \"default\"\n                            ]\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"BrowserContext to set download behavior. When omitted, default browser context is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"BrowserContextID\"\n                        },\n                        {\n                            \"name\": \"downloadPath\",\n                            \"description\": \"The default path to save downloaded files to. This is required if behavior is set to 'allow'\\nor 'allowAndName'.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"eventsEnabled\",\n                            \"description\": \"Whether to emit download events (defaults to false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"cancelDownload\",\n                    \"description\": \"Cancel a download if in progress\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"guid\",\n                            \"description\": \"Global unique identifier of the download.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"BrowserContext to perform the action in. When omitted, default browser context is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"close\",\n                    \"description\": \"Close browser gracefully.\"\n                },\n                {\n                    \"name\": \"crash\",\n                    \"description\": \"Crashes browser on the main thread.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"crashGpuProcess\",\n                    \"description\": \"Crashes GPU process.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"getVersion\",\n                    \"description\": \"Returns version information.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"protocolVersion\",\n                            \"description\": \"Protocol version.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"product\",\n                            \"description\": \"Product name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"revision\",\n                            \"description\": \"Product revision.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"userAgent\",\n                            \"description\": \"User-Agent.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"jsVersion\",\n                            \"description\": \"V8 version.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getBrowserCommandLine\",\n                    \"description\": \"Returns the command line switches for the browser process if, and only if\\n--enable-automation is on the commandline.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"arguments\",\n                            \"description\": \"Commandline parameters\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getHistograms\",\n                    \"description\": \"Get Chrome histograms.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"query\",\n                            \"description\": \"Requested substring in name. Only histograms which have query as a\\nsubstring in their name are extracted. An empty or absent query returns\\nall histograms.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"delta\",\n                            \"description\": \"If true, retrieve delta since last delta call.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"histograms\",\n                            \"description\": \"Histograms.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Histogram\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getHistogram\",\n                    \"description\": \"Get a Chrome histogram by name.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Requested histogram name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"delta\",\n                            \"description\": \"If true, retrieve delta since last delta call.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"histogram\",\n                            \"description\": \"Histogram.\",\n                            \"$ref\": \"Histogram\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getWindowBounds\",\n                    \"description\": \"Get position and size of the browser window.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"windowId\",\n                            \"description\": \"Browser window id.\",\n                            \"$ref\": \"WindowID\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"bounds\",\n                            \"description\": \"Bounds information of the window. When window state is 'minimized', the restored window\\nposition and size are returned.\",\n                            \"$ref\": \"Bounds\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getWindowForTarget\",\n                    \"description\": \"Get the browser window that contains the devtools target.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"Devtools agent host id. If called as a part of the session, associated targetId is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"Target.TargetID\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"windowId\",\n                            \"description\": \"Browser window id.\",\n                            \"$ref\": \"WindowID\"\n                        },\n                        {\n                            \"name\": \"bounds\",\n                            \"description\": \"Bounds information of the window. When window state is 'minimized', the restored window\\nposition and size are returned.\",\n                            \"$ref\": \"Bounds\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setWindowBounds\",\n                    \"description\": \"Set position and/or size of the browser window.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"windowId\",\n                            \"description\": \"Browser window id.\",\n                            \"$ref\": \"WindowID\"\n                        },\n                        {\n                            \"name\": \"bounds\",\n                            \"description\": \"New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined\\nwith 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged.\",\n                            \"$ref\": \"Bounds\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setContentsSize\",\n                    \"description\": \"Set size of the browser contents resizing browser window as necessary.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"windowId\",\n                            \"description\": \"Browser window id.\",\n                            \"$ref\": \"WindowID\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"The window contents width in DIP. Assumes current width if omitted.\\nMust be specified if 'height' is omitted.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"The window contents height in DIP. Assumes current height if omitted.\\nMust be specified if 'width' is omitted.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDockTile\",\n                    \"description\": \"Set dock tile details, platform-specific.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"badgeLabel\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"image\",\n                            \"description\": \"Png encoded image. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"executeBrowserCommand\",\n                    \"description\": \"Invoke custom browser commands used by telemetry.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"commandId\",\n                            \"$ref\": \"BrowserCommandId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addPrivacySandboxEnrollmentOverride\",\n                    \"description\": \"Allows a site to use privacy sandbox features that require enrollment\\nwithout the site actually being enrolled. Only supported on page targets.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addPrivacySandboxCoordinatorKeyConfig\",\n                    \"description\": \"Configures encryption keys used with a given privacy sandbox API to talk\\nto a trusted coordinator.  Since this is intended for test automation only,\\ncoordinatorOrigin must be a .test domain. No existing coordinator\\nconfiguration for the origin may exist.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"api\",\n                            \"$ref\": \"PrivacySandboxAPI\"\n                        },\n                        {\n                            \"name\": \"coordinatorOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyConfig\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"BrowserContext to perform the action in. When omitted, default browser\\ncontext is used.\",\n                            \"optional\": true,\n                            \"$ref\": \"BrowserContextID\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"downloadWillBegin\",\n                    \"description\": \"Fired when page is about to start a download.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that caused the download to begin.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"guid\",\n                            \"description\": \"Global unique identifier of the download.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resource being downloaded.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"suggestedFilename\",\n                            \"description\": \"Suggested file name of the resource (the actual name of the file saved on disk may differ).\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"downloadProgress\",\n                    \"description\": \"Fired when download makes progress. Last call has |done| == true.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"guid\",\n                            \"description\": \"Global unique identifier of the download.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"totalBytes\",\n                            \"description\": \"Total expected bytes to download.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"receivedBytes\",\n                            \"description\": \"Total bytes received.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"state\",\n                            \"description\": \"Download status.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"inProgress\",\n                                \"completed\",\n                                \"canceled\"\n                            ]\n                        },\n                        {\n                            \"name\": \"filePath\",\n                            \"description\": \"If download is \\\"completed\\\", provides the path of the downloaded file.\\nDepending on the platform, it is not guaranteed to be set, nor the file\\nis guaranteed to exist.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"CSS\",\n            \"description\": \"This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\\nhave an associated `id` used in subsequent operations on the related object. Each object type has\\na specific `id` structure, and those are not interchangeable between objects of different kinds.\\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"DOM\",\n                \"Page\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"StyleSheetOrigin\",\n                    \"description\": \"Stylesheet type: \\\"injected\\\" for stylesheets injected via extension, \\\"user-agent\\\" for user-agent\\nstylesheets, \\\"inspector\\\" for stylesheets created by the inspector (i.e. those holding the \\\"via\\ninspector\\\" rules), \\\"regular\\\" for regular stylesheets.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"injected\",\n                        \"user-agent\",\n                        \"inspector\",\n                        \"regular\"\n                    ]\n                },\n                {\n                    \"id\": \"PseudoElementMatches\",\n                    \"description\": \"CSS rule collection for a single pseudo style.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"pseudoType\",\n                            \"description\": \"Pseudo element type.\",\n                            \"$ref\": \"DOM.PseudoType\"\n                        },\n                        {\n                            \"name\": \"pseudoIdentifier\",\n                            \"description\": \"Pseudo element custom ident.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"matches\",\n                            \"description\": \"Matches of CSS rules applicable to the pseudo style.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RuleMatch\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSAnimationStyle\",\n                    \"description\": \"CSS style coming from animations with the name of the animation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The name of the animation.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"The style coming from the animation.\",\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InheritedStyleEntry\",\n                    \"description\": \"Inherited CSS rule collection from ancestor node.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"inlineStyle\",\n                            \"description\": \"The ancestor node's inline style, if any, in the style inheritance chain.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"matchedCSSRules\",\n                            \"description\": \"Matches of CSS rules matching the ancestor node in the style inheritance chain.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RuleMatch\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InheritedAnimatedStyleEntry\",\n                    \"description\": \"Inherited CSS style collection for animated styles from ancestor node.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"animationStyles\",\n                            \"description\": \"Styles coming from the animations of the ancestor, if any, in the style inheritance chain.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSAnimationStyle\"\n                            }\n                        },\n                        {\n                            \"name\": \"transitionsStyle\",\n                            \"description\": \"The style coming from the transitions of the ancestor, if any, in the style inheritance chain.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InheritedPseudoElementMatches\",\n                    \"description\": \"Inherited pseudo element matches from pseudos of an ancestor node.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"pseudoElements\",\n                            \"description\": \"Matches of pseudo styles from the pseudos of an ancestor node.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PseudoElementMatches\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RuleMatch\",\n                    \"description\": \"Match data for a CSS rule.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"rule\",\n                            \"description\": \"CSS rule in the match.\",\n                            \"$ref\": \"CSSRule\"\n                        },\n                        {\n                            \"name\": \"matchingSelectors\",\n                            \"description\": \"Matching selector indices in the rule's selectorList selectors (0-based).\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Value\",\n                    \"description\": \"Data for a simple selector (these are delimited by commas in a selector list).\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Value text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"Value range in the underlying resource (if available).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"specificity\",\n                            \"description\": \"Specificity of the selector.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Specificity\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Specificity\",\n                    \"description\": \"Specificity:\\nhttps://drafts.csswg.org/selectors/#specificity-rules\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"a\",\n                            \"description\": \"The a component, which represents the number of ID selectors.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"b\",\n                            \"description\": \"The b component, which represents the number of class selectors, attributes selectors, and\\npseudo-classes.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"c\",\n                            \"description\": \"The c component, which represents the number of type selectors and pseudo-elements.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SelectorList\",\n                    \"description\": \"Selector list data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"selectors\",\n                            \"description\": \"Selectors in the list.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Value\"\n                            }\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Rule selector text.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSStyleSheetHeader\",\n                    \"description\": \"CSS stylesheet metainformation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The stylesheet identifier.\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Owner frame identifier.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"sourceURL\",\n                            \"description\": \"Stylesheet resource URL. Empty if this is a constructed stylesheet created using\\nnew CSSStyleSheet() (but non-empty if this is a constructed stylesheet imported\\nas a CSS module script).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sourceMapURL\",\n                            \"description\": \"URL of source map associated with the stylesheet (if any).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Stylesheet origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Stylesheet title.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"ownerNode\",\n                            \"description\": \"The backend id for the owner node of the stylesheet.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"disabled\",\n                            \"description\": \"Denotes whether the stylesheet is disabled.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hasSourceURL\",\n                            \"description\": \"Whether the sourceURL field value comes from the sourceURL comment.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isInline\",\n                            \"description\": \"Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\\ndocument.written STYLE tags.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isMutable\",\n                            \"description\": \"Whether this stylesheet is mutable. Inline stylesheets become mutable\\nafter they have been modified via CSSOM API.\\n`<link>` element's stylesheets become mutable only if DevTools modifies them.\\nConstructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isConstructed\",\n                            \"description\": \"True if this stylesheet is created through new CSSStyleSheet() or imported as a\\nCSS module script.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"startLine\",\n                            \"description\": \"Line offset of the stylesheet within the resource (zero based).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"startColumn\",\n                            \"description\": \"Column offset of the stylesheet within the resource (zero based).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"length\",\n                            \"description\": \"Size of the content (in characters).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"endLine\",\n                            \"description\": \"Line offset of the end of the stylesheet within the resource (zero based).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"endColumn\",\n                            \"description\": \"Column offset of the end of the stylesheet within the resource (zero based).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"loadingFailed\",\n                            \"description\": \"If the style sheet was loaded from a network resource, this indicates when the resource failed to load\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSRule\",\n                    \"description\": \"CSS rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"selectorList\",\n                            \"description\": \"Rule selector data.\",\n                            \"$ref\": \"SelectorList\"\n                        },\n                        {\n                            \"name\": \"nestingSelectors\",\n                            \"description\": \"Array of selectors from ancestor style rules, sorted by distance from the current rule.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Associated style declaration.\",\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"originTreeScopeNodeId\",\n                            \"description\": \"The BackendNodeId of the DOM node that constitutes the origin tree scope of this rule.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"media\",\n                            \"description\": \"Media list array (for rules involving media queries). The array enumerates media queries\\nstarting with the innermost one, going outwards.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSMedia\"\n                            }\n                        },\n                        {\n                            \"name\": \"containerQueries\",\n                            \"description\": \"Container query list array (for rules involving container queries).\\nThe array enumerates container queries starting with the innermost one, going outwards.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSContainerQuery\"\n                            }\n                        },\n                        {\n                            \"name\": \"supports\",\n                            \"description\": \"@supports CSS at-rule array.\\nThe array enumerates @supports at-rules starting with the innermost one, going outwards.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSSupports\"\n                            }\n                        },\n                        {\n                            \"name\": \"layers\",\n                            \"description\": \"Cascade layer array. Contains the layer hierarchy that this rule belongs to starting\\nwith the innermost layer and going outwards.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSLayer\"\n                            }\n                        },\n                        {\n                            \"name\": \"scopes\",\n                            \"description\": \"@scope CSS at-rule array.\\nThe array enumerates @scope at-rules starting with the innermost one, going outwards.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSScope\"\n                            }\n                        },\n                        {\n                            \"name\": \"ruleTypes\",\n                            \"description\": \"The array keeps the types of ancestor CSSRules from the innermost going outwards.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSRuleType\"\n                            }\n                        },\n                        {\n                            \"name\": \"startingStyles\",\n                            \"description\": \"@starting-style CSS at-rule array.\\nThe array enumerates @starting-style at-rules starting with the innermost one, going outwards.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSStartingStyle\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSRuleType\",\n                    \"description\": \"Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors.\\nThis list only contains rule types that are collected during the ancestor rule collection.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"MediaRule\",\n                        \"SupportsRule\",\n                        \"ContainerRule\",\n                        \"LayerRule\",\n                        \"ScopeRule\",\n                        \"StyleRule\",\n                        \"StartingStyleRule\"\n                    ]\n                },\n                {\n                    \"id\": \"RuleUsage\",\n                    \"description\": \"CSS coverage information.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"startOffset\",\n                            \"description\": \"Offset of the start of the rule (including selector) from the beginning of the stylesheet.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"endOffset\",\n                            \"description\": \"Offset of the end of the rule body from the beginning of the stylesheet.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"used\",\n                            \"description\": \"Indicates whether the rule was actually used by some element in the page.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SourceRange\",\n                    \"description\": \"Text range within a resource. All numbers are zero-based.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"startLine\",\n                            \"description\": \"Start line of range.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"startColumn\",\n                            \"description\": \"Start column of range (inclusive).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endLine\",\n                            \"description\": \"End line of range\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endColumn\",\n                            \"description\": \"End column of range (exclusive).\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ShorthandEntry\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Shorthand name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Shorthand value.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"important\",\n                            \"description\": \"Whether the property has \\\"!important\\\" annotation (implies `false` if absent).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSComputedStyleProperty\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Computed style property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Computed style property value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ComputedStyleExtraFields\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"isAppearanceBase\",\n                            \"description\": \"Returns whether or not this node is being rendered with base appearance,\\nwhich happens when it has its appearance property set to base/base-select\\nor it is in the subtree of an element being rendered with base appearance.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSStyle\",\n                    \"description\": \"CSS style representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"cssProperties\",\n                            \"description\": \"CSS properties in the style.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSProperty\"\n                            }\n                        },\n                        {\n                            \"name\": \"shorthandEntries\",\n                            \"description\": \"Computed values for all shorthands found in the style.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ShorthandEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"cssText\",\n                            \"description\": \"Style declaration text (if available).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"Style declaration range in the enclosing stylesheet (if available).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSProperty\",\n                    \"description\": \"CSS property declaration data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The property value.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"important\",\n                            \"description\": \"Whether the property has \\\"!important\\\" annotation (implies `false` if absent).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"implicit\",\n                            \"description\": \"Whether the property is implicit (implies `false` if absent).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"The full property text as specified in the style.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"parsedOk\",\n                            \"description\": \"Whether the property is understood by the browser (implies `true` if absent).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disabled\",\n                            \"description\": \"Whether the property is disabled by the user (present for source-based properties only).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The entire property range in the enclosing style declaration (if available).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"longhandProperties\",\n                            \"description\": \"Parsed longhand components of this property if it is a shorthand.\\nThis field will be empty if the given property is not a shorthand.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSProperty\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSMedia\",\n                    \"description\": \"CSS media rule descriptor.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Media query text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"source\",\n                            \"description\": \"Source of the media query: \\\"mediaRule\\\" if specified by a @media rule, \\\"importRule\\\" if\\nspecified by an @import rule, \\\"linkedSheet\\\" if specified by a \\\"media\\\" attribute in a linked\\nstylesheet's LINK tag, \\\"inlineSheet\\\" if specified by a \\\"media\\\" attribute in an inline\\nstylesheet's STYLE tag.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mediaRule\",\n                                \"importRule\",\n                                \"linkedSheet\",\n                                \"inlineSheet\"\n                            ]\n                        },\n                        {\n                            \"name\": \"sourceURL\",\n                            \"description\": \"URL of the document containing the media query description.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The associated rule (@media or @import) header range in the enclosing stylesheet (if\\navailable).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the stylesheet containing this object (if exists).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"mediaList\",\n                            \"description\": \"Array of media queries.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"MediaQuery\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"MediaQuery\",\n                    \"description\": \"Media query descriptor.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"expressions\",\n                            \"description\": \"Array of media query expressions.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"MediaQueryExpression\"\n                            }\n                        },\n                        {\n                            \"name\": \"active\",\n                            \"description\": \"Whether the media query condition is satisfied.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"MediaQueryExpression\",\n                    \"description\": \"Media query expression descriptor.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Media query expression value.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"unit\",\n                            \"description\": \"Media query expression units.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"feature\",\n                            \"description\": \"Media query expression feature.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"valueRange\",\n                            \"description\": \"The associated range of the value text in the enclosing stylesheet (if available).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"computedLength\",\n                            \"description\": \"Computed length of media query expression (if applicable).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSContainerQuery\",\n                    \"description\": \"CSS container query rule descriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Container query text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The associated rule header range in the enclosing stylesheet (if\\navailable).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the stylesheet containing this object (if exists).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Optional name for the container.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"physicalAxes\",\n                            \"description\": \"Optional physical axes queried for the container.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.PhysicalAxes\"\n                        },\n                        {\n                            \"name\": \"logicalAxes\",\n                            \"description\": \"Optional logical axes queried for the container.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.LogicalAxes\"\n                        },\n                        {\n                            \"name\": \"queriesScrollState\",\n                            \"description\": \"true if the query contains scroll-state() queries.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"queriesAnchored\",\n                            \"description\": \"true if the query contains anchored() queries.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSSupports\",\n                    \"description\": \"CSS Supports at-rule descriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Supports rule text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"active\",\n                            \"description\": \"Whether the supports condition is satisfied.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The associated rule header range in the enclosing stylesheet (if\\navailable).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the stylesheet containing this object (if exists).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSScope\",\n                    \"description\": \"CSS Scope at-rule descriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Scope rule text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The associated rule header range in the enclosing stylesheet (if\\navailable).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the stylesheet containing this object (if exists).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSLayer\",\n                    \"description\": \"CSS Layer at-rule descriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Layer name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The associated rule header range in the enclosing stylesheet (if\\navailable).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the stylesheet containing this object (if exists).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSStartingStyle\",\n                    \"description\": \"CSS Starting Style at-rule descriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The associated rule header range in the enclosing stylesheet (if\\navailable).\",\n                            \"optional\": true,\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the stylesheet containing this object (if exists).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSLayerData\",\n                    \"description\": \"CSS Layer data.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Layer name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"subLayers\",\n                            \"description\": \"Direct sub-layers\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSLayerData\"\n                            }\n                        },\n                        {\n                            \"name\": \"order\",\n                            \"description\": \"Layer order. The order determines the order of the layer in the cascade order.\\nA higher number has higher priority in the cascade order.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PlatformFontUsage\",\n                    \"description\": \"Information about amount of glyphs that were rendered with given font.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"familyName\",\n                            \"description\": \"Font's family name reported by platform.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"postScriptName\",\n                            \"description\": \"Font's PostScript name reported by platform.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isCustomFont\",\n                            \"description\": \"Indicates if the font was downloaded or resolved locally.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"glyphCount\",\n                            \"description\": \"Amount of glyphs that were rendered with this font.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FontVariationAxis\",\n                    \"description\": \"Information about font variation axes for variable fonts\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"tag\",\n                            \"description\": \"The font-variation-setting tag (a.k.a. \\\"axis tag\\\").\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Human-readable variation name in the default language (normally, \\\"en\\\").\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"minValue\",\n                            \"description\": \"The minimum value (inclusive) the font supports for this tag.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"maxValue\",\n                            \"description\": \"The maximum value (inclusive) the font supports for this tag.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"defaultValue\",\n                            \"description\": \"The default value.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FontFace\",\n                    \"description\": \"Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions\\nand additional information such as platformFontFamily and fontVariationAxes.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"fontFamily\",\n                            \"description\": \"The font-family.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontStyle\",\n                            \"description\": \"The font-style.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontVariant\",\n                            \"description\": \"The font-variant.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontWeight\",\n                            \"description\": \"The font-weight.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontStretch\",\n                            \"description\": \"The font-stretch.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontDisplay\",\n                            \"description\": \"The font-display.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"unicodeRange\",\n                            \"description\": \"The unicode-range.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"src\",\n                            \"description\": \"The src.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"platformFontFamily\",\n                            \"description\": \"The resolved platform font family\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontVariationAxes\",\n                            \"description\": \"Available variation settings (a.k.a. \\\"axes\\\").\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FontVariationAxis\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSTryRule\",\n                    \"description\": \"CSS try rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Associated style declaration.\",\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSPositionTryRule\",\n                    \"description\": \"CSS @position-try rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The prelude dashed-ident name\",\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Associated style declaration.\",\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"active\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSKeyframesRule\",\n                    \"description\": \"CSS keyframes rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"animationName\",\n                            \"description\": \"Animation name.\",\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"keyframes\",\n                            \"description\": \"List of keyframes.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSKeyframeRule\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSPropertyRegistration\",\n                    \"description\": \"Representation of a custom property registration through CSS.registerProperty\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"propertyName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"initialValue\",\n                            \"optional\": true,\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"inherits\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"syntax\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSAtRule\",\n                    \"description\": \"CSS generic @rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of at-rule.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"font-face\",\n                                \"font-feature-values\",\n                                \"font-palette-values\"\n                            ]\n                        },\n                        {\n                            \"name\": \"subsection\",\n                            \"description\": \"Subsection of font-feature-values, if this is a subsection.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"swash\",\n                                \"annotation\",\n                                \"ornaments\",\n                                \"stylistic\",\n                                \"styleset\",\n                                \"character-variant\"\n                            ]\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"LINT.ThenChange(//third_party/blink/renderer/core/inspector/inspector_style_sheet.cc:FontVariantAlternatesFeatureType,//third_party/blink/renderer/core/inspector/inspector_css_agent.cc:FontVariantAlternatesFeatureType)\\nAssociated name, if applicable.\",\n                            \"optional\": true,\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Associated style declaration.\",\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSPropertyRule\",\n                    \"description\": \"CSS property at-rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"propertyName\",\n                            \"description\": \"Associated property name.\",\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Associated style declaration.\",\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSFunctionParameter\",\n                    \"description\": \"CSS function argument representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The parameter name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"The parameter type.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSFunctionConditionNode\",\n                    \"description\": \"CSS function conditional block representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"media\",\n                            \"description\": \"Media query for this conditional block. Only one type of condition should be set.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSMedia\"\n                        },\n                        {\n                            \"name\": \"containerQueries\",\n                            \"description\": \"Container query for this conditional block. Only one type of condition should be set.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSContainerQuery\"\n                        },\n                        {\n                            \"name\": \"supports\",\n                            \"description\": \"@supports CSS at-rule condition. Only one type of condition should be set.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSSupports\"\n                        },\n                        {\n                            \"name\": \"children\",\n                            \"description\": \"Block body.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSFunctionNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"conditionText\",\n                            \"description\": \"The condition text.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSFunctionNode\",\n                    \"description\": \"Section of the body of a CSS function rule.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"condition\",\n                            \"description\": \"A conditional block. If set, style should not be set.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSFunctionConditionNode\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Values set by this node. If set, condition should not be set.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSFunctionRule\",\n                    \"description\": \"CSS function at-rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name of the function.\",\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"parameters\",\n                            \"description\": \"List of parameters.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSFunctionParameter\"\n                            }\n                        },\n                        {\n                            \"name\": \"children\",\n                            \"description\": \"Function body.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSFunctionNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSKeyframeRule\",\n                    \"description\": \"CSS keyframe rule representation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Parent stylesheet's origin.\",\n                            \"$ref\": \"StyleSheetOrigin\"\n                        },\n                        {\n                            \"name\": \"keyText\",\n                            \"description\": \"Associated key text.\",\n                            \"$ref\": \"Value\"\n                        },\n                        {\n                            \"name\": \"style\",\n                            \"description\": \"Associated style declaration.\",\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StyleDeclarationEdit\",\n                    \"description\": \"A descriptor of operation to mutate style declaration text.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier.\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"description\": \"The range of the style text in the enclosing stylesheet.\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"New style text.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"addRule\",\n                    \"description\": \"Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\\nposition specified by `location`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"The css style sheet identifier where a new rule should be inserted.\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"ruleText\",\n                            \"description\": \"The text of a new rule.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Text position of a new rule in the target style sheet.\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"nodeForPropertySyntaxValidation\",\n                            \"description\": \"NodeId for the DOM node in whose context custom property declarations for registered properties should be\\nvalidated. If omitted, declarations in the new rule text can only be validated statically, which may produce\\nincorrect results if the declaration contains a var() for example.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"rule\",\n                            \"description\": \"The newly created rule.\",\n                            \"$ref\": \"CSSRule\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"collectClassNames\",\n                    \"description\": \"Returns all class names from specified stylesheet.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"classNames\",\n                            \"description\": \"Class name list.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"createStyleSheet\",\n                    \"description\": \"Creates a new special \\\"via-inspector\\\" stylesheet in the frame with given `frameId`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Identifier of the frame where \\\"via-inspector\\\" stylesheet should be created.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"force\",\n                            \"description\": \"If true, creates a new stylesheet for every call. If false,\\nreturns a stylesheet previously created by a call with force=false\\nfor the frame's document if it exists or creates a new stylesheet\\n(default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the created \\\"via-inspector\\\" stylesheet.\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables the CSS agent for the given page.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\\nenabled until the result of this command is received.\"\n                },\n                {\n                    \"name\": \"forcePseudoState\",\n                    \"description\": \"Ensures that the given node will have specified pseudo-classes whenever its style is computed by\\nthe browser.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The element id for which to force the pseudo state.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"forcedPseudoClasses\",\n                            \"description\": \"Element pseudo classes to force when computing the element's style.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"forceStartingStyle\",\n                    \"description\": \"Ensures that the given node is in its starting-style state.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The element id for which to force the starting-style state.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"forced\",\n                            \"description\": \"Boolean indicating if this is on or off.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getBackgroundColors\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to get background colors for.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"backgroundColors\",\n                            \"description\": \"The range of background colors behind this element, if it contains any visible text. If no\\nvisible text is present, this will be undefined. In the case of a flat background color,\\nthis will consist of simply that color. In the case of a gradient, this will consist of each\\nof the color stops. For anything more complicated, this will be an empty array. Images will\\nbe ignored (as if the image had failed to load).\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"computedFontSize\",\n                            \"description\": \"The computed font size for this node, as a CSS computed value string (e.g. '12px').\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"computedFontWeight\",\n                            \"description\": \"The computed font weight for this node, as a CSS computed value string (e.g. 'normal' or\\n'100').\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getComputedStyleForNode\",\n                    \"description\": \"Returns the computed style for a DOM node identified by `nodeId`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"computedStyle\",\n                            \"description\": \"Computed style for the specified DOM node.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSComputedStyleProperty\"\n                            }\n                        },\n                        {\n                            \"name\": \"extraFields\",\n                            \"description\": \"A list of non-standard \\\"extra fields\\\" which blink stores alongside each\\ncomputed style.\",\n                            \"experimental\": true,\n                            \"$ref\": \"ComputedStyleExtraFields\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resolveValues\",\n                    \"description\": \"Resolve the specified values in the context of the provided element.\\nFor example, a value of '1em' is evaluated according to the computed\\n'font-size' of the element and a value 'calc(1px + 2px)' will be\\nresolved to '3px'.\\nIf the `propertyName` was specified the `values` are resolved as if\\nthey were property's declaration. If a value cannot be parsed according\\nto the provided property syntax, the value is parsed using combined\\nsyntax as if null `propertyName` was provided. If the value cannot be\\nresolved even then, return the provided value without any changes.\\nNote: this function currently does not resolve CSS random() function,\\nit returns unmodified random() function parts.`\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"values\",\n                            \"description\": \"Cascade-dependent keywords (revert/revert-layer) do not work.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node in whose context the expression is evaluated\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"propertyName\",\n                            \"description\": \"Only longhands and custom property names are accepted.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"pseudoType\",\n                            \"description\": \"Pseudo element type, only works for pseudo elements that generate\\nelements in the tree, such as ::before and ::after.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.PseudoType\"\n                        },\n                        {\n                            \"name\": \"pseudoIdentifier\",\n                            \"description\": \"Pseudo element custom ident.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"results\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getLonghandProperties\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"shorthandName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"longhandProperties\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSProperty\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getInlineStylesForNode\",\n                    \"description\": \"Returns the styles defined inline (explicitly in the \\\"style\\\" attribute and implicitly, using DOM\\nattributes) for a DOM node identified by `nodeId`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"inlineStyle\",\n                            \"description\": \"Inline style for the specified DOM node.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"attributesStyle\",\n                            \"description\": \"Attribute-defined element style (e.g. resulting from \\\"width=20 height=100%\\\").\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAnimatedStylesForNode\",\n                    \"description\": \"Returns the styles coming from animations & transitions\\nincluding the animation & transition styles coming from inheritance chain.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"animationStyles\",\n                            \"description\": \"Styles coming from animations.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSAnimationStyle\"\n                            }\n                        },\n                        {\n                            \"name\": \"transitionsStyle\",\n                            \"description\": \"Style coming from transitions.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"inherited\",\n                            \"description\": \"Inherited style entries for animationsStyle and transitionsStyle from\\nthe inheritance chain of the element.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InheritedAnimatedStyleEntry\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getMatchedStylesForNode\",\n                    \"description\": \"Returns requested styles for a DOM node identified by `nodeId`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"inlineStyle\",\n                            \"description\": \"Inline style for the specified DOM node.\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"attributesStyle\",\n                            \"description\": \"Attribute-defined element style (e.g. resulting from \\\"width=20 height=100%\\\").\",\n                            \"optional\": true,\n                            \"$ref\": \"CSSStyle\"\n                        },\n                        {\n                            \"name\": \"matchedCSSRules\",\n                            \"description\": \"CSS rules matching this node, from all applicable stylesheets.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RuleMatch\"\n                            }\n                        },\n                        {\n                            \"name\": \"pseudoElements\",\n                            \"description\": \"Pseudo style matches for this node.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PseudoElementMatches\"\n                            }\n                        },\n                        {\n                            \"name\": \"inherited\",\n                            \"description\": \"A chain of inherited styles (from the immediate node parent up to the DOM tree root).\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InheritedStyleEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"inheritedPseudoElements\",\n                            \"description\": \"A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root).\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InheritedPseudoElementMatches\"\n                            }\n                        },\n                        {\n                            \"name\": \"cssKeyframesRules\",\n                            \"description\": \"A list of CSS keyframed animations matching this node.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSKeyframesRule\"\n                            }\n                        },\n                        {\n                            \"name\": \"cssPositionTryRules\",\n                            \"description\": \"A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSPositionTryRule\"\n                            }\n                        },\n                        {\n                            \"name\": \"activePositionFallbackIndex\",\n                            \"description\": \"Index of the active fallback in the applied position-try-fallback property,\\nwill not be set if there is no active position-try fallback.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"cssPropertyRules\",\n                            \"description\": \"A list of CSS at-property rules matching this node.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSPropertyRule\"\n                            }\n                        },\n                        {\n                            \"name\": \"cssPropertyRegistrations\",\n                            \"description\": \"A list of CSS property registrations matching this node.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSPropertyRegistration\"\n                            }\n                        },\n                        {\n                            \"name\": \"cssAtRules\",\n                            \"description\": \"A list of simple @rules matching this node or its pseudo-elements.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSAtRule\"\n                            }\n                        },\n                        {\n                            \"name\": \"parentLayoutNodeId\",\n                            \"description\": \"Id of the first parent element that does not have display: contents.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"cssFunctionRules\",\n                            \"description\": \"A list of CSS at-function rules referenced by styles of this node.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSFunctionRule\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getEnvironmentVariables\",\n                    \"description\": \"Returns the values of the default UA-defined environment variables used in env()\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"environmentVariables\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getMediaQueries\",\n                    \"description\": \"Returns all media queries parsed by the rendering engine.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"medias\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSMedia\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getPlatformFontsForNode\",\n                    \"description\": \"Requests information about platform fonts which we used to render child TextNodes in the given\\nnode.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"fonts\",\n                            \"description\": \"Usage statistics for every employed platform font.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlatformFontUsage\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getStyleSheetText\",\n                    \"description\": \"Returns the current textual content for a stylesheet.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"The stylesheet text.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getLayersForNode\",\n                    \"description\": \"Returns all layers parsed by the rendering engine for the tree scope of a node.\\nGiven a DOM element identified by nodeId, getLayersForNode returns the root\\nlayer for the nearest ancestor document or shadow root. The layer root contains\\nthe full layer tree for the tree scope and their ordering.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"rootLayer\",\n                            \"$ref\": \"CSSLayerData\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getLocationForSelector\",\n                    \"description\": \"Given a CSS selector text and a style sheet ID, getLocationForSelector\\nreturns an array of locations of the CSS selector in the style sheet.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"selectorText\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"ranges\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SourceRange\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trackComputedStyleUpdatesForNode\",\n                    \"description\": \"Starts tracking the given node for the computed style updates\\nand whenever the computed style is updated for node, it queues\\na `computedStyleUpdated` event with throttling.\\nThere can only be 1 node tracked for computed style updates\\nso passing a new node id removes tracking from the previous node.\\nPass `undefined` to disable tracking.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trackComputedStyleUpdates\",\n                    \"description\": \"Starts tracking the given computed styles for updates. The specified array of properties\\nreplaces the one previously specified. Pass empty array to disable tracking.\\nUse takeComputedStyleUpdates to retrieve the list of nodes that had properties modified.\\nThe changes to computed style properties are only tracked for nodes pushed to the front-end\\nby the DOM agent. If no changes to the tracked properties occur after the node has been pushed\\nto the front-end, no updates will be issued for the node.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"propertiesToTrack\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSComputedStyleProperty\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"takeComputedStyleUpdates\",\n                    \"description\": \"Polls the next batch of computed style updates.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"The list of node Ids that have their tracked computed styles updated.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOM.NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setEffectivePropertyValueForNode\",\n                    \"description\": \"Find a rule with the given active property for the given node and set the new value for this\\nproperty\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The element id for which to set property.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"propertyName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPropertyRulePropertyName\",\n                    \"description\": \"Modifies the property rule property name.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"propertyName\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"propertyName\",\n                            \"description\": \"The resulting key text after modification.\",\n                            \"$ref\": \"Value\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setKeyframeKey\",\n                    \"description\": \"Modifies the keyframe rule key text.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"keyText\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"keyText\",\n                            \"description\": \"The resulting key text after modification.\",\n                            \"$ref\": \"Value\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setMediaText\",\n                    \"description\": \"Modifies the rule selector.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"media\",\n                            \"description\": \"The resulting CSS media rule after modification.\",\n                            \"$ref\": \"CSSMedia\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setContainerQueryText\",\n                    \"description\": \"Modifies the expression of a container query.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"containerQuery\",\n                            \"description\": \"The resulting CSS container query rule after modification.\",\n                            \"$ref\": \"CSSContainerQuery\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSupportsText\",\n                    \"description\": \"Modifies the expression of a supports at-rule.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"supports\",\n                            \"description\": \"The resulting CSS Supports rule after modification.\",\n                            \"$ref\": \"CSSSupports\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setScopeText\",\n                    \"description\": \"Modifies the expression of a scope at-rule.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"scope\",\n                            \"description\": \"The resulting CSS Scope rule after modification.\",\n                            \"$ref\": \"CSSScope\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setRuleSelector\",\n                    \"description\": \"Modifies the rule selector.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"range\",\n                            \"$ref\": \"SourceRange\"\n                        },\n                        {\n                            \"name\": \"selector\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"selectorList\",\n                            \"description\": \"The resulting selector list after modification.\",\n                            \"$ref\": \"SelectorList\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setStyleSheetText\",\n                    \"description\": \"Sets the new stylesheet text.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"sourceMapURL\",\n                            \"description\": \"URL of source map associated with script (if any).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setStyleTexts\",\n                    \"description\": \"Applies specified style edits one after another in the given order.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"edits\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StyleDeclarationEdit\"\n                            }\n                        },\n                        {\n                            \"name\": \"nodeForPropertySyntaxValidation\",\n                            \"description\": \"NodeId for the DOM node in whose context custom property declarations for registered properties should be\\nvalidated. If omitted, declarations in the new rule text can only be validated statically, which may produce\\nincorrect results if the declaration contains a var() for example.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"styles\",\n                            \"description\": \"The resulting styles after modification.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSStyle\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startRuleUsageTracking\",\n                    \"description\": \"Enables the selector recording.\"\n                },\n                {\n                    \"name\": \"stopRuleUsageTracking\",\n                    \"description\": \"Stop tracking rule usage and return the list of rules that were used since last call to\\n`takeCoverageDelta` (or since start of coverage instrumentation).\",\n                    \"returns\": [\n                        {\n                            \"name\": \"ruleUsage\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RuleUsage\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"takeCoverageDelta\",\n                    \"description\": \"Obtain list of rules that became used since last call to this method (or since start of coverage\\ninstrumentation).\",\n                    \"returns\": [\n                        {\n                            \"name\": \"coverage\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RuleUsage\"\n                            }\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Monotonically increasing time, in seconds.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setLocalFontsEnabled\",\n                    \"description\": \"Enables/disables rendering of local CSS fonts (enabled by default).\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether rendering of local fonts is enabled.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"fontsUpdated\",\n                    \"description\": \"Fires whenever a web font is updated.  A non-empty font parameter indicates a successfully loaded\\nweb font.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"font\",\n                            \"description\": \"The web font that has loaded.\",\n                            \"optional\": true,\n                            \"$ref\": \"FontFace\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"mediaQueryResultChanged\",\n                    \"description\": \"Fires whenever a MediaQuery result changes (for example, after a browser window has been\\nresized.) The current implementation considers only viewport-dependent media features.\"\n                },\n                {\n                    \"name\": \"styleSheetAdded\",\n                    \"description\": \"Fired whenever an active document stylesheet is added.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"header\",\n                            \"description\": \"Added stylesheet metainfo.\",\n                            \"$ref\": \"CSSStyleSheetHeader\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"styleSheetChanged\",\n                    \"description\": \"Fired whenever a stylesheet is changed as a result of the client operation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"styleSheetRemoved\",\n                    \"description\": \"Fired whenever an active document stylesheet is removed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"styleSheetId\",\n                            \"description\": \"Identifier of the removed stylesheet.\",\n                            \"$ref\": \"DOM.StyleSheetId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"computedStyleUpdated\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The node id that has updated computed styles.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"CacheStorage\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Storage\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"CacheId\",\n                    \"description\": \"Unique identifier of the Cache object.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"CachedResponseType\",\n                    \"description\": \"type of HTTP response cached\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"basic\",\n                        \"cors\",\n                        \"default\",\n                        \"error\",\n                        \"opaqueResponse\",\n                        \"opaqueRedirect\"\n                    ]\n                },\n                {\n                    \"id\": \"DataEntry\",\n                    \"description\": \"Data entry.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"requestURL\",\n                            \"description\": \"Request URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestMethod\",\n                            \"description\": \"Request method.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestHeaders\",\n                            \"description\": \"Request headers\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Header\"\n                            }\n                        },\n                        {\n                            \"name\": \"responseTime\",\n                            \"description\": \"Number of seconds since epoch.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"responseStatus\",\n                            \"description\": \"HTTP response status code.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responseStatusText\",\n                            \"description\": \"HTTP response status text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"responseType\",\n                            \"description\": \"HTTP response type\",\n                            \"$ref\": \"CachedResponseType\"\n                        },\n                        {\n                            \"name\": \"responseHeaders\",\n                            \"description\": \"Response headers\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Header\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Cache\",\n                    \"description\": \"Cache identifier.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"cacheId\",\n                            \"description\": \"An opaque unique id of the cache.\",\n                            \"$ref\": \"CacheId\"\n                        },\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"Security origin of the cache.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key of the cache.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket of the cache.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"cacheName\",\n                            \"description\": \"The name of the cache.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Header\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CachedResponse\",\n                    \"description\": \"Cached response\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"body\",\n                            \"description\": \"Entry content, base64-encoded. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"deleteCache\",\n                    \"description\": \"Deletes a cache.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cacheId\",\n                            \"description\": \"Id of cache for deletion.\",\n                            \"$ref\": \"CacheId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteEntry\",\n                    \"description\": \"Deletes a cache entry.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cacheId\",\n                            \"description\": \"Id of cache where the entry will be deleted.\",\n                            \"$ref\": \"CacheId\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"URL spec of the request.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestCacheNames\",\n                    \"description\": \"Requests cache names.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"caches\",\n                            \"description\": \"Caches for the security origin.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Cache\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestCachedResponse\",\n                    \"description\": \"Fetches cache entry.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cacheId\",\n                            \"description\": \"Id of cache that contains the entry.\",\n                            \"$ref\": \"CacheId\"\n                        },\n                        {\n                            \"name\": \"requestURL\",\n                            \"description\": \"URL spec of the request.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestHeaders\",\n                            \"description\": \"headers of the request.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Header\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"Response read from the cache.\",\n                            \"$ref\": \"CachedResponse\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestEntries\",\n                    \"description\": \"Requests data from cache.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cacheId\",\n                            \"description\": \"ID of cache to get entries from.\",\n                            \"$ref\": \"CacheId\"\n                        },\n                        {\n                            \"name\": \"skipCount\",\n                            \"description\": \"Number of records to skip.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pageSize\",\n                            \"description\": \"Number of records to fetch.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pathFilter\",\n                            \"description\": \"If present, only return the entries containing this substring in the path\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"cacheDataEntries\",\n                            \"description\": \"Array of object store data entries.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DataEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"returnCount\",\n                            \"description\": \"Count of returned entries from this storage. If pathFilter is empty, it\\nis the count of all entries from this storage.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Cast\",\n            \"description\": \"A domain for interacting with Cast, Presentation API, and Remote Playback API\\nfunctionalities.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"Sink\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"session\",\n                            \"description\": \"Text describing the current session. Present only if there is an active\\nsession on the sink.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Starts observing for sinks that can be used for tab mirroring, and if set,\\nsinks compatible with |presentationUrl| as well. When sinks are found, a\\n|sinksUpdated| event is fired.\\nAlso starts observing for issue messages. When an issue is added or removed,\\nan |issueUpdated| event is fired.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"presentationUrl\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Stops observing for sinks and issues.\"\n                },\n                {\n                    \"name\": \"setSinkToUse\",\n                    \"description\": \"Sets a sink to be used when the web page requests the browser to choose a\\nsink via Presentation API, Remote Playback API, or Cast SDK.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sinkName\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startDesktopMirroring\",\n                    \"description\": \"Starts mirroring the desktop to the sink.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sinkName\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startTabMirroring\",\n                    \"description\": \"Starts mirroring the tab to the sink.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sinkName\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopCasting\",\n                    \"description\": \"Stops the active Cast session on the sink.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sinkName\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"sinksUpdated\",\n                    \"description\": \"This is fired whenever the list of available sinks changes. A sink is a\\ndevice or a software surface that you can cast to.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sinks\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Sink\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"issueUpdated\",\n                    \"description\": \"This is fired whenever the outstanding issue/error message changes.\\n|issueMessage| is empty if there is no issue.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"issueMessage\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"DOM\",\n            \"description\": \"This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\\nand never sends the same node twice. It is client's responsibility to collect information about\\nthe nodes that were sent to the client. Note that `iframe` owner elements will return\\ncorresponding document elements as their child nodes.\",\n            \"dependencies\": [\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"NodeId\",\n                    \"description\": \"Unique DOM node identifier.\",\n                    \"type\": \"integer\"\n                },\n                {\n                    \"id\": \"BackendNodeId\",\n                    \"description\": \"Unique DOM node identifier used to reference a node that may not have been pushed to the\\nfront-end.\",\n                    \"type\": \"integer\"\n                },\n                {\n                    \"id\": \"StyleSheetId\",\n                    \"description\": \"Unique identifier for a CSS stylesheet.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"BackendNode\",\n                    \"description\": \"Backend node with a friendly name.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeType\",\n                            \"description\": \"`Node`'s nodeType.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"nodeName\",\n                            \"description\": \"`Node`'s nodeName.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"$ref\": \"BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PseudoType\",\n                    \"description\": \"Pseudo element type.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"first-line\",\n                        \"first-letter\",\n                        \"checkmark\",\n                        \"before\",\n                        \"after\",\n                        \"picker-icon\",\n                        \"interest-hint\",\n                        \"marker\",\n                        \"backdrop\",\n                        \"column\",\n                        \"selection\",\n                        \"search-text\",\n                        \"target-text\",\n                        \"spelling-error\",\n                        \"grammar-error\",\n                        \"highlight\",\n                        \"first-line-inherited\",\n                        \"scroll-marker\",\n                        \"scroll-marker-group\",\n                        \"scroll-button\",\n                        \"scrollbar\",\n                        \"scrollbar-thumb\",\n                        \"scrollbar-button\",\n                        \"scrollbar-track\",\n                        \"scrollbar-track-piece\",\n                        \"scrollbar-corner\",\n                        \"resizer\",\n                        \"input-list-button\",\n                        \"view-transition\",\n                        \"view-transition-group\",\n                        \"view-transition-image-pair\",\n                        \"view-transition-group-children\",\n                        \"view-transition-old\",\n                        \"view-transition-new\",\n                        \"placeholder\",\n                        \"file-selector-button\",\n                        \"details-content\",\n                        \"picker\",\n                        \"permission-icon\",\n                        \"overscroll-area-parent\"\n                    ]\n                },\n                {\n                    \"id\": \"ShadowRootType\",\n                    \"description\": \"Shadow root type.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"user-agent\",\n                        \"open\",\n                        \"closed\"\n                    ]\n                },\n                {\n                    \"id\": \"CompatibilityMode\",\n                    \"description\": \"Document compatibility mode.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"QuirksMode\",\n                        \"LimitedQuirksMode\",\n                        \"NoQuirksMode\"\n                    ]\n                },\n                {\n                    \"id\": \"PhysicalAxes\",\n                    \"description\": \"ContainerSelector physical axes\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Horizontal\",\n                        \"Vertical\",\n                        \"Both\"\n                    ]\n                },\n                {\n                    \"id\": \"LogicalAxes\",\n                    \"description\": \"ContainerSelector logical axes\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Inline\",\n                        \"Block\",\n                        \"Both\"\n                    ]\n                },\n                {\n                    \"id\": \"ScrollOrientation\",\n                    \"description\": \"Physical scroll orientation\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"horizontal\",\n                        \"vertical\"\n                    ]\n                },\n                {\n                    \"id\": \"Node\",\n                    \"description\": \"DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\\nDOMNode is a base node mirror type.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\\nwill only push node with given `id` once. It is aware of all requested nodes and will only\\nfire DOM events for nodes known to the client.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"The id of the parent node if any.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"The BackendNodeId for this node.\",\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"nodeType\",\n                            \"description\": \"`Node`'s nodeType.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"nodeName\",\n                            \"description\": \"`Node`'s nodeName.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"localName\",\n                            \"description\": \"`Node`'s localName.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"nodeValue\",\n                            \"description\": \"`Node`'s nodeValue.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"childNodeCount\",\n                            \"description\": \"Child count for `Container` nodes.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"children\",\n                            \"description\": \"Child nodes of this node when requested with children.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Node\"\n                            }\n                        },\n                        {\n                            \"name\": \"attributes\",\n                            \"description\": \"Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"documentURL\",\n                            \"description\": \"Document URL that `Document` or `FrameOwner` node points to.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"baseURL\",\n                            \"description\": \"Base URL that `Document` or `FrameOwner` node uses for URL completion.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"publicId\",\n                            \"description\": \"`DocumentType`'s publicId.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"systemId\",\n                            \"description\": \"`DocumentType`'s systemId.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"internalSubset\",\n                            \"description\": \"`DocumentType`'s internalSubset.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"xmlVersion\",\n                            \"description\": \"`Document`'s XML version in case of XML documents.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"`Attr`'s name.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"`Attr`'s value.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"pseudoType\",\n                            \"description\": \"Pseudo element type for this node.\",\n                            \"optional\": true,\n                            \"$ref\": \"PseudoType\"\n                        },\n                        {\n                            \"name\": \"pseudoIdentifier\",\n                            \"description\": \"Pseudo element identifier for this node. Only present if there is a\\nvalid pseudoType.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"shadowRootType\",\n                            \"description\": \"Shadow root type.\",\n                            \"optional\": true,\n                            \"$ref\": \"ShadowRootType\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame ID for frame owner elements.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"contentDocument\",\n                            \"description\": \"Content document for frame owner elements.\",\n                            \"optional\": true,\n                            \"$ref\": \"Node\"\n                        },\n                        {\n                            \"name\": \"shadowRoots\",\n                            \"description\": \"Shadow root list for given element host.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Node\"\n                            }\n                        },\n                        {\n                            \"name\": \"templateContent\",\n                            \"description\": \"Content document fragment for template elements.\",\n                            \"optional\": true,\n                            \"$ref\": \"Node\"\n                        },\n                        {\n                            \"name\": \"pseudoElements\",\n                            \"description\": \"Pseudo elements associated with this node.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Node\"\n                            }\n                        },\n                        {\n                            \"name\": \"importedDocument\",\n                            \"description\": \"Deprecated, as the HTML Imports API has been removed (crbug.com/937746).\\nThis property used to return the imported document for the HTMLImport links.\\nThe property is always undefined now.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Node\"\n                        },\n                        {\n                            \"name\": \"distributedNodes\",\n                            \"description\": \"Distributed nodes for given insertion point.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackendNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"isSVG\",\n                            \"description\": \"Whether the node is SVG.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"compatibilityMode\",\n                            \"optional\": true,\n                            \"$ref\": \"CompatibilityMode\"\n                        },\n                        {\n                            \"name\": \"assignedSlot\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNode\"\n                        },\n                        {\n                            \"name\": \"isScrollable\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"affectedByStartingStyles\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"adoptedStyleSheets\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StyleSheetId\"\n                            }\n                        },\n                        {\n                            \"name\": \"isAdRelated\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DetachedElementInfo\",\n                    \"description\": \"A structure to hold the top-level node of a detached tree and an array of its retained descendants.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"treeNode\",\n                            \"$ref\": \"Node\"\n                        },\n                        {\n                            \"name\": \"retainedNodeIds\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RGBA\",\n                    \"description\": \"A structure holding an RGBA color.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"r\",\n                            \"description\": \"The red component, in the [0-255] range.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"g\",\n                            \"description\": \"The green component, in the [0-255] range.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"b\",\n                            \"description\": \"The blue component, in the [0-255] range.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"a\",\n                            \"description\": \"The alpha component, in the [0-1] range (default: 1).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Quad\",\n                    \"description\": \"An array of quad vertices, x immediately followed by y for each point, points clock-wise.\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"number\"\n                    }\n                },\n                {\n                    \"id\": \"BoxModel\",\n                    \"description\": \"Box model.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"content\",\n                            \"description\": \"Content box\",\n                            \"$ref\": \"Quad\"\n                        },\n                        {\n                            \"name\": \"padding\",\n                            \"description\": \"Padding box\",\n                            \"$ref\": \"Quad\"\n                        },\n                        {\n                            \"name\": \"border\",\n                            \"description\": \"Border box\",\n                            \"$ref\": \"Quad\"\n                        },\n                        {\n                            \"name\": \"margin\",\n                            \"description\": \"Margin box\",\n                            \"$ref\": \"Quad\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Node width\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Node height\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"shapeOutside\",\n                            \"description\": \"Shape outside coordinates\",\n                            \"optional\": true,\n                            \"$ref\": \"ShapeOutsideInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ShapeOutsideInfo\",\n                    \"description\": \"CSS Shape Outside details.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"bounds\",\n                            \"description\": \"Shape bounds\",\n                            \"$ref\": \"Quad\"\n                        },\n                        {\n                            \"name\": \"shape\",\n                            \"description\": \"Shape coordinate details\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"any\"\n                            }\n                        },\n                        {\n                            \"name\": \"marginShape\",\n                            \"description\": \"Margin shape bounds\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"any\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Rect\",\n                    \"description\": \"Rectangle.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Rectangle width\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Rectangle height\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CSSComputedStyleProperty\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Computed style property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Computed style property value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"collectClassNamesFromSubtree\",\n                    \"description\": \"Collects class names for the node with given id and all of it's child nodes.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to collect class names.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"classNames\",\n                            \"description\": \"Class name list.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"copyTo\",\n                    \"description\": \"Creates a deep copy of the specified node and places it into the target container before the\\ngiven anchor.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to copy.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"targetNodeId\",\n                            \"description\": \"Id of the element to drop the copy into.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"insertBeforeNodeId\",\n                            \"description\": \"Drop the copy before this node (if absent, the copy becomes the last child of\\n`targetNodeId`).\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node clone.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"describeNode\",\n                    \"description\": \"Describes node given its id, does not require domain to be enabled. Does not start tracking any\\nobjects, can be used for automation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pierce\",\n                            \"description\": \"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"node\",\n                            \"description\": \"Node description.\",\n                            \"$ref\": \"Node\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"scrollIntoViewIfNeeded\",\n                    \"description\": \"Scrolls the specified rect of the given node into view if not already visible.\\nNote: exactly one between nodeId, backendNodeId and objectId should be passed\\nto identify the node.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"rect\",\n                            \"description\": \"The rect to be scrolled into view, relative to the node's border box, in CSS pixels.\\nWhen omitted, center of the node will be used, similar to Element.scrollIntoView.\",\n                            \"optional\": true,\n                            \"$ref\": \"Rect\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables DOM agent for the given page.\"\n                },\n                {\n                    \"name\": \"discardSearchResults\",\n                    \"description\": \"Discards search results from the session with the given id. `getSearchResults` should no longer\\nbe called for that search.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"searchId\",\n                            \"description\": \"Unique search session identifier.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables DOM agent for the given page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"includeWhitespace\",\n                            \"description\": \"Whether to include whitespaces in the children array of returned Nodes.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"none\",\n                                \"all\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"focus\",\n                    \"description\": \"Focuses the given element.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAttributes\",\n                    \"description\": \"Returns attributes for the specified node.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to retrieve attributes for.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"attributes\",\n                            \"description\": \"An interleaved array of node attribute names and values.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getBoxModel\",\n                    \"description\": \"Returns boxes for the given node.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"model\",\n                            \"description\": \"Box model for the node.\",\n                            \"$ref\": \"BoxModel\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getContentQuads\",\n                    \"description\": \"Returns quads that describe node position on the page. This method\\nmight return multiple quads for inline nodes.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"quads\",\n                            \"description\": \"Quads that describe node layout relative to viewport.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Quad\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getDocument\",\n                    \"description\": \"Returns the root DOM node (and optionally the subtree) to the caller.\\nImplicitly enables the DOM domain events for the current target.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pierce\",\n                            \"description\": \"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"root\",\n                            \"description\": \"Resulting node.\",\n                            \"$ref\": \"Node\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getFlattenedDocument\",\n                    \"description\": \"Returns the root DOM node (and optionally the subtree) to the caller.\\nDeprecated, as it is not designed to work well with the rest of the DOM agent.\\nUse DOMSnapshot.captureSnapshot instead.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pierce\",\n                            \"description\": \"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"Resulting node.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Node\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getNodesForSubtreeByStyle\",\n                    \"description\": \"Finds nodes with a given computed style in a subtree.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Node ID pointing to the root of a subtree.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"computedStyles\",\n                            \"description\": \"The style to filter nodes by (includes nodes if any of properties matches).\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSSComputedStyleProperty\"\n                            }\n                        },\n                        {\n                            \"name\": \"pierce\",\n                            \"description\": \"Whether or not iframes and shadow roots in the same target should be traversed when returning the\\nresults (default is false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"Resulting nodes.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getNodeForLocation\",\n                    \"description\": \"Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\\neither returned or not.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"includeUserAgentShadowDOM\",\n                            \"description\": \"False to skip to the nearest non-UA shadow root ancestor (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"ignorePointerEventsNone\",\n                            \"description\": \"Whether to ignore pointer-events: none on elements and hit test them.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Resulting node.\",\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame this node belongs to.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node at given coordinates, only when enabled and requested document.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getOuterHTML\",\n                    \"description\": \"Returns node's HTML markup.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"includeShadowDOM\",\n                            \"description\": \"Include all shadow roots. Equals to false if not specified.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"outerHTML\",\n                            \"description\": \"Outer HTML markup.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getRelayoutBoundary\",\n                    \"description\": \"Returns the id of the nearest ancestor that is a relayout boundary.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Relayout boundary node id for the given node.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSearchResults\",\n                    \"description\": \"Returns search results from given `fromIndex` to given `toIndex` from the search with the given\\nidentifier.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"searchId\",\n                            \"description\": \"Unique search session identifier.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fromIndex\",\n                            \"description\": \"Start index of the search result to be returned.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"toIndex\",\n                            \"description\": \"End index of the search result to be returned.\",\n                            \"type\": \"integer\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"Ids of the search result nodes.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"hideHighlight\",\n                    \"description\": \"Hides any highlight.\",\n                    \"redirect\": \"Overlay\"\n                },\n                {\n                    \"name\": \"highlightNode\",\n                    \"description\": \"Highlights DOM node.\",\n                    \"redirect\": \"Overlay\"\n                },\n                {\n                    \"name\": \"highlightRect\",\n                    \"description\": \"Highlights given rectangle.\",\n                    \"redirect\": \"Overlay\"\n                },\n                {\n                    \"name\": \"markUndoableState\",\n                    \"description\": \"Marks last undoable state.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"moveTo\",\n                    \"description\": \"Moves node into the new container, places it before the given anchor.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to move.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"targetNodeId\",\n                            \"description\": \"Id of the element to drop the moved node into.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"insertBeforeNodeId\",\n                            \"description\": \"Drop node before this one (if absent, the moved node becomes the last child of\\n`targetNodeId`).\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"New id of the moved node.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"performSearch\",\n                    \"description\": \"Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\\n`cancelSearch` to end this search session.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"query\",\n                            \"description\": \"Plain text or query selector or XPath search query.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"includeUserAgentShadowDOM\",\n                            \"description\": \"True to search in user agent shadow DOM.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"searchId\",\n                            \"description\": \"Unique search session identifier.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"resultCount\",\n                            \"description\": \"Number of search results.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"pushNodeByPathToFrontend\",\n                    \"description\": \"Requests that the node is sent to the caller given its path. // FIXME, use XPath\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"Path to node in the proprietary format.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node for given path.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"pushNodesByBackendIdsToFrontend\",\n                    \"description\": \"Requests that a batch of nodes is sent to the caller given their backend node ids.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"backendNodeIds\",\n                            \"description\": \"The array of backend node ids.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackendNodeId\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"The array of ids of pushed nodes that correspond to the backend ids specified in\\nbackendNodeIds.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"querySelector\",\n                    \"description\": \"Executes `querySelector` on a given node.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to query upon.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"selector\",\n                            \"description\": \"Selector string.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Query selector result.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"querySelectorAll\",\n                    \"description\": \"Executes `querySelectorAll` on a given node.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to query upon.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"selector\",\n                            \"description\": \"Selector string.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"Query selector result.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getTopLayerElements\",\n                    \"description\": \"Returns NodeIds of current top layer elements.\\nTop layer is rendered closest to the user within a viewport, therefore its elements always\\nappear on top of all other content.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"NodeIds of top layer elements\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getElementByRelation\",\n                    \"description\": \"Returns the NodeId of the matched element according to certain relations.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node from which to query the relation.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"relation\",\n                            \"description\": \"Type of relation to get.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"PopoverTarget\",\n                                \"InterestTarget\",\n                                \"CommandFor\"\n                            ]\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"NodeId of the element matching the queried relation.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"redo\",\n                    \"description\": \"Re-does the last undone action.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"removeAttribute\",\n                    \"description\": \"Removes attribute with given name from an element with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the element to remove attribute from.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name of the attribute to remove.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeNode\",\n                    \"description\": \"Removes node with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to remove.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestChildNodes\",\n                    \"description\": \"Requests that children of the node with given id are returned to the caller in form of\\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\\nthe specified depth.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to get children for.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pierce\",\n                            \"description\": \"Whether or not iframes and shadow roots should be traversed when returning the sub-tree\\n(default is false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestNode\",\n                    \"description\": \"Requests that the node is sent to the caller given the JavaScript node object reference. All\\nnodes that form the path from the node to the root are also sent to the client as a series of\\n`setChildNodes` notifications.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id to convert into node.\",\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Node id for given object.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resolveNode\",\n                    \"description\": \"Resolves the JavaScript node object for a given NodeId or BackendNodeId.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to resolve.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Backend identifier of the node to resolve.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic group name that can be used to release multiple objects.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Execution context in which to resolve the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.ExecutionContextId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"object\",\n                            \"description\": \"JavaScript object wrapper for given node.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAttributeValue\",\n                    \"description\": \"Sets attribute for an element with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the element to set attribute for.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Attribute name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Attribute value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAttributesAsText\",\n                    \"description\": \"Sets attributes on element with given id. This method is useful when user edits some existing\\nattribute value and types in several attribute name/value pairs.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the element to set attributes for.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Text with a number of attributes. Will parse this text using HTML parser.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Attribute name to replace with new attributes derived from text in case text parsed\\nsuccessfully.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setFileInputFiles\",\n                    \"description\": \"Sets files for the given file input element.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"files\",\n                            \"description\": \"Array of file paths to set.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setNodeStackTracesEnabled\",\n                    \"description\": \"Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"description\": \"Enable or disable.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getNodeStackTraces\",\n                    \"description\": \"Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to get stack traces for.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"creation\",\n                            \"description\": \"Creation stack trace, if available.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getFileInfo\",\n                    \"description\": \"Returns file information for the given\\nFile wrapper.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node wrapper.\",\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"path\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getDetachedDomNodes\",\n                    \"description\": \"Returns list of detached nodes\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"detachedNodes\",\n                            \"description\": \"The list of detached nodes\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DetachedElementInfo\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInspectedNode\",\n                    \"description\": \"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"DOM node id to be accessible by means of $x command line API.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setNodeName\",\n                    \"description\": \"Sets node name for a node with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to set name for.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"New node's name.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"New node's id.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setNodeValue\",\n                    \"description\": \"Sets node value for a node with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to set value for.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"New node's value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setOuterHTML\",\n                    \"description\": \"Sets node HTML markup, returns new node id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to set markup for.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"outerHTML\",\n                            \"description\": \"Outer HTML markup to set.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"undo\",\n                    \"description\": \"Undoes the last performed action.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"getFrameOwner\",\n                    \"description\": \"Returns iframe node that owns iframe with the given domain.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Resulting node.\",\n                            \"$ref\": \"BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node at given coordinates, only when enabled and requested document.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getContainerForNode\",\n                    \"description\": \"Returns the query container of the given node based on container query\\nconditions: containerName, physical and logical axes, and whether it queries\\nscroll-state or anchored elements. If no axes are provided and\\nqueriesScrollState is false, the style container is returned, which is the\\ndirect parent or the closest element with a matching container-name.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"containerName\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"physicalAxes\",\n                            \"optional\": true,\n                            \"$ref\": \"PhysicalAxes\"\n                        },\n                        {\n                            \"name\": \"logicalAxes\",\n                            \"optional\": true,\n                            \"$ref\": \"LogicalAxes\"\n                        },\n                        {\n                            \"name\": \"queriesScrollState\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"queriesAnchored\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The container node for the given node, or null if not found.\",\n                            \"optional\": true,\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getQueryingDescendantsForContainer\",\n                    \"description\": \"Returns the descendants of a container query container that have\\ncontainer queries against this container.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the container node to find querying descendants from.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"Descendant nodes with container queries against the given container.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAnchorElement\",\n                    \"description\": \"Returns the target anchor element of the given anchor query according to\\nhttps://www.w3.org/TR/css-anchor-position-1/#target.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the positioned element from which to find the anchor.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"anchorSpecifier\",\n                            \"description\": \"An optional anchor specifier, as defined in\\nhttps://www.w3.org/TR/css-anchor-position-1/#anchor-specifier.\\nIf not provided, it will return the implicit anchor element for\\nthe given positioned element.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The anchor element of the given anchor query.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"forceShowPopover\",\n                    \"description\": \"When enabling, this API force-opens the popover identified by nodeId\\nand keeps it open until disabled.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the popover HTMLElement\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"enable\",\n                            \"description\": \"If true, opens the popover and keeps it open. If false, closes the\\npopover if it was previously force-opened.\",\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"List of popovers that were closed in order to respect popover stacking order.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"attributeModified\",\n                    \"description\": \"Fired when `Element`'s attribute is modified.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node that has changed.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Attribute name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Attribute value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"adoptedStyleSheetsModified\",\n                    \"description\": \"Fired when `Element`'s adoptedStyleSheets are modified.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node that has changed.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"adoptedStyleSheets\",\n                            \"description\": \"New adoptedStyleSheets array.\",\n                            \"experimental\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StyleSheetId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attributeRemoved\",\n                    \"description\": \"Fired when `Element`'s attribute is removed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node that has changed.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"A ttribute name.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"characterDataModified\",\n                    \"description\": \"Mirrors `DOMCharacterDataModified` event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node that has changed.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"characterData\",\n                            \"description\": \"New text value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"childNodeCountUpdated\",\n                    \"description\": \"Fired when `Container`'s child node count has changed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node that has changed.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"childNodeCount\",\n                            \"description\": \"New node count.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"childNodeInserted\",\n                    \"description\": \"Mirrors `DOMNodeInserted` event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"parentNodeId\",\n                            \"description\": \"Id of the node that has changed.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"previousNodeId\",\n                            \"description\": \"Id of the previous sibling.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"node\",\n                            \"description\": \"Inserted node data.\",\n                            \"$ref\": \"Node\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"childNodeRemoved\",\n                    \"description\": \"Mirrors `DOMNodeRemoved` event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"parentNodeId\",\n                            \"description\": \"Parent id.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node that has been removed.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"distributedNodesUpdated\",\n                    \"description\": \"Called when distribution is changed.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"insertionPointId\",\n                            \"description\": \"Insertion point where distributed nodes were updated.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"distributedNodes\",\n                            \"description\": \"Distributed nodes for given insertion point.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackendNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"documentUpdated\",\n                    \"description\": \"Fired when `Document` has been totally updated. Node ids are no longer valid.\"\n                },\n                {\n                    \"name\": \"inlineStyleInvalidated\",\n                    \"description\": \"Fired when `Element`'s inline style is modified via a CSS property modification.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"Ids of the nodes for which the inline styles have been invalidated.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"pseudoElementAdded\",\n                    \"description\": \"Called when a pseudo element is added to an element.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"Pseudo element's parent element id.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"pseudoElement\",\n                            \"description\": \"The added pseudo element.\",\n                            \"$ref\": \"Node\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"topLayerElementsUpdated\",\n                    \"description\": \"Called when top layer elements are changed.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"scrollableFlagUpdated\",\n                    \"description\": \"Fired when a node's scrollability state changes.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The id of the node.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"isScrollable\",\n                            \"description\": \"If the node is scrollable.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"adRelatedStateUpdated\",\n                    \"description\": \"Fired when a node's ad related state changes.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The id of the node.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"isAdRelated\",\n                            \"description\": \"If the node is ad related.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"affectedByStartingStylesFlagUpdated\",\n                    \"description\": \"Fired when a node's starting styles changes.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"The id of the node.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"affectedByStartingStyles\",\n                            \"description\": \"If the node has starting styles.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"pseudoElementRemoved\",\n                    \"description\": \"Called when a pseudo element is removed from an element.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"Pseudo element's parent element id.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"pseudoElementId\",\n                            \"description\": \"The removed pseudo element id.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setChildNodes\",\n                    \"description\": \"Fired when backend wants to provide client with the missing DOM structure. This happens upon\\nmost of the calls requesting node ids.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"Parent node id to populate with children.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"Child nodes array.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Node\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"shadowRootPopped\",\n                    \"description\": \"Called when shadow root is popped from the element.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"hostId\",\n                            \"description\": \"Host element id.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"rootId\",\n                            \"description\": \"Shadow root id.\",\n                            \"$ref\": \"NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"shadowRootPushed\",\n                    \"description\": \"Called when shadow root is pushed into the element.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"hostId\",\n                            \"description\": \"Host element id.\",\n                            \"$ref\": \"NodeId\"\n                        },\n                        {\n                            \"name\": \"root\",\n                            \"description\": \"Shadow root.\",\n                            \"$ref\": \"Node\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"DOMDebugger\",\n            \"description\": \"DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\\nexecution will stop on these operations as if there was a regular breakpoint set.\",\n            \"dependencies\": [\n                \"DOM\",\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"DOMBreakpointType\",\n                    \"description\": \"DOM breakpoint type.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"subtree-modified\",\n                        \"attribute-modified\",\n                        \"node-removed\"\n                    ]\n                },\n                {\n                    \"id\": \"CSPViolationType\",\n                    \"description\": \"CSP Violation type.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"trustedtype-sink-violation\",\n                        \"trustedtype-policy-violation\"\n                    ]\n                },\n                {\n                    \"id\": \"EventListener\",\n                    \"description\": \"Object event listener.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"`EventListener`'s type.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"useCapture\",\n                            \"description\": \"`EventListener`'s useCapture.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"passive\",\n                            \"description\": \"`EventListener`'s passive flag.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"once\",\n                            \"description\": \"`EventListener`'s once flag.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Script id of the handler code.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number in the script (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Column number in the script (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"handler\",\n                            \"description\": \"Event handler function value.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"originalHandler\",\n                            \"description\": \"Event original handler function value.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Node the listener is added to (if any).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getEventListeners\",\n                    \"description\": \"Returns event listeners of the given object.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Identifier of the object to return listeners for.\",\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pierce\",\n                            \"description\": \"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false). Reports listeners for all contexts if pierce is enabled.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"listeners\",\n                            \"description\": \"Array of relevant listeners.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"EventListener\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeDOMBreakpoint\",\n                    \"description\": \"Removes DOM breakpoint that was set using `setDOMBreakpoint`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to remove breakpoint from.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the breakpoint to remove.\",\n                            \"$ref\": \"DOMBreakpointType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeEventListenerBreakpoint\",\n                    \"description\": \"Removes breakpoint on particular DOM event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"Event name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"targetName\",\n                            \"description\": \"EventTarget interface name.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeInstrumentationBreakpoint\",\n                    \"description\": \"Removes breakpoint on particular native event.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"EventBreakpoints\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"Instrumentation name to stop on.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeXHRBreakpoint\",\n                    \"description\": \"Removes breakpoint from XMLHttpRequest.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Resource URL substring.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBreakOnCSPViolation\",\n                    \"description\": \"Sets breakpoint on particular CSP violations.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"violationTypes\",\n                            \"description\": \"CSP Violations to stop upon.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CSPViolationType\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDOMBreakpoint\",\n                    \"description\": \"Sets breakpoint on particular operation with DOM.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to set breakpoint on.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the operation to stop upon.\",\n                            \"$ref\": \"DOMBreakpointType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setEventListenerBreakpoint\",\n                    \"description\": \"Sets breakpoint on particular DOM event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"DOM Event name to stop on (any DOM event will do).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"targetName\",\n                            \"description\": \"EventTarget interface name to stop on. If equal to `\\\"*\\\"` or not provided, will stop on any\\nEventTarget.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInstrumentationBreakpoint\",\n                    \"description\": \"Sets breakpoint on particular native event.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"EventBreakpoints\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"Instrumentation name to stop on.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setXHRBreakpoint\",\n                    \"description\": \"Sets breakpoint on XMLHttpRequest.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Resource URL substring. All XHRs having this substring in the URL will get stopped upon.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"DOMSnapshot\",\n            \"description\": \"This domain facilitates obtaining document snapshots with DOM, layout, and style information.\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"CSS\",\n                \"DOM\",\n                \"DOMDebugger\",\n                \"Page\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"DOMNode\",\n                    \"description\": \"A Node in the DOM tree.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeType\",\n                            \"description\": \"`Node`'s nodeType.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"nodeName\",\n                            \"description\": \"`Node`'s nodeName.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"nodeValue\",\n                            \"description\": \"`Node`'s nodeValue.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"textValue\",\n                            \"description\": \"Only set for textarea elements, contains the text value.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"inputValue\",\n                            \"description\": \"Only set for input elements, contains the input's associated text value.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"inputChecked\",\n                            \"description\": \"Only set for radio and checkbox input elements, indicates if the element has been checked\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"optionSelected\",\n                            \"description\": \"Only set for option elements, indicates if the element has been selected\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"`Node`'s id, corresponds to DOM.Node.backendNodeId.\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"childNodeIndexes\",\n                            \"description\": \"The indexes of the node's child nodes in the `domNodes` array returned by `getSnapshot`, if\\nany.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"attributes\",\n                            \"description\": \"Attributes of an `Element` node.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NameValue\"\n                            }\n                        },\n                        {\n                            \"name\": \"pseudoElementIndexes\",\n                            \"description\": \"Indexes of pseudo elements associated with this node in the `domNodes` array returned by\\n`getSnapshot`, if any.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"layoutNodeIndex\",\n                            \"description\": \"The index of the node's related layout tree node in the `layoutTreeNodes` array returned by\\n`getSnapshot`, if any.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"documentURL\",\n                            \"description\": \"Document URL that `Document` or `FrameOwner` node points to.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"baseURL\",\n                            \"description\": \"Base URL that `Document` or `FrameOwner` node uses for URL completion.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contentLanguage\",\n                            \"description\": \"Only set for documents, contains the document's content language.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"documentEncoding\",\n                            \"description\": \"Only set for documents, contains the document's character set encoding.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"publicId\",\n                            \"description\": \"`DocumentType` node's publicId.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"systemId\",\n                            \"description\": \"`DocumentType` node's systemId.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame ID for frame owner elements and also for the document node.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"contentDocumentIndex\",\n                            \"description\": \"The index of a frame owner element's content document in the `domNodes` array returned by\\n`getSnapshot`, if any.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pseudoType\",\n                            \"description\": \"Type of a pseudo element node.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.PseudoType\"\n                        },\n                        {\n                            \"name\": \"shadowRootType\",\n                            \"description\": \"Shadow root type.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.ShadowRootType\"\n                        },\n                        {\n                            \"name\": \"isClickable\",\n                            \"description\": \"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"eventListeners\",\n                            \"description\": \"Details of the node's event listeners, if any.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOMDebugger.EventListener\"\n                            }\n                        },\n                        {\n                            \"name\": \"currentSourceURL\",\n                            \"description\": \"The selected url for nodes with a srcset attribute.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"originURL\",\n                            \"description\": \"The url of the script (if any) that generates this node.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scrollOffsetX\",\n                            \"description\": \"Scroll offsets, set when this node is a Document.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scrollOffsetY\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InlineTextBox\",\n                    \"description\": \"Details of post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"boundingBox\",\n                            \"description\": \"The bounding box in document coordinates. Note that scroll offset of the document is ignored.\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"startCharacterIndex\",\n                            \"description\": \"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"numCharacters\",\n                            \"description\": \"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LayoutTreeNode\",\n                    \"description\": \"Details of an element in the DOM tree with a LayoutObject.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"domNodeIndex\",\n                            \"description\": \"The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"boundingBox\",\n                            \"description\": \"The bounding box in document coordinates. Note that scroll offset of the document is ignored.\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"layoutText\",\n                            \"description\": \"Contents of the LayoutText, if any.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"inlineTextNodes\",\n                            \"description\": \"The post-layout inline text nodes, if any.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InlineTextBox\"\n                            }\n                        },\n                        {\n                            \"name\": \"styleIndex\",\n                            \"description\": \"Index into the `computedStyles` array returned by `getSnapshot`.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"paintOrder\",\n                            \"description\": \"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ngetSnapshot was true.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"isStackingContext\",\n                            \"description\": \"Set to true to indicate the element begins a new stacking context.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ComputedStyle\",\n                    \"description\": \"A subset of the full ComputedStyle as defined by the request whitelist.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"properties\",\n                            \"description\": \"Name/value pairs of computed style properties.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NameValue\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"NameValue\",\n                    \"description\": \"A name/value pair.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Attribute/property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Attribute/property value.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StringIndex\",\n                    \"description\": \"Index of the string in the strings table.\",\n                    \"type\": \"integer\"\n                },\n                {\n                    \"id\": \"ArrayOfStrings\",\n                    \"description\": \"Index of the string in the strings table.\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"StringIndex\"\n                    }\n                },\n                {\n                    \"id\": \"RareStringData\",\n                    \"description\": \"Data that is only present on rare nodes.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"index\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StringIndex\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RareBooleanData\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"index\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RareIntegerData\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"index\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Rectangle\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"number\"\n                    }\n                },\n                {\n                    \"id\": \"DocumentSnapshot\",\n                    \"description\": \"Document snapshot.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"documentURL\",\n                            \"description\": \"Document URL that `Document` or `FrameOwner` node points to.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Document title.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"baseURL\",\n                            \"description\": \"Base URL that `Document` or `FrameOwner` node uses for URL completion.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"contentLanguage\",\n                            \"description\": \"Contains the document's content language.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"encodingName\",\n                            \"description\": \"Contains the document's character set encoding.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"publicId\",\n                            \"description\": \"`DocumentType` node's publicId.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"systemId\",\n                            \"description\": \"`DocumentType` node's systemId.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame ID for frame owner elements and also for the document node.\",\n                            \"$ref\": \"StringIndex\"\n                        },\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"A table with dom nodes.\",\n                            \"$ref\": \"NodeTreeSnapshot\"\n                        },\n                        {\n                            \"name\": \"layout\",\n                            \"description\": \"The nodes in the layout tree.\",\n                            \"$ref\": \"LayoutTreeSnapshot\"\n                        },\n                        {\n                            \"name\": \"textBoxes\",\n                            \"description\": \"The post-layout inline text nodes.\",\n                            \"$ref\": \"TextBoxSnapshot\"\n                        },\n                        {\n                            \"name\": \"scrollOffsetX\",\n                            \"description\": \"Horizontal scroll offset.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scrollOffsetY\",\n                            \"description\": \"Vertical scroll offset.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"contentWidth\",\n                            \"description\": \"Document content width.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"contentHeight\",\n                            \"description\": \"Document content height.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"NodeTreeSnapshot\",\n                    \"description\": \"Table containing nodes.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"parentIndex\",\n                            \"description\": \"Parent node index.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"nodeType\",\n                            \"description\": \"`Node`'s nodeType.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"shadowRootType\",\n                            \"description\": \"Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        },\n                        {\n                            \"name\": \"nodeName\",\n                            \"description\": \"`Node`'s nodeName.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StringIndex\"\n                            }\n                        },\n                        {\n                            \"name\": \"nodeValue\",\n                            \"description\": \"`Node`'s nodeValue.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StringIndex\"\n                            }\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"`Node`'s id, corresponds to DOM.Node.backendNodeId.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOM.BackendNodeId\"\n                            }\n                        },\n                        {\n                            \"name\": \"attributes\",\n                            \"description\": \"Attributes of an `Element` node. Flatten name, value pairs.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ArrayOfStrings\"\n                            }\n                        },\n                        {\n                            \"name\": \"textValue\",\n                            \"description\": \"Only set for textarea elements, contains the text value.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        },\n                        {\n                            \"name\": \"inputValue\",\n                            \"description\": \"Only set for input elements, contains the input's associated text value.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        },\n                        {\n                            \"name\": \"inputChecked\",\n                            \"description\": \"Only set for radio and checkbox input elements, indicates if the element has been checked\",\n                            \"optional\": true,\n                            \"$ref\": \"RareBooleanData\"\n                        },\n                        {\n                            \"name\": \"optionSelected\",\n                            \"description\": \"Only set for option elements, indicates if the element has been selected\",\n                            \"optional\": true,\n                            \"$ref\": \"RareBooleanData\"\n                        },\n                        {\n                            \"name\": \"contentDocumentIndex\",\n                            \"description\": \"The index of the document in the list of the snapshot documents.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareIntegerData\"\n                        },\n                        {\n                            \"name\": \"pseudoType\",\n                            \"description\": \"Type of a pseudo element node.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        },\n                        {\n                            \"name\": \"pseudoIdentifier\",\n                            \"description\": \"Pseudo element identifier for this node. Only present if there is a\\nvalid pseudoType.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        },\n                        {\n                            \"name\": \"isClickable\",\n                            \"description\": \"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareBooleanData\"\n                        },\n                        {\n                            \"name\": \"currentSourceURL\",\n                            \"description\": \"The selected url for nodes with a srcset attribute.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        },\n                        {\n                            \"name\": \"originURL\",\n                            \"description\": \"The url of the script (if any) that generates this node.\",\n                            \"optional\": true,\n                            \"$ref\": \"RareStringData\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LayoutTreeSnapshot\",\n                    \"description\": \"Table of details of an element in the DOM tree with a LayoutObject.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeIndex\",\n                            \"description\": \"Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"styles\",\n                            \"description\": \"Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ArrayOfStrings\"\n                            }\n                        },\n                        {\n                            \"name\": \"bounds\",\n                            \"description\": \"The absolute position bounding box.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Rectangle\"\n                            }\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Contents of the LayoutText, if any.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StringIndex\"\n                            }\n                        },\n                        {\n                            \"name\": \"stackingContexts\",\n                            \"description\": \"Stacking context information.\",\n                            \"$ref\": \"RareBooleanData\"\n                        },\n                        {\n                            \"name\": \"paintOrders\",\n                            \"description\": \"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ncaptureSnapshot was true.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"offsetRects\",\n                            \"description\": \"The offset rect of nodes. Only available when includeDOMRects is set to true\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Rectangle\"\n                            }\n                        },\n                        {\n                            \"name\": \"scrollRects\",\n                            \"description\": \"The scroll rect of nodes. Only available when includeDOMRects is set to true\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Rectangle\"\n                            }\n                        },\n                        {\n                            \"name\": \"clientRects\",\n                            \"description\": \"The client rect of nodes. Only available when includeDOMRects is set to true\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Rectangle\"\n                            }\n                        },\n                        {\n                            \"name\": \"blendedBackgroundColors\",\n                            \"description\": \"The list of background colors that are blended with colors of overlapping elements.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"StringIndex\"\n                            }\n                        },\n                        {\n                            \"name\": \"textColorOpacities\",\n                            \"description\": \"The list of computed text opacities.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"number\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"TextBoxSnapshot\",\n                    \"description\": \"Table of details of the post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"layoutIndex\",\n                            \"description\": \"Index of the layout tree node that owns this box collection.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"bounds\",\n                            \"description\": \"The absolute position bounding box.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Rectangle\"\n                            }\n                        },\n                        {\n                            \"name\": \"start\",\n                            \"description\": \"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"length\",\n                            \"description\": \"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables DOM snapshot agent for the given page.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables DOM snapshot agent for the given page.\"\n                },\n                {\n                    \"name\": \"getSnapshot\",\n                    \"description\": \"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"computedStyleWhitelist\",\n                            \"description\": \"Whitelist of computed styles to return.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"includeEventListeners\",\n                            \"description\": \"Whether or not to retrieve details of DOM listeners (default false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includePaintOrder\",\n                            \"description\": \"Whether to determine and include the paint order index of LayoutTreeNodes (default false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeUserAgentShadowTree\",\n                            \"description\": \"Whether to include UA shadow tree in the snapshot (default false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"domNodes\",\n                            \"description\": \"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOMNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"layoutTreeNodes\",\n                            \"description\": \"The nodes in the layout tree.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"LayoutTreeNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"computedStyles\",\n                            \"description\": \"Whitelisted ComputedStyle properties for each node in the layout tree.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ComputedStyle\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"captureSnapshot\",\n                    \"description\": \"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"computedStyles\",\n                            \"description\": \"Whitelist of computed styles to return.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"includePaintOrder\",\n                            \"description\": \"Whether to include layout object paint orders into the snapshot.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeDOMRects\",\n                            \"description\": \"Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeBlendedBackgroundColors\",\n                            \"description\": \"Whether to include blended background colors in the snapshot (default: false).\\nBlended background color is achieved by blending background colors of all elements\\nthat overlap with the current element.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeTextColorOpacities\",\n                            \"description\": \"Whether to include text color opacity in the snapshot (default: false).\\nAn element might have the opacity property set that affects the text color of the element.\\nThe final text color opacity is computed based on the opacity of all overlapping elements.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"documents\",\n                            \"description\": \"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DocumentSnapshot\"\n                            }\n                        },\n                        {\n                            \"name\": \"strings\",\n                            \"description\": \"Shared string table that all string properties refer to with indexes.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"DOMStorage\",\n            \"description\": \"Query and modify DOM storage.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"SerializedStorageKey\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"StorageId\",\n                    \"description\": \"DOM Storage identifier.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"Security origin for the storage.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Represents a key by which DOM Storage keys its CachedStorageAreas\",\n                            \"optional\": true,\n                            \"$ref\": \"SerializedStorageKey\"\n                        },\n                        {\n                            \"name\": \"isLocalStorage\",\n                            \"description\": \"Whether the storage is local storage (not session storage).\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Item\",\n                    \"description\": \"DOM Storage item.\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"clear\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables storage tracking, prevents storage events from being sent to the client.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables storage tracking, storage events will now be delivered to the client.\"\n                },\n                {\n                    \"name\": \"getDOMStorageItems\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"entries\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Item\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeDOMStorageItem\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDOMStorageItem\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"domStorageItemAdded\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"newValue\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"domStorageItemRemoved\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"domStorageItemUpdated\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"oldValue\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"newValue\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"domStorageItemsCleared\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageId\",\n                            \"$ref\": \"StorageId\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"DeviceAccess\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"RequestId\",\n                    \"description\": \"Device request id.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"DeviceId\",\n                    \"description\": \"A device id.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"PromptDevice\",\n                    \"description\": \"Device information displayed in a user prompt to select a device.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"DeviceId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Display name as it appears in a device request user prompt.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enable events in this domain.\"\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disable events in this domain.\"\n                },\n                {\n                    \"name\": \"selectPrompt\",\n                    \"description\": \"Select a device in response to a DeviceAccess.deviceRequestPrompted event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"deviceId\",\n                            \"$ref\": \"DeviceId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"cancelPrompt\",\n                    \"description\": \"Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"deviceRequestPrompted\",\n                    \"description\": \"A device request opened a user prompt to select a device. Respond with the\\nselectPrompt or cancelPrompt command.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"devices\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PromptDevice\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"DeviceOrientation\",\n            \"experimental\": true,\n            \"commands\": [\n                {\n                    \"name\": \"clearDeviceOrientationOverride\",\n                    \"description\": \"Clears the overridden Device Orientation.\"\n                },\n                {\n                    \"name\": \"setDeviceOrientationOverride\",\n                    \"description\": \"Overrides the Device Orientation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"alpha\",\n                            \"description\": \"Mock alpha\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"beta\",\n                            \"description\": \"Mock beta\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"gamma\",\n                            \"description\": \"Mock gamma\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Emulation\",\n            \"description\": \"This domain emulates different environments for the page.\",\n            \"dependencies\": [\n                \"DOM\",\n                \"Page\",\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"SafeAreaInsets\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"top\",\n                            \"description\": \"Overrides safe-area-inset-top.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"topMax\",\n                            \"description\": \"Overrides safe-area-max-inset-top.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"left\",\n                            \"description\": \"Overrides safe-area-inset-left.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"leftMax\",\n                            \"description\": \"Overrides safe-area-max-inset-left.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"bottom\",\n                            \"description\": \"Overrides safe-area-inset-bottom.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"bottomMax\",\n                            \"description\": \"Overrides safe-area-max-inset-bottom.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"right\",\n                            \"description\": \"Overrides safe-area-inset-right.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"rightMax\",\n                            \"description\": \"Overrides safe-area-max-inset-right.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScreenOrientation\",\n                    \"description\": \"Screen orientation.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Orientation type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"portraitPrimary\",\n                                \"portraitSecondary\",\n                                \"landscapePrimary\",\n                                \"landscapeSecondary\"\n                            ]\n                        },\n                        {\n                            \"name\": \"angle\",\n                            \"description\": \"Orientation angle.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DisplayFeature\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"orientation\",\n                            \"description\": \"Orientation of a display feature in relation to screen\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"vertical\",\n                                \"horizontal\"\n                            ]\n                        },\n                        {\n                            \"name\": \"offset\",\n                            \"description\": \"The offset from the screen origin in either the x (for vertical\\norientation) or y (for horizontal orientation) direction.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maskLength\",\n                            \"description\": \"A display feature may mask content such that it is not physically\\ndisplayed - this length along with the offset describes this area.\\nA display feature that only splits content will have a 0 mask_length.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DevicePosture\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Current posture of the device\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"continuous\",\n                                \"folded\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"MediaFeature\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"VirtualTimePolicy\",\n                    \"description\": \"advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\\nresource fetches.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"advance\",\n                        \"pause\",\n                        \"pauseIfNetworkFetchesPending\"\n                    ]\n                },\n                {\n                    \"id\": \"UserAgentBrandVersion\",\n                    \"description\": \"Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"brand\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"version\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"UserAgentMetadata\",\n                    \"description\": \"Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints\\nMissing optional values will be filled in by the target with what it would normally use.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"brands\",\n                            \"description\": \"Brands appearing in Sec-CH-UA.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"UserAgentBrandVersion\"\n                            }\n                        },\n                        {\n                            \"name\": \"fullVersionList\",\n                            \"description\": \"Brands appearing in Sec-CH-UA-Full-Version-List.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"UserAgentBrandVersion\"\n                            }\n                        },\n                        {\n                            \"name\": \"fullVersion\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"platform\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"platformVersion\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"architecture\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"model\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mobile\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"bitness\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"wow64\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"formFactors\",\n                            \"description\": \"Used to specify User Agent form-factor values.\\nSee https://wicg.github.io/ua-client-hints/#sec-ch-ua-form-factors\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SensorType\",\n                    \"description\": \"Used to specify sensor types to emulate.\\nSee https://w3c.github.io/sensors/#automation for more information.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"absolute-orientation\",\n                        \"accelerometer\",\n                        \"ambient-light\",\n                        \"gravity\",\n                        \"gyroscope\",\n                        \"linear-acceleration\",\n                        \"magnetometer\",\n                        \"relative-orientation\"\n                    ]\n                },\n                {\n                    \"id\": \"SensorMetadata\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"available\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"minimumFrequency\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"maximumFrequency\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SensorReadingSingle\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SensorReadingXYZ\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"x\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"z\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SensorReadingQuaternion\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"x\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"z\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"w\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SensorReading\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"single\",\n                            \"optional\": true,\n                            \"$ref\": \"SensorReadingSingle\"\n                        },\n                        {\n                            \"name\": \"xyz\",\n                            \"optional\": true,\n                            \"$ref\": \"SensorReadingXYZ\"\n                        },\n                        {\n                            \"name\": \"quaternion\",\n                            \"optional\": true,\n                            \"$ref\": \"SensorReadingQuaternion\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PressureSource\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"cpu\"\n                    ]\n                },\n                {\n                    \"id\": \"PressureState\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"nominal\",\n                        \"fair\",\n                        \"serious\",\n                        \"critical\"\n                    ]\n                },\n                {\n                    \"id\": \"PressureMetadata\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"available\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WorkAreaInsets\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"top\",\n                            \"description\": \"Work area top inset in pixels. Default is 0;\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"left\",\n                            \"description\": \"Work area left inset in pixels. Default is 0;\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"bottom\",\n                            \"description\": \"Work area bottom inset in pixels. Default is 0;\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"right\",\n                            \"description\": \"Work area right inset in pixels. Default is 0;\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScreenId\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ScreenInfo\",\n                    \"description\": \"Screen information similar to the one returned by window.getScreenDetails() method,\\nsee https://w3c.github.io/window-management/#screendetailed.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"left\",\n                            \"description\": \"Offset of the left edge of the screen.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"top\",\n                            \"description\": \"Offset of the top edge of the screen.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Width of the screen.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Height of the screen.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"availLeft\",\n                            \"description\": \"Offset of the left edge of the available screen area.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"availTop\",\n                            \"description\": \"Offset of the top edge of the available screen area.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"availWidth\",\n                            \"description\": \"Width of the available screen area.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"availHeight\",\n                            \"description\": \"Height of the available screen area.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"devicePixelRatio\",\n                            \"description\": \"Specifies the screen's device pixel ratio.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"orientation\",\n                            \"description\": \"Specifies the screen's orientation.\",\n                            \"$ref\": \"ScreenOrientation\"\n                        },\n                        {\n                            \"name\": \"colorDepth\",\n                            \"description\": \"Specifies the screen's color depth in bits.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"isExtended\",\n                            \"description\": \"Indicates whether the device has multiple screens.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isInternal\",\n                            \"description\": \"Indicates whether the screen is internal to the device or external, attached to the device.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isPrimary\",\n                            \"description\": \"Indicates whether the screen is set as the the operating system primary screen.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"label\",\n                            \"description\": \"Specifies the descriptive label for the screen.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Specifies the unique identifier of the screen.\",\n                            \"$ref\": \"ScreenId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DisabledImageType\",\n                    \"description\": \"Enum of image types that can be disabled.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"avif\",\n                        \"jxl\",\n                        \"webp\"\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"canEmulate\",\n                    \"description\": \"Tells whether emulation is supported.\",\n                    \"deprecated\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"True if emulation is supported.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearDeviceMetricsOverride\",\n                    \"description\": \"Clears the overridden device metrics.\"\n                },\n                {\n                    \"name\": \"clearGeolocationOverride\",\n                    \"description\": \"Clears the overridden Geolocation Position and Error.\"\n                },\n                {\n                    \"name\": \"resetPageScaleFactor\",\n                    \"description\": \"Requests that page scale factor is reset to initial values.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"setFocusEmulationEnabled\",\n                    \"description\": \"Enables or disables simulating a focused and active page.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether to enable to disable focus emulation.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAutoDarkModeOverride\",\n                    \"description\": \"Automatically render all web contents using a dark theme.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether to enable or disable automatic dark mode.\\nIf not specified, any existing override will be cleared.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCPUThrottlingRate\",\n                    \"description\": \"Enables CPU throttling to emulate slow CPUs.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"rate\",\n                            \"description\": \"Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDefaultBackgroundColorOverride\",\n                    \"description\": \"Sets or clears an override of the default background color of the frame. This override is used\\nif the content does not specify one.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"color\",\n                            \"description\": \"RGBA of the default background color. If not specified, any existing override will be\\ncleared.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSafeAreaInsetsOverride\",\n                    \"description\": \"Overrides the values for env(safe-area-inset-*) and env(safe-area-max-inset-*). Unset values will cause the\\nrespective variables to be undefined, even if previously overridden.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"insets\",\n                            \"$ref\": \"SafeAreaInsets\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDeviceMetricsOverride\",\n                    \"description\": \"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\\"device-width\\\"/\\\"device-height\\\"-related CSS media\\nquery results).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"deviceScaleFactor\",\n                            \"description\": \"Overriding device scale factor value. 0 disables the override.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"mobile\",\n                            \"description\": \"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"scale\",\n                            \"description\": \"Scale to apply to resulting view image.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"screenWidth\",\n                            \"description\": \"Overriding screen width value in pixels (minimum 0, maximum 10000000).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"screenHeight\",\n                            \"description\": \"Overriding screen height value in pixels (minimum 0, maximum 10000000).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"positionX\",\n                            \"description\": \"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"positionY\",\n                            \"description\": \"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"dontSetVisibleSize\",\n                            \"description\": \"Do not set visible view size, rely upon explicit setVisibleSize call.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"screenOrientation\",\n                            \"description\": \"Screen orientation override.\",\n                            \"optional\": true,\n                            \"$ref\": \"ScreenOrientation\"\n                        },\n                        {\n                            \"name\": \"viewport\",\n                            \"description\": \"If set, the visible area of the page will be overridden to this viewport. This viewport\\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Page.Viewport\"\n                        },\n                        {\n                            \"name\": \"displayFeature\",\n                            \"description\": \"If set, the display feature of a multi-segment screen. If not set, multi-segment support\\nis turned-off.\\nDeprecated, use Emulation.setDisplayFeaturesOverride.\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DisplayFeature\"\n                        },\n                        {\n                            \"name\": \"devicePosture\",\n                            \"description\": \"If set, the posture of a foldable device. If not set the posture is set\\nto continuous.\\nDeprecated, use Emulation.setDevicePostureOverride.\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DevicePosture\"\n                        },\n                        {\n                            \"name\": \"scrollbarType\",\n                            \"description\": \"Scrollbar type. Default: `default`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"overlay\",\n                                \"default\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDevicePostureOverride\",\n                    \"description\": \"Start reporting the given posture value to the Device Posture API.\\nThis override can also be set in setDeviceMetricsOverride().\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"posture\",\n                            \"$ref\": \"DevicePosture\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearDevicePostureOverride\",\n                    \"description\": \"Clears a device posture override set with either setDeviceMetricsOverride()\\nor setDevicePostureOverride() and starts using posture information from the\\nplatform again.\\nDoes nothing if no override is set.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"setDisplayFeaturesOverride\",\n                    \"description\": \"Start using the given display features to pupulate the Viewport Segments API.\\nThis override can also be set in setDeviceMetricsOverride().\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"features\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DisplayFeature\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearDisplayFeaturesOverride\",\n                    \"description\": \"Clears the display features override set with either setDeviceMetricsOverride()\\nor setDisplayFeaturesOverride() and starts using display features from the\\nplatform again.\\nDoes nothing if no override is set.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"setScrollbarsHidden\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"hidden\",\n                            \"description\": \"Whether scrollbars should be always hidden.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDocumentCookieDisabled\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"disabled\",\n                            \"description\": \"Whether document.coookie API should be disabled.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setEmitTouchEventsForMouse\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether touch emulation based on mouse input should be enabled.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"configuration\",\n                            \"description\": \"Touch/gesture events configuration. Default: current platform.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mobile\",\n                                \"desktop\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setEmulatedMedia\",\n                    \"description\": \"Emulates the given media type or media feature for CSS media queries.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"media\",\n                            \"description\": \"Media type to emulate. Empty string disables the override.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"features\",\n                            \"description\": \"Media features to emulate.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"MediaFeature\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setEmulatedVisionDeficiency\",\n                    \"description\": \"Emulates the given vision deficiency.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Vision deficiency to emulate. Order: best-effort emulations come first, followed by any\\nphysiologically accurate emulations for medically recognized color vision deficiencies.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"none\",\n                                \"blurredVision\",\n                                \"reducedContrast\",\n                                \"achromatopsia\",\n                                \"deuteranopia\",\n                                \"protanopia\",\n                                \"tritanopia\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setEmulatedOSTextScale\",\n                    \"description\": \"Emulates the given OS text scale.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scale\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setGeolocationOverride\",\n                    \"description\": \"Overrides the Geolocation Position or Error. Omitting latitude, longitude or\\naccuracy emulates position unavailable.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"latitude\",\n                            \"description\": \"Mock latitude\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"longitude\",\n                            \"description\": \"Mock longitude\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"accuracy\",\n                            \"description\": \"Mock accuracy\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"altitude\",\n                            \"description\": \"Mock altitude\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"altitudeAccuracy\",\n                            \"description\": \"Mock altitudeAccuracy\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"heading\",\n                            \"description\": \"Mock heading\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"speed\",\n                            \"description\": \"Mock speed\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getOverriddenSensorInformation\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"SensorType\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"requestedSamplingFrequency\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSensorOverrideEnabled\",\n                    \"description\": \"Overrides a platform sensor of a given type. If |enabled| is true, calls to\\nSensor.start() will use a virtual sensor as backend rather than fetching\\ndata from a real hardware sensor. Otherwise, existing virtual\\nsensor-backend Sensor objects will fire an error event and new calls to\\nSensor.start() will attempt to use a real sensor instead.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"SensorType\"\n                        },\n                        {\n                            \"name\": \"metadata\",\n                            \"optional\": true,\n                            \"$ref\": \"SensorMetadata\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSensorOverrideReadings\",\n                    \"description\": \"Updates the sensor readings reported by a sensor type previously overridden\\nby setSensorOverrideEnabled.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"SensorType\"\n                        },\n                        {\n                            \"name\": \"reading\",\n                            \"$ref\": \"SensorReading\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPressureSourceOverrideEnabled\",\n                    \"description\": \"Overrides a pressure source of a given type, as used by the Compute\\nPressure API, so that updates to PressureObserver.observe() are provided\\nvia setPressureStateOverride instead of being retrieved from\\nplatform-provided telemetry data.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"source\",\n                            \"$ref\": \"PressureSource\"\n                        },\n                        {\n                            \"name\": \"metadata\",\n                            \"optional\": true,\n                            \"$ref\": \"PressureMetadata\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPressureStateOverride\",\n                    \"description\": \"TODO: OBSOLETE: To remove when setPressureDataOverride is merged.\\nProvides a given pressure state that will be processed and eventually be\\ndelivered to PressureObserver users. |source| must have been previously\\noverridden by setPressureSourceOverrideEnabled.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"source\",\n                            \"$ref\": \"PressureSource\"\n                        },\n                        {\n                            \"name\": \"state\",\n                            \"$ref\": \"PressureState\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPressureDataOverride\",\n                    \"description\": \"Provides a given pressure data set that will be processed and eventually be\\ndelivered to PressureObserver users. |source| must have been previously\\noverridden by setPressureSourceOverrideEnabled.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"source\",\n                            \"$ref\": \"PressureSource\"\n                        },\n                        {\n                            \"name\": \"state\",\n                            \"$ref\": \"PressureState\"\n                        },\n                        {\n                            \"name\": \"ownContributionEstimate\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setIdleOverride\",\n                    \"description\": \"Overrides the Idle state.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"isUserActive\",\n                            \"description\": \"Mock isUserActive\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isScreenUnlocked\",\n                            \"description\": \"Mock isScreenUnlocked\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearIdleOverride\",\n                    \"description\": \"Clears Idle state overrides.\"\n                },\n                {\n                    \"name\": \"setNavigatorOverrides\",\n                    \"description\": \"Overrides value returned by the javascript navigator object.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"platform\",\n                            \"description\": \"The platform navigator.platform should return.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPageScaleFactor\",\n                    \"description\": \"Sets a specified page scale factor.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"pageScaleFactor\",\n                            \"description\": \"Page scale factor.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setScriptExecutionDisabled\",\n                    \"description\": \"Switches script execution in the page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Whether script execution should be disabled in the page.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setTouchEmulationEnabled\",\n                    \"description\": \"Enables touch on platforms which do not support them.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether the touch event emulation should be enabled.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"maxTouchPoints\",\n                            \"description\": \"Maximum touch points supported. Defaults to one.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setVirtualTimePolicy\",\n                    \"description\": \"Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\\nthe current virtual time policy.  Note this supersedes any previous time budget.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"policy\",\n                            \"$ref\": \"VirtualTimePolicy\"\n                        },\n                        {\n                            \"name\": \"budget\",\n                            \"description\": \"If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\\nvirtualTimeBudgetExpired event is sent.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"maxVirtualTimeTaskStarvationCount\",\n                            \"description\": \"If set this specifies the maximum number of tasks that can be run before virtual is forced\\nforwards to prevent deadlock.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"initialVirtualTime\",\n                            \"description\": \"If set, base::Time::Now will be overridden to initially return this value.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"virtualTimeTicksBase\",\n                            \"description\": \"Absolute timestamp at which virtual time was first enabled (up time in milliseconds).\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setLocaleOverride\",\n                    \"description\": \"Overrides default host system locale with the specified one.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"locale\",\n                            \"description\": \"ICU style C locale (e.g. \\\"en_US\\\"). If not specified or empty, disables the override and\\nrestores default host system locale.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setTimezoneOverride\",\n                    \"description\": \"Overrides default host system timezone with the specified one.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"timezoneId\",\n                            \"description\": \"The timezone identifier. List of supported timezones:\\nhttps://source.chromium.org/chromium/chromium/deps/icu.git/+/faee8bc70570192d82d2978a71e2a615788597d1:source/data/misc/metaZones.txt\\nIf empty, disables the override and restores default host system timezone.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setVisibleSize\",\n                    \"description\": \"Resizes the frame/viewport of the page. Note that this does not affect the frame's container\\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\\non Android.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Frame width (DIP).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Frame height (DIP).\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDisabledImageTypes\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"imageTypes\",\n                            \"description\": \"Image types to disable.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DisabledImageType\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDataSaverOverride\",\n                    \"description\": \"Override the value of navigator.connection.saveData\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"dataSaverEnabled\",\n                            \"description\": \"Override value. Omitting the parameter disables the override.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setHardwareConcurrencyOverride\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"hardwareConcurrency\",\n                            \"description\": \"Hardware concurrency to report\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setUserAgentOverride\",\n                    \"description\": \"Allows overriding user agent with the given string.\\n`userAgentMetadata` must be set for Client Hint headers to be sent.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"userAgent\",\n                            \"description\": \"User agent to use.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"acceptLanguage\",\n                            \"description\": \"Browser language to emulate.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"platform\",\n                            \"description\": \"The platform navigator.platform should return.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"userAgentMetadata\",\n                            \"description\": \"To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"UserAgentMetadata\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAutomationOverride\",\n                    \"description\": \"Allows overriding the automation flag.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether the override should be enabled.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSmallViewportHeightDifferenceOverride\",\n                    \"description\": \"Allows overriding the difference between the small and large viewport sizes, which determine the\\nvalue of the `svh` and `lvh` unit, respectively. Only supported for top-level frames.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"difference\",\n                            \"description\": \"This will cause an element of size 100svh to be `difference` pixels smaller than an element\\nof size 100lvh.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getScreenInfos\",\n                    \"description\": \"Returns device's screen configuration. In headful mode, the physical screens configuration is returned,\\nwhereas in headless mode, a virtual headless screen configuration is provided instead.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"screenInfos\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScreenInfo\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addScreen\",\n                    \"description\": \"Add a new screen to the device. Only supported in headless mode.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"left\",\n                            \"description\": \"Offset of the left edge of the screen in pixels.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"top\",\n                            \"description\": \"Offset of the top edge of the screen in pixels.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"The width of the screen in pixels.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"The height of the screen in pixels.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"workAreaInsets\",\n                            \"description\": \"Specifies the screen's work area. Default is entire screen.\",\n                            \"optional\": true,\n                            \"$ref\": \"WorkAreaInsets\"\n                        },\n                        {\n                            \"name\": \"devicePixelRatio\",\n                            \"description\": \"Specifies the screen's device pixel ratio. Default is 1.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"rotation\",\n                            \"description\": \"Specifies the screen's rotation angle. Available values are 0, 90, 180 and 270. Default is 0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"colorDepth\",\n                            \"description\": \"Specifies the screen's color depth in bits. Default is 24.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"label\",\n                            \"description\": \"Specifies the descriptive label for the screen. Default is none.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isInternal\",\n                            \"description\": \"Indicates whether the screen is internal to the device or external, attached to the device. Default is false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"screenInfo\",\n                            \"$ref\": \"ScreenInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeScreen\",\n                    \"description\": \"Remove screen from the device. Only supported in headless mode.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"screenId\",\n                            \"$ref\": \"ScreenId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPrimaryScreen\",\n                    \"description\": \"Set primary screen. Only supported in headless mode.\\nNote that this changes the coordinate system origin to the top-left\\nof the new primary screen, updating the bounds and work areas\\nof all existing screens accordingly.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"screenId\",\n                            \"$ref\": \"ScreenId\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"virtualTimeBudgetExpired\",\n                    \"description\": \"Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.\",\n                    \"experimental\": true\n                }\n            ]\n        },\n        {\n            \"domain\": \"EventBreakpoints\",\n            \"description\": \"EventBreakpoints permits setting JavaScript breakpoints on operations and events\\noccurring in native code invoked from JavaScript. Once breakpoint is hit, it is\\nreported through Debugger domain, similarly to regular breakpoints being hit.\",\n            \"experimental\": true,\n            \"commands\": [\n                {\n                    \"name\": \"setInstrumentationBreakpoint\",\n                    \"description\": \"Sets breakpoint on particular native event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"Instrumentation name to stop on.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeInstrumentationBreakpoint\",\n                    \"description\": \"Removes breakpoint on particular native event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"Instrumentation name to stop on.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Removes all breakpoints\"\n                }\n            ]\n        },\n        {\n            \"domain\": \"Extensions\",\n            \"description\": \"Defines commands and events for browser extensions.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"StorageArea\",\n                    \"description\": \"Storage areas.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"session\",\n                        \"local\",\n                        \"sync\",\n                        \"managed\"\n                    ]\n                },\n                {\n                    \"id\": \"ExtensionInfo\",\n                    \"description\": \"Detailed information about an extension.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Extension id.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Extension name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"version\",\n                            \"description\": \"Extension version.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"The path from which the extension was loaded.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Extension enabled status.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"triggerAction\",\n                    \"description\": \"Runs an extension default action.\\nAvailable if the client is connected using the --remote-debugging-pipe\\nflag and the --enable-unsafe-extension-debugging flag is set.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Extension id.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"A tab target ID to trigger the default extension action on.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"loadUnpacked\",\n                    \"description\": \"Installs an unpacked extension from the filesystem similar to\\n--load-extension CLI flags. Returns extension ID once the extension\\nhas been installed. Available if the client is connected using the\\n--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging\\nflag is set.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"Absolute file path.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"enableInIncognito\",\n                            \"description\": \"Enable the extension in incognito\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Extension id.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getExtensions\",\n                    \"description\": \"Gets a list of all unpacked extensions.\\nAvailable if the client is connected using the --remote-debugging-pipe flag\\nand the --enable-unsafe-extension-debugging flag is set.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"extensions\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ExtensionInfo\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"uninstall\",\n                    \"description\": \"Uninstalls an unpacked extension (others not supported) from the profile.\\nAvailable if the client is connected using the --remote-debugging-pipe flag\\nand the --enable-unsafe-extension-debugging.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Extension id.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getStorageItems\",\n                    \"description\": \"Gets data from extension storage in the given `storageArea`. If `keys` is\\nspecified, these are used to filter the result.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"ID of extension.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageArea\",\n                            \"description\": \"StorageArea to retrieve data from.\",\n                            \"$ref\": \"StorageArea\"\n                        },\n                        {\n                            \"name\": \"keys\",\n                            \"description\": \"Keys to retrieve.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeStorageItems\",\n                    \"description\": \"Removes `keys` from extension storage in the given `storageArea`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"ID of extension.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageArea\",\n                            \"description\": \"StorageArea to remove data from.\",\n                            \"$ref\": \"StorageArea\"\n                        },\n                        {\n                            \"name\": \"keys\",\n                            \"description\": \"Keys to remove.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearStorageItems\",\n                    \"description\": \"Clears extension storage in the given `storageArea`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"ID of extension.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageArea\",\n                            \"description\": \"StorageArea to remove data from.\",\n                            \"$ref\": \"StorageArea\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setStorageItems\",\n                    \"description\": \"Sets `values` in extension storage in the given `storageArea`. The provided `values`\\nwill be merged with existing values in the storage area.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"ID of extension.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageArea\",\n                            \"description\": \"StorageArea to set data in.\",\n                            \"$ref\": \"StorageArea\"\n                        },\n                        {\n                            \"name\": \"values\",\n                            \"description\": \"Values to set.\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"FedCm\",\n            \"description\": \"This domain allows interacting with the FedCM dialog.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"LoginState\",\n                    \"description\": \"Whether this is a sign-up or sign-in action for this account, i.e.\\nwhether this account has ever been used to sign in to this RP before.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SignIn\",\n                        \"SignUp\"\n                    ]\n                },\n                {\n                    \"id\": \"DialogType\",\n                    \"description\": \"The types of FedCM dialogs.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"AccountChooser\",\n                        \"AutoReauthn\",\n                        \"ConfirmIdpLogin\",\n                        \"Error\"\n                    ]\n                },\n                {\n                    \"id\": \"DialogButton\",\n                    \"description\": \"The buttons on the FedCM dialog.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ConfirmIdpLoginContinue\",\n                        \"ErrorGotIt\",\n                        \"ErrorMoreDetails\"\n                    ]\n                },\n                {\n                    \"id\": \"AccountUrlType\",\n                    \"description\": \"The URLs that each account has\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"TermsOfService\",\n                        \"PrivacyPolicy\"\n                    ]\n                },\n                {\n                    \"id\": \"Account\",\n                    \"description\": \"Corresponds to IdentityRequestAccount\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"accountId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"email\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"givenName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"pictureUrl\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"idpConfigUrl\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"idpLoginUrl\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"loginState\",\n                            \"$ref\": \"LoginState\"\n                        },\n                        {\n                            \"name\": \"termsOfServiceUrl\",\n                            \"description\": \"These two are only set if the loginState is signUp\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"privacyPolicyUrl\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"dialogShown\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dialogId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"dialogType\",\n                            \"$ref\": \"DialogType\"\n                        },\n                        {\n                            \"name\": \"accounts\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Account\"\n                            }\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"These exist primarily so that the caller can verify the\\nRP context was used appropriately.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"subtitle\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dialogClosed\",\n                    \"description\": \"Triggered when a dialog is closed, either by user action, JS abort,\\nor a command below.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dialogId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"disableRejectionDelay\",\n                            \"description\": \"Allows callers to disable the promise rejection delay that would\\nnormally happen, if this is unimportant to what's being tested.\\n(step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in)\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\"\n                },\n                {\n                    \"name\": \"selectAccount\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dialogId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"accountIndex\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clickDialogButton\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dialogId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"dialogButton\",\n                            \"$ref\": \"DialogButton\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"openUrl\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dialogId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"accountIndex\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"accountUrlType\",\n                            \"$ref\": \"AccountUrlType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dismissDialog\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dialogId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"triggerCooldown\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resetCooldown\",\n                    \"description\": \"Resets the cooldown time, if any, to allow the next FedCM call to show\\na dialog even if one was recently dismissed by the user.\"\n                }\n            ]\n        },\n        {\n            \"domain\": \"Fetch\",\n            \"description\": \"A domain for letting clients substitute browser's network layer with client code.\",\n            \"dependencies\": [\n                \"Network\",\n                \"IO\",\n                \"Page\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"RequestId\",\n                    \"description\": \"Unique request identifier.\\nNote that this does not identify individual HTTP requests that are part of\\na network request.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"RequestStage\",\n                    \"description\": \"Stages of the request to handle. Request will intercept before the request is\\nsent. Response will intercept after the response is received (but before response\\nbody is received).\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Request\",\n                        \"Response\"\n                    ]\n                },\n                {\n                    \"id\": \"RequestPattern\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"urlPattern\",\n                            \"description\": \"Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to `\\\"*\\\"`.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"resourceType\",\n                            \"description\": \"If set, only requests for matching resource types will be intercepted.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.ResourceType\"\n                        },\n                        {\n                            \"name\": \"requestStage\",\n                            \"description\": \"Stage at which to begin intercepting requests. Default is Request.\",\n                            \"optional\": true,\n                            \"$ref\": \"RequestStage\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"HeaderEntry\",\n                    \"description\": \"Response HTTP header entry\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AuthChallenge\",\n                    \"description\": \"Authorization challenge for HTTP status code 401 or 407.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"source\",\n                            \"description\": \"Source of the authentication challenge.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Server\",\n                                \"Proxy\"\n                            ]\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin of the challenger.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scheme\",\n                            \"description\": \"The authentication scheme used, such as basic or digest\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"realm\",\n                            \"description\": \"The realm of the challenge. May be empty.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AuthChallengeResponse\",\n                    \"description\": \"Response to an AuthChallenge.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"The decision on what to do in response to the authorization challenge.  Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Default\",\n                                \"CancelAuth\",\n                                \"ProvideCredentials\"\n                            ]\n                        },\n                        {\n                            \"name\": \"username\",\n                            \"description\": \"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"password\",\n                            \"description\": \"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables the fetch domain.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables issuing of requestPaused events. A request will be paused until client\\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"patterns\",\n                            \"description\": \"If specified, only requests matching any of these patterns will produce\\nfetchRequested event and will be paused until clients response. If not set,\\nall requests will be affected.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RequestPattern\"\n                            }\n                        },\n                        {\n                            \"name\": \"handleAuthRequests\",\n                            \"description\": \"If true, authRequired events will be issued and requests will be paused\\nexpecting a call to continueWithAuth.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"failRequest\",\n                    \"description\": \"Causes the request to fail with specified reason.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"An id the client received in requestPaused event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"errorReason\",\n                            \"description\": \"Causes the request to fail with the given reason.\",\n                            \"$ref\": \"Network.ErrorReason\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"fulfillRequest\",\n                    \"description\": \"Provides response to the request.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"An id the client received in requestPaused event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"responseCode\",\n                            \"description\": \"An HTTP response code.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responseHeaders\",\n                            \"description\": \"Response headers.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"HeaderEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"binaryResponseHeaders\",\n                            \"description\": \"Alternative way of specifying response headers as a \\\\0-separated\\nseries of name: value pairs. Prefer the above method unless you\\nneed to represent some non-UTF8 values that can't be transmitted\\nover the protocol as text. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"body\",\n                            \"description\": \"A response body. If absent, original response body will be used if\\nthe request is intercepted at the response stage and empty body\\nwill be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"responsePhrase\",\n                            \"description\": \"A textual representation of responseCode.\\nIf absent, a standard phrase matching responseCode is used.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"continueRequest\",\n                    \"description\": \"Continues the request, optionally modifying some of its parameters.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"An id the client received in requestPaused event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"If set, the request url will be modified in a way that's not observable by page.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"method\",\n                            \"description\": \"If set, the request method is overridden.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"postData\",\n                            \"description\": \"If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"If set, overrides the request headers. Note that the overrides do not\\nextend to subsequent redirect hops, if a redirect happens. Another override\\nmay be applied to a different request produced by a redirect.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"HeaderEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"interceptResponse\",\n                            \"description\": \"If set, overrides response interception behavior for this request.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"continueWithAuth\",\n                    \"description\": \"Continues a request supplying authChallengeResponse following authRequired event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"An id the client received in authRequired event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"authChallengeResponse\",\n                            \"description\": \"Response to  with an authChallenge.\",\n                            \"$ref\": \"AuthChallengeResponse\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"continueResponse\",\n                    \"description\": \"Continues loading of the paused response, optionally modifying the\\nresponse headers. If either responseCode or headers are modified, all of them\\nmust be present.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"An id the client received in requestPaused event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"responseCode\",\n                            \"description\": \"An HTTP response code. If absent, original response code will be used.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responsePhrase\",\n                            \"description\": \"A textual representation of responseCode.\\nIf absent, a standard phrase matching responseCode is used.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"responseHeaders\",\n                            \"description\": \"Response headers. If absent, original response headers will be used.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"HeaderEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"binaryResponseHeaders\",\n                            \"description\": \"Alternative way of specifying response headers as a \\\\0-separated\\nseries of name: value pairs. Prefer the above method unless you\\nneed to represent some non-UTF8 values that can't be transmitted\\nover the protocol as text. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getResponseBody\",\n                    \"description\": \"Causes the body of the response to be received from the server and\\nreturned as a single string. May only be issued for a request that\\nis paused in the Response stage and is mutually exclusive with\\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\\naffect the request or disabling fetch domain before body is received\\nresults in an undefined behavior.\\nNote that the response body is not available for redirects. Requests\\npaused in the _redirect received_ state may be differentiated by\\n`responseCode` and presence of `location` response header, see\\ncomments to `requestPaused` for details.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier for the intercepted request to get body for.\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"body\",\n                            \"description\": \"Response body.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"base64Encoded\",\n                            \"description\": \"True, if content was sent as base64.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"takeResponseBodyAsStream\",\n                    \"description\": \"Returns a handle to the stream representing the response body.\\nThe request must be paused in the HeadersReceived stage.\\nNote that after this command the request can't be continued\\nas is -- client either needs to cancel it or to provide the\\nresponse body.\\nThe stream only supports sequential read, IO.read will fail if the position\\nis specified.\\nThis method is mutually exclusive with getResponseBody.\\nCalling other methods that affect the request or disabling fetch\\ndomain before body is received results in an undefined behavior.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"stream\",\n                            \"$ref\": \"IO.StreamHandle\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"requestPaused\",\n                    \"description\": \"Issued when the domain is enabled and the request URL matches the\\nspecified filter. The request is paused until the client responds\\nwith one of continueRequest, failRequest or fulfillRequest.\\nThe stage of the request can be determined by presence of responseErrorReason\\nand responseStatusCode -- the request is at the response stage if either\\nof these fields is present and in the request stage otherwise.\\nRedirect responses and subsequent requests are reported similarly to regular\\nresponses and requests. Redirect responses may be distinguished by the value\\nof `responseStatusCode` (which is one of 301, 302, 303, 307, 308) along with\\npresence of the `location` header. Requests resulting from a redirect will\\nhave `redirectedRequestId` field set.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Each request the page makes will have a unique id.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"The details of the request.\",\n                            \"$ref\": \"Network.Request\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The id of the frame that initiated the request.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"resourceType\",\n                            \"description\": \"How the requested resource will be used.\",\n                            \"$ref\": \"Network.ResourceType\"\n                        },\n                        {\n                            \"name\": \"responseErrorReason\",\n                            \"description\": \"Response error if intercepted at response stage.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.ErrorReason\"\n                        },\n                        {\n                            \"name\": \"responseStatusCode\",\n                            \"description\": \"Response code if intercepted at response stage.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responseStatusText\",\n                            \"description\": \"Response status text if intercepted at response stage.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"responseHeaders\",\n                            \"description\": \"Response headers if intercepted at the response stage.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"HeaderEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"networkId\",\n                            \"description\": \"If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.RequestId\"\n                        },\n                        {\n                            \"name\": \"redirectedRequestId\",\n                            \"description\": \"If the request is due to a redirect response from the server, the id of the request that\\nhas caused the redirect.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"authRequired\",\n                    \"description\": \"Issued when the domain is enabled with handleAuthRequests set to true.\\nThe request is paused until client responds with continueWithAuth.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Each request the page makes will have a unique id.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"The details of the request.\",\n                            \"$ref\": \"Network.Request\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The id of the frame that initiated the request.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"resourceType\",\n                            \"description\": \"How the requested resource will be used.\",\n                            \"$ref\": \"Network.ResourceType\"\n                        },\n                        {\n                            \"name\": \"authChallenge\",\n                            \"description\": \"Details of the Authorization Challenge encountered.\\nIf this is set, client should respond with continueRequest that\\ncontains AuthChallengeResponse.\",\n                            \"$ref\": \"AuthChallenge\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"FileSystem\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Network\",\n                \"Storage\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"File\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lastModified\",\n                            \"description\": \"Timestamp\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"Size in bytes\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Directory\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"nestedDirectories\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"nestedFiles\",\n                            \"description\": \"Files that are directly nested under this directory.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"File\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BucketFileSystemLocator\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key\",\n                            \"$ref\": \"Storage.SerializedStorageKey\"\n                        },\n                        {\n                            \"name\": \"bucketName\",\n                            \"description\": \"Bucket name. Not passing a `bucketName` will retrieve the default Bucket. (https://developer.mozilla.org/en-US/docs/Web/API/Storage_API#storage_buckets)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"pathComponents\",\n                            \"description\": \"Path to the directory using each path component as an array item.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getDirectory\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"bucketFileSystemLocator\",\n                            \"$ref\": \"BucketFileSystemLocator\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"directory\",\n                            \"description\": \"Returns the directory object at the path.\",\n                            \"$ref\": \"Directory\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"HeadlessExperimental\",\n            \"description\": \"This domain provides experimental commands only supported in headless mode.\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Page\",\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"ScreenshotParams\",\n                    \"description\": \"Encoding options for a screenshot.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"format\",\n                            \"description\": \"Image compression format (defaults to png).\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"jpeg\",\n                                \"png\",\n                                \"webp\"\n                            ]\n                        },\n                        {\n                            \"name\": \"quality\",\n                            \"description\": \"Compression quality from range [0..100] (jpeg and webp only).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"optimizeForSpeed\",\n                            \"description\": \"Optimize image encoding for speed, not for resulting size (defaults to false)\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"beginFrame\",\n                    \"description\": \"Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\\nscreenshot from the resulting frame. Requires that the target was created with enabled\\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\\nhttps://goo.gle/chrome-headless-rendering for more background.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameTimeTicks\",\n                            \"description\": \"Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\\nthe current time will be used.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"interval\",\n                            \"description\": \"The interval between BeginFrames that is reported to the compositor, in milliseconds.\\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"noDisplayUpdates\",\n                            \"description\": \"Whether updates should not be committed and drawn onto the display. False by default. If\\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\\nany visual updates may not be visible on the display or in screenshots.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"screenshot\",\n                            \"description\": \"If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\\nduring renderer initialization. In such a case, no screenshot data will be returned.\",\n                            \"optional\": true,\n                            \"$ref\": \"ScreenshotParams\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"hasDamage\",\n                            \"description\": \"Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\\ndisplay. Reported for diagnostic uses, may be removed in the future.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"screenshotData\",\n                            \"description\": \"Base64-encoded image data of the screenshot, if one was requested and successfully taken. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables headless events for the target.\",\n                    \"deprecated\": true\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables headless events for the target.\",\n                    \"deprecated\": true\n                }\n            ]\n        },\n        {\n            \"domain\": \"IO\",\n            \"description\": \"Input/Output operations for streams produced by DevTools.\",\n            \"types\": [\n                {\n                    \"id\": \"StreamHandle\",\n                    \"description\": \"This is either obtained from another method or specified as `blob:<uuid>` where\\n`<uuid>` is an UUID of a Blob.\",\n                    \"type\": \"string\"\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"close\",\n                    \"description\": \"Close the stream, discard any temporary backing storage.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"handle\",\n                            \"description\": \"Handle of the stream to close.\",\n                            \"$ref\": \"StreamHandle\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"read\",\n                    \"description\": \"Read a chunk of the stream\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"handle\",\n                            \"description\": \"Handle of the stream to read.\",\n                            \"$ref\": \"StreamHandle\"\n                        },\n                        {\n                            \"name\": \"offset\",\n                            \"description\": \"Seek to the specified offset before reading (if not specified, proceed with offset\\nfollowing the last read). Some types of streams may only support sequential reads.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"Maximum number of bytes to read (left upon the agent discretion if not specified).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"base64Encoded\",\n                            \"description\": \"Set if the data is base64-encoded\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Data that were read.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"eof\",\n                            \"description\": \"Set if the end-of-file condition occurred while reading.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resolveBlob\",\n                    \"description\": \"Return UUID of Blob object specified by a remote object id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Object id of a Blob object wrapper.\",\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"uuid\",\n                            \"description\": \"UUID of the specified Blob.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"IndexedDB\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Runtime\",\n                \"Storage\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"DatabaseWithObjectStores\",\n                    \"description\": \"Database with an array of object stores.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Database name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"version\",\n                            \"description\": \"Database version (type is not 'integer', as the standard\\nrequires the version number to be 'unsigned long long')\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"objectStores\",\n                            \"description\": \"Object stores in this database.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ObjectStore\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ObjectStore\",\n                    \"description\": \"Object store.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Object store name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyPath\",\n                            \"description\": \"Object store key path.\",\n                            \"$ref\": \"KeyPath\"\n                        },\n                        {\n                            \"name\": \"autoIncrement\",\n                            \"description\": \"If true, object store has auto increment flag set.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"indexes\",\n                            \"description\": \"Indexes in this object store.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ObjectStoreIndex\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ObjectStoreIndex\",\n                    \"description\": \"Object store index.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Index name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyPath\",\n                            \"description\": \"Index key path.\",\n                            \"$ref\": \"KeyPath\"\n                        },\n                        {\n                            \"name\": \"unique\",\n                            \"description\": \"If true, index is unique.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"multiEntry\",\n                            \"description\": \"If true, index allows multiple entries for a key.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Key\",\n                    \"description\": \"Key.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Key type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"number\",\n                                \"string\",\n                                \"date\",\n                                \"array\"\n                            ]\n                        },\n                        {\n                            \"name\": \"number\",\n                            \"description\": \"Number value.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"string\",\n                            \"description\": \"String value.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"date\",\n                            \"description\": \"Date value.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"array\",\n                            \"description\": \"Array value.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Key\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"KeyRange\",\n                    \"description\": \"Key range.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"lower\",\n                            \"description\": \"Lower bound.\",\n                            \"optional\": true,\n                            \"$ref\": \"Key\"\n                        },\n                        {\n                            \"name\": \"upper\",\n                            \"description\": \"Upper bound.\",\n                            \"optional\": true,\n                            \"$ref\": \"Key\"\n                        },\n                        {\n                            \"name\": \"lowerOpen\",\n                            \"description\": \"If true lower bound is open.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"upperOpen\",\n                            \"description\": \"If true upper bound is open.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DataEntry\",\n                    \"description\": \"Data entry.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"description\": \"Key object.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"primaryKey\",\n                            \"description\": \"Primary key object.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Value object.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"KeyPath\",\n                    \"description\": \"Key path.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Key path type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"null\",\n                                \"string\",\n                                \"array\"\n                            ]\n                        },\n                        {\n                            \"name\": \"string\",\n                            \"description\": \"String value.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"array\",\n                            \"description\": \"Array value.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"clearObjectStore\",\n                    \"description\": \"Clears all entries from an object store.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"description\": \"Database name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectStoreName\",\n                            \"description\": \"Object store name.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteDatabase\",\n                    \"description\": \"Deletes a database.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"description\": \"Database name.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteObjectStoreEntries\",\n                    \"description\": \"Delete a range of entries from an object store\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectStoreName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyRange\",\n                            \"description\": \"Range of entry keys to delete\",\n                            \"$ref\": \"KeyRange\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables events from backend.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables events from backend.\"\n                },\n                {\n                    \"name\": \"requestData\",\n                    \"description\": \"Requests data from object store or index.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"description\": \"Database name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectStoreName\",\n                            \"description\": \"Object store name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"indexName\",\n                            \"description\": \"Index name. If not specified, it performs an object store data request.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"skipCount\",\n                            \"description\": \"Number of records to skip.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pageSize\",\n                            \"description\": \"Number of records to fetch.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"keyRange\",\n                            \"description\": \"Key range.\",\n                            \"optional\": true,\n                            \"$ref\": \"KeyRange\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"objectStoreDataEntries\",\n                            \"description\": \"Array of object store data entries.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DataEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"hasMore\",\n                            \"description\": \"If true, there are more entries to fetch in the given range.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getMetadata\",\n                    \"description\": \"Gets metadata of an object store.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"description\": \"Database name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectStoreName\",\n                            \"description\": \"Object store name.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"entriesCount\",\n                            \"description\": \"the entries count\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"keyGeneratorValue\",\n                            \"description\": \"the current value of key generator, to become the next inserted\\nkey into the object store. Valid if objectStore.autoIncrement\\nis true.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestDatabase\",\n                    \"description\": \"Requests database with given name in given frame.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"description\": \"Database name.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"databaseWithObjectStores\",\n                            \"description\": \"Database with an array of object stores.\",\n                            \"$ref\": \"DatabaseWithObjectStores\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestDatabaseNames\",\n                    \"description\": \"Requests database names for given security origin.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageBucket\",\n                            \"description\": \"Storage bucket. If not specified, it uses the default bucket.\",\n                            \"optional\": true,\n                            \"$ref\": \"Storage.StorageBucket\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"databaseNames\",\n                            \"description\": \"Database names for origin.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Input\",\n            \"types\": [\n                {\n                    \"id\": \"TouchPoint\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the event relative to the main frame's viewport in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"radiusX\",\n                            \"description\": \"X radius of the touch area (default: 1.0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"radiusY\",\n                            \"description\": \"Y radius of the touch area (default: 1.0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"rotationAngle\",\n                            \"description\": \"Rotation angle (default: 0.0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"force\",\n                            \"description\": \"Force (default: 1.0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"tangentialPressure\",\n                            \"description\": \"The normalized tangential pressure, which has a range of [-1,1] (default: 0).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"tiltX\",\n                            \"description\": \"The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0)\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"tiltY\",\n                            \"description\": \"The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"twist\",\n                            \"description\": \"The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Identifier used to track touch sources between events, must be unique within an event.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"GestureSourceType\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"default\",\n                        \"touch\",\n                        \"mouse\"\n                    ]\n                },\n                {\n                    \"id\": \"MouseButton\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"none\",\n                        \"left\",\n                        \"middle\",\n                        \"right\",\n                        \"back\",\n                        \"forward\"\n                    ]\n                },\n                {\n                    \"id\": \"TimeSinceEpoch\",\n                    \"description\": \"UTC time in seconds, counted from January 1, 1970.\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"id\": \"DragDataItem\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"mimeType\",\n                            \"description\": \"Mime type of the dragged data.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Depending of the value of `mimeType`, it contains the dragged link,\\ntext, HTML markup or any other data.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Title associated with a link. Only valid when `mimeType` == \\\"text/uri-list\\\".\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"baseURL\",\n                            \"description\": \"Stores the base URL for the contained markup. Only valid when `mimeType`\\n== \\\"text/html\\\".\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DragData\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"items\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DragDataItem\"\n                            }\n                        },\n                        {\n                            \"name\": \"files\",\n                            \"description\": \"List of filenames that should be included when dropping\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"dragOperationsMask\",\n                            \"description\": \"Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"dispatchDragEvent\",\n                    \"description\": \"Dispatches a drag event into the page.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the drag event.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"dragEnter\",\n                                \"dragOver\",\n                                \"drop\",\n                                \"dragCancel\"\n                            ]\n                        },\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the event relative to the main frame's viewport in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"$ref\": \"DragData\"\n                        },\n                        {\n                            \"name\": \"modifiers\",\n                            \"description\": \"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dispatchKeyEvent\",\n                    \"description\": \"Dispatches a key event to the page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the key event.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"keyDown\",\n                                \"keyUp\",\n                                \"rawKeyDown\",\n                                \"char\"\n                            ]\n                        },\n                        {\n                            \"name\": \"modifiers\",\n                            \"description\": \"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Time at which the event occurred.\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Text as generated by processing a virtual key code with a keyboard layout. Not needed for\\nfor `keyUp` and `rawKeyDown` events (default: \\\"\\\")\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"unmodifiedText\",\n                            \"description\": \"Text that would have been generated by the keyboard if no modifiers were pressed (except for\\nshift). Useful for shortcut (accelerator) key handling (default: \\\"\\\").\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyIdentifier\",\n                            \"description\": \"Unique key identifier (e.g., 'U+0041') (default: \\\"\\\").\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"code\",\n                            \"description\": \"Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \\\"\\\").\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"description\": \"Unique DOM defined string value describing the meaning of the key in the context of active\\nmodifiers, keyboard layout, etc (e.g., 'AltGr') (default: \\\"\\\").\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"windowsVirtualKeyCode\",\n                            \"description\": \"Windows virtual key code (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"nativeVirtualKeyCode\",\n                            \"description\": \"Native virtual key code (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"autoRepeat\",\n                            \"description\": \"Whether the event was generated from auto repeat (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isKeypad\",\n                            \"description\": \"Whether the event was generated from the keypad (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isSystemKey\",\n                            \"description\": \"Whether the event was a system key event (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\\n0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"commands\",\n                            \"description\": \"Editing commands to send with the key event (e.g., 'selectAll') (default: []).\\nThese are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding.\\nSee https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"insertText\",\n                    \"description\": \"This method emulates inserting text that doesn't come from a key press,\\nfor example an emoji keyboard or an IME.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"The text to insert.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"imeSetComposition\",\n                    \"description\": \"This method sets the current candidate text for IME.\\nUse imeCommitComposition to commit the final text.\\nUse imeSetComposition with empty string as text to cancel composition.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"The text to insert\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"selectionStart\",\n                            \"description\": \"selection start\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"selectionEnd\",\n                            \"description\": \"selection end\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"replacementStart\",\n                            \"description\": \"replacement start\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"replacementEnd\",\n                            \"description\": \"replacement end\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dispatchMouseEvent\",\n                    \"description\": \"Dispatches a mouse event to the page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the mouse event.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mousePressed\",\n                                \"mouseReleased\",\n                                \"mouseMoved\",\n                                \"mouseWheel\"\n                            ]\n                        },\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the event relative to the main frame's viewport in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"modifiers\",\n                            \"description\": \"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Time at which the event occurred.\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"button\",\n                            \"description\": \"Mouse button (default: \\\"none\\\").\",\n                            \"optional\": true,\n                            \"$ref\": \"MouseButton\"\n                        },\n                        {\n                            \"name\": \"buttons\",\n                            \"description\": \"A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"clickCount\",\n                            \"description\": \"Number of times the mouse button was clicked (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"force\",\n                            \"description\": \"The normalized pressure, which has a range of [0,1] (default: 0).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"tangentialPressure\",\n                            \"description\": \"The normalized tangential pressure, which has a range of [-1,1] (default: 0).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"tiltX\",\n                            \"description\": \"The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"tiltY\",\n                            \"description\": \"The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"twist\",\n                            \"description\": \"The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"deltaX\",\n                            \"description\": \"X delta in CSS pixels for mouse wheel event (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"deltaY\",\n                            \"description\": \"Y delta in CSS pixels for mouse wheel event (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pointerType\",\n                            \"description\": \"Pointer type (default: \\\"mouse\\\").\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mouse\",\n                                \"pen\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dispatchTouchEvent\",\n                    \"description\": \"Dispatches a touch event to the page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\\nTouchStart and TouchMove must contains at least one.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"touchStart\",\n                                \"touchEnd\",\n                                \"touchMove\",\n                                \"touchCancel\"\n                            ]\n                        },\n                        {\n                            \"name\": \"touchPoints\",\n                            \"description\": \"Active touch points on the touch device. One event per any changed point (compared to\\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\\none by one.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"TouchPoint\"\n                            }\n                        },\n                        {\n                            \"name\": \"modifiers\",\n                            \"description\": \"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Time at which the event occurred.\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"cancelDragging\",\n                    \"description\": \"Cancels any active dragging in the page.\"\n                },\n                {\n                    \"name\": \"emulateTouchFromMouseEvent\",\n                    \"description\": \"Emulates touch event from the mouse event parameters.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the mouse event.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mousePressed\",\n                                \"mouseReleased\",\n                                \"mouseMoved\",\n                                \"mouseWheel\"\n                            ]\n                        },\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the mouse pointer in DIP.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the mouse pointer in DIP.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"button\",\n                            \"description\": \"Mouse button. Only \\\"none\\\", \\\"left\\\", \\\"right\\\" are supported.\",\n                            \"$ref\": \"MouseButton\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Time at which the event occurred (default: current time).\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"deltaX\",\n                            \"description\": \"X delta in DIP for mouse wheel event (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"deltaY\",\n                            \"description\": \"Y delta in DIP for mouse wheel event (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"modifiers\",\n                            \"description\": \"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"clickCount\",\n                            \"description\": \"Number of times the mouse button was clicked (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setIgnoreInputEvents\",\n                    \"description\": \"Ignores input events (useful while auditing page).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"ignore\",\n                            \"description\": \"Ignores input events processing when set to true.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInterceptDrags\",\n                    \"description\": \"Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events.\\nDrag and drop behavior can be directly controlled via `Input.dispatchDragEvent`.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"synthesizePinchGesture\",\n                    \"description\": \"Synthesizes a pinch gesture over a time period by issuing appropriate touch events.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the start of the gesture in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the start of the gesture in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scaleFactor\",\n                            \"description\": \"Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"relativeSpeed\",\n                            \"description\": \"Relative pointer speed in pixels per second (default: 800).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"gestureSourceType\",\n                            \"description\": \"Which type of input events to be generated (default: 'default', which queries the platform\\nfor the preferred input type).\",\n                            \"optional\": true,\n                            \"$ref\": \"GestureSourceType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"synthesizeScrollGesture\",\n                    \"description\": \"Synthesizes a scroll gesture over a time period by issuing appropriate touch events.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the start of the gesture in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the start of the gesture in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"xDistance\",\n                            \"description\": \"The distance to scroll along the X axis (positive to scroll left).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"yDistance\",\n                            \"description\": \"The distance to scroll along the Y axis (positive to scroll up).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"xOverscroll\",\n                            \"description\": \"The number of additional pixels to scroll back along the X axis, in addition to the given\\ndistance.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"yOverscroll\",\n                            \"description\": \"The number of additional pixels to scroll back along the Y axis, in addition to the given\\ndistance.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"preventFling\",\n                            \"description\": \"Prevent fling (default: true).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"speed\",\n                            \"description\": \"Swipe speed in pixels per second (default: 800).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"gestureSourceType\",\n                            \"description\": \"Which type of input events to be generated (default: 'default', which queries the platform\\nfor the preferred input type).\",\n                            \"optional\": true,\n                            \"$ref\": \"GestureSourceType\"\n                        },\n                        {\n                            \"name\": \"repeatCount\",\n                            \"description\": \"The number of times to repeat the gesture (default: 0).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"repeatDelayMs\",\n                            \"description\": \"The number of milliseconds delay between each repeat. (default: 250).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"interactionMarkerName\",\n                            \"description\": \"The name of the interaction markers to generate, if not empty (default: \\\"\\\").\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"synthesizeTapGesture\",\n                    \"description\": \"Synthesizes a tap gesture over a time period by issuing appropriate touch events.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate of the start of the gesture in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate of the start of the gesture in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"duration\",\n                            \"description\": \"Duration between touchdown and touchup events in ms (default: 50).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"tapCount\",\n                            \"description\": \"Number of times to perform the tap (e.g. 2 for double tap, default: 1).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"gestureSourceType\",\n                            \"description\": \"Which type of input events to be generated (default: 'default', which queries the platform\\nfor the preferred input type).\",\n                            \"optional\": true,\n                            \"$ref\": \"GestureSourceType\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"dragIntercepted\",\n                    \"description\": \"Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to\\nrestore normal drag and drop behavior.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"data\",\n                            \"$ref\": \"DragData\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Inspector\",\n            \"experimental\": true,\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables inspector domain notifications.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables inspector domain notifications.\"\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"detached\",\n                    \"description\": \"Fired when remote debugging connection is about to be terminated. Contains detach reason.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"The reason why connection has been terminated.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"targetCrashed\",\n                    \"description\": \"Fired when debugging target has crashed\"\n                },\n                {\n                    \"name\": \"targetReloadedAfterCrash\",\n                    \"description\": \"Fired when debugging target has reloaded after crash\"\n                },\n                {\n                    \"name\": \"workerScriptLoaded\",\n                    \"description\": \"Fired on worker targets when main worker script and any imported scripts have been evaluated.\",\n                    \"experimental\": true\n                }\n            ]\n        },\n        {\n            \"domain\": \"LayerTree\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"DOM\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"LayerId\",\n                    \"description\": \"Unique Layer identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"SnapshotId\",\n                    \"description\": \"Unique snapshot identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ScrollRect\",\n                    \"description\": \"Rectangle where scrolling happens on the main thread.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"rect\",\n                            \"description\": \"Rectangle itself.\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Reason for rectangle to force scrolling on the main thread\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"RepaintsOnScroll\",\n                                \"TouchEventHandler\",\n                                \"WheelEventHandler\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StickyPositionConstraint\",\n                    \"description\": \"Sticky position constraints.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"stickyBoxRect\",\n                            \"description\": \"Layout rectangle of the sticky element before being shifted\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"containingBlockRect\",\n                            \"description\": \"Layout rectangle of the containing block of the sticky element\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"nearestLayerShiftingStickyBox\",\n                            \"description\": \"The nearest sticky layer that shifts the sticky box\",\n                            \"optional\": true,\n                            \"$ref\": \"LayerId\"\n                        },\n                        {\n                            \"name\": \"nearestLayerShiftingContainingBlock\",\n                            \"description\": \"The nearest sticky layer that shifts the containing block\",\n                            \"optional\": true,\n                            \"$ref\": \"LayerId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PictureTile\",\n                    \"description\": \"Serialized fragment of layer picture along with its offset within the layer.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"Offset from owning layer left boundary\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Offset from owning layer top boundary\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"picture\",\n                            \"description\": \"Base64-encoded snapshot data. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Layer\",\n                    \"description\": \"Information about a compositing layer.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"layerId\",\n                            \"description\": \"The unique id for this layer.\",\n                            \"$ref\": \"LayerId\"\n                        },\n                        {\n                            \"name\": \"parentLayerId\",\n                            \"description\": \"The id of parent (not present for root).\",\n                            \"optional\": true,\n                            \"$ref\": \"LayerId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"The backend id for the node associated with this layer.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"offsetX\",\n                            \"description\": \"Offset from parent layer, X coordinate.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"offsetY\",\n                            \"description\": \"Offset from parent layer, Y coordinate.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Layer width.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Layer height.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"transform\",\n                            \"description\": \"Transformation matrix for layer, default is identity matrix\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"number\"\n                            }\n                        },\n                        {\n                            \"name\": \"anchorX\",\n                            \"description\": \"Transform anchor point X, absent if no transform specified\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"anchorY\",\n                            \"description\": \"Transform anchor point Y, absent if no transform specified\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"anchorZ\",\n                            \"description\": \"Transform anchor point Z, absent if no transform specified\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"paintCount\",\n                            \"description\": \"Indicates how many time this layer has painted.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"drawsContent\",\n                            \"description\": \"Indicates whether this layer hosts any content, rather than being used for\\ntransform/scrolling purposes only.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"invisible\",\n                            \"description\": \"Set if layer is not visible.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"scrollRects\",\n                            \"description\": \"Rectangles scrolling on main thread only.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScrollRect\"\n                            }\n                        },\n                        {\n                            \"name\": \"stickyPositionConstraint\",\n                            \"description\": \"Sticky position constraint information\",\n                            \"optional\": true,\n                            \"$ref\": \"StickyPositionConstraint\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PaintProfile\",\n                    \"description\": \"Array of timings, one per paint step.\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"number\"\n                    }\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"compositingReasons\",\n                    \"description\": \"Provides the reasons why the given layer was composited.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"layerId\",\n                            \"description\": \"The id of the layer for which we want to get the reasons it was composited.\",\n                            \"$ref\": \"LayerId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"compositingReasons\",\n                            \"description\": \"A list of strings specifying reasons for the given layer to become composited.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"compositingReasonIds\",\n                            \"description\": \"A list of strings specifying reason IDs for the given layer to become composited.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables compositing tree inspection.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables compositing tree inspection.\"\n                },\n                {\n                    \"name\": \"loadSnapshot\",\n                    \"description\": \"Returns the snapshot identifier.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"tiles\",\n                            \"description\": \"An array of tiles composing the snapshot.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PictureTile\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"snapshotId\",\n                            \"description\": \"The id of the snapshot.\",\n                            \"$ref\": \"SnapshotId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"makeSnapshot\",\n                    \"description\": \"Returns the layer snapshot identifier.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"layerId\",\n                            \"description\": \"The id of the layer.\",\n                            \"$ref\": \"LayerId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"snapshotId\",\n                            \"description\": \"The id of the layer snapshot.\",\n                            \"$ref\": \"SnapshotId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"profileSnapshot\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"snapshotId\",\n                            \"description\": \"The id of the layer snapshot.\",\n                            \"$ref\": \"SnapshotId\"\n                        },\n                        {\n                            \"name\": \"minRepeatCount\",\n                            \"description\": \"The maximum number of times to replay the snapshot (1, if not specified).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"minDuration\",\n                            \"description\": \"The minimum duration (in seconds) to replay the snapshot.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"clipRect\",\n                            \"description\": \"The clip rectangle to apply when replaying the snapshot.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.Rect\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"timings\",\n                            \"description\": \"The array of paint profiles, one per run.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PaintProfile\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"releaseSnapshot\",\n                    \"description\": \"Releases layer snapshot captured by the back-end.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"snapshotId\",\n                            \"description\": \"The id of the layer snapshot.\",\n                            \"$ref\": \"SnapshotId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"replaySnapshot\",\n                    \"description\": \"Replays the layer snapshot and returns the resulting bitmap.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"snapshotId\",\n                            \"description\": \"The id of the layer snapshot.\",\n                            \"$ref\": \"SnapshotId\"\n                        },\n                        {\n                            \"name\": \"fromStep\",\n                            \"description\": \"The first step to replay from (replay from the very start if not specified).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"toStep\",\n                            \"description\": \"The last step to replay to (replay till the end if not specified).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"scale\",\n                            \"description\": \"The scale to apply while replaying (defaults to 1).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"dataURL\",\n                            \"description\": \"A data: URL for resulting image.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"snapshotCommandLog\",\n                    \"description\": \"Replays the layer snapshot and returns canvas log.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"snapshotId\",\n                            \"description\": \"The id of the layer snapshot.\",\n                            \"$ref\": \"SnapshotId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"commandLog\",\n                            \"description\": \"The array of canvas function calls.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"object\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"layerPainted\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"layerId\",\n                            \"description\": \"The id of the painted layer.\",\n                            \"$ref\": \"LayerId\"\n                        },\n                        {\n                            \"name\": \"clip\",\n                            \"description\": \"Clip rectangle.\",\n                            \"$ref\": \"DOM.Rect\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"layerTreeDidChange\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"layers\",\n                            \"description\": \"Layer tree, absent if not in the compositing mode.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Layer\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Log\",\n            \"description\": \"Provides access to log entries.\",\n            \"dependencies\": [\n                \"Runtime\",\n                \"Network\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"LogEntry\",\n                    \"description\": \"Log entry.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"source\",\n                            \"description\": \"Log entry source.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"xml\",\n                                \"javascript\",\n                                \"network\",\n                                \"storage\",\n                                \"appcache\",\n                                \"rendering\",\n                                \"security\",\n                                \"deprecation\",\n                                \"worker\",\n                                \"violation\",\n                                \"intervention\",\n                                \"recommendation\",\n                                \"other\"\n                            ]\n                        },\n                        {\n                            \"name\": \"level\",\n                            \"description\": \"Log entry severity.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"verbose\",\n                                \"info\",\n                                \"warning\",\n                                \"error\"\n                            ]\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Logged text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"category\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"cors\"\n                            ]\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp when this entry was added.\",\n                            \"$ref\": \"Runtime.Timestamp\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resource if known.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number in the resource.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"stackTrace\",\n                            \"description\": \"JavaScript stack trace.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"networkRequestId\",\n                            \"description\": \"Identifier of the network request associated with this entry.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.RequestId\"\n                        },\n                        {\n                            \"name\": \"workerId\",\n                            \"description\": \"Identifier of the worker associated with this entry.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"args\",\n                            \"description\": \"Call arguments.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Runtime.RemoteObject\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ViolationSetting\",\n                    \"description\": \"Violation configuration setting.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Violation type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"longTask\",\n                                \"longLayout\",\n                                \"blockedEvent\",\n                                \"blockedParser\",\n                                \"discouragedAPIUse\",\n                                \"handler\",\n                                \"recurringHandler\"\n                            ]\n                        },\n                        {\n                            \"name\": \"threshold\",\n                            \"description\": \"Time threshold to trigger upon.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"clear\",\n                    \"description\": \"Clears the log.\"\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables log domain, prevents further log entries from being reported to the client.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables log domain, sends the entries collected so far to the client by means of the\\n`entryAdded` notification.\"\n                },\n                {\n                    \"name\": \"startViolationsReport\",\n                    \"description\": \"start violation reporting.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"config\",\n                            \"description\": \"Configuration for violations.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ViolationSetting\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopViolationsReport\",\n                    \"description\": \"Stop violation reporting.\"\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"entryAdded\",\n                    \"description\": \"Issued when new message was logged.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"entry\",\n                            \"description\": \"The entry.\",\n                            \"$ref\": \"LogEntry\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Media\",\n            \"description\": \"This domain allows detailed inspection of media elements.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"PlayerId\",\n                    \"description\": \"Players will get an ID that is unique within the agent context.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"Timestamp\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"id\": \"PlayerMessage\",\n                    \"description\": \"Have one type per entry in MediaLogRecord::Type\\nCorresponds to kMessage\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"level\",\n                            \"description\": \"Keep in sync with MediaLogMessageLevel\\nWe are currently keeping the message level 'error' separate from the\\nPlayerError type because right now they represent different things,\\nthis one being a DVLOG(ERROR) style log message that gets printed\\nbased on what log level is selected in the UI, and the other is a\\nrepresentation of a media::PipelineStatus object. Soon however we're\\ngoing to be moving away from using PipelineStatus for errors and\\nintroducing a new error type which should hopefully let us integrate\\nthe error log level into the PlayerError type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"error\",\n                                \"warning\",\n                                \"info\",\n                                \"debug\"\n                            ]\n                        },\n                        {\n                            \"name\": \"message\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PlayerProperty\",\n                    \"description\": \"Corresponds to kMediaPropertyChange\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PlayerEvent\",\n                    \"description\": \"Corresponds to kMediaEventTriggered\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"Timestamp\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PlayerErrorSourceLocation\",\n                    \"description\": \"Represents logged source line numbers reported in an error.\\nNOTE: file and line are from chromium c++ implementation code, not js.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"file\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"line\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PlayerError\",\n                    \"description\": \"Corresponds to kMediaError\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"errorType\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"code\",\n                            \"description\": \"Code is the numeric enum entry for a specific set of error codes, such\\nas PipelineStatusCodes in media/base/pipeline_status.h\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"stack\",\n                            \"description\": \"A trace of where this error was caused / where it passed through.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlayerErrorSourceLocation\"\n                            }\n                        },\n                        {\n                            \"name\": \"cause\",\n                            \"description\": \"Errors potentially have a root cause error, ie, a DecoderError might be\\ncaused by an WindowsError\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlayerError\"\n                            }\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Extra data attached to an error, such as an HRESULT, Video Codec, etc.\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Player\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"playerId\",\n                            \"$ref\": \"PlayerId\"\n                        },\n                        {\n                            \"name\": \"domNodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"playerPropertiesChanged\",\n                    \"description\": \"This can be called multiple times, and can be used to set / override /\\nremove player properties. A null propValue indicates removal.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"playerId\",\n                            \"$ref\": \"PlayerId\"\n                        },\n                        {\n                            \"name\": \"properties\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlayerProperty\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"playerEventsAdded\",\n                    \"description\": \"Send events as a list, allowing them to be batched on the browser for less\\ncongestion. If batched, events must ALWAYS be in chronological order.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"playerId\",\n                            \"$ref\": \"PlayerId\"\n                        },\n                        {\n                            \"name\": \"events\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlayerEvent\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"playerMessagesLogged\",\n                    \"description\": \"Send a list of any messages that need to be delivered.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"playerId\",\n                            \"$ref\": \"PlayerId\"\n                        },\n                        {\n                            \"name\": \"messages\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlayerMessage\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"playerErrorsRaised\",\n                    \"description\": \"Send a list of any errors that need to be delivered.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"playerId\",\n                            \"$ref\": \"PlayerId\"\n                        },\n                        {\n                            \"name\": \"errors\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PlayerError\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"playerCreated\",\n                    \"description\": \"Called whenever a player is created, or when a new agent joins and receives\\na list of active players. If an agent is restored, it will receive one\\nevent for each active player.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"player\",\n                            \"$ref\": \"Player\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables the Media domain\"\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables the Media domain.\"\n                }\n            ]\n        },\n        {\n            \"domain\": \"Memory\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"PressureLevel\",\n                    \"description\": \"Memory pressure level.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"moderate\",\n                        \"critical\"\n                    ]\n                },\n                {\n                    \"id\": \"SamplingProfileNode\",\n                    \"description\": \"Heap profile sample.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"Size of the sampled allocation.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"total\",\n                            \"description\": \"Total bytes attributed to this sample.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"stack\",\n                            \"description\": \"Execution stack at the point of allocation.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SamplingProfile\",\n                    \"description\": \"Array of heap profile samples.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"samples\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SamplingProfileNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"modules\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Module\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Module\",\n                    \"description\": \"Executable module information\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name of the module.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"uuid\",\n                            \"description\": \"UUID of the module.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"baseAddress\",\n                            \"description\": \"Base address where the module is loaded into memory. Encoded as a decimal\\nor hexadecimal (0x prefixed) string.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"Size of the module in bytes.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DOMCounter\",\n                    \"description\": \"DOM object counter data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Object name. Note: object names should be presumed volatile and clients should not expect\\nthe returned names to be consistent across runs.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"count\",\n                            \"description\": \"Object count.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getDOMCounters\",\n                    \"description\": \"Retruns current DOM object counters.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"documents\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"nodes\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"jsEventListeners\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getDOMCountersForLeakDetection\",\n                    \"description\": \"Retruns DOM object counters after preparing renderer for leak detection.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"counters\",\n                            \"description\": \"DOM object counters.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOMCounter\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"prepareForLeakDetection\",\n                    \"description\": \"Prepares for leak detection by terminating workers, stopping spellcheckers,\\ndropping non-essential internal caches, running garbage collections, etc.\"\n                },\n                {\n                    \"name\": \"forciblyPurgeJavaScriptMemory\",\n                    \"description\": \"Simulate OomIntervention by purging V8 memory.\"\n                },\n                {\n                    \"name\": \"setPressureNotificationsSuppressed\",\n                    \"description\": \"Enable/disable suppressing memory pressure notifications in all processes.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"suppressed\",\n                            \"description\": \"If true, memory pressure notifications will be suppressed.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"simulatePressureNotification\",\n                    \"description\": \"Simulate a memory pressure notification in all processes.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"level\",\n                            \"description\": \"Memory pressure level of the notification.\",\n                            \"$ref\": \"PressureLevel\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startSampling\",\n                    \"description\": \"Start collecting native memory profile.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"samplingInterval\",\n                            \"description\": \"Average number of bytes between samples.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"suppressRandomness\",\n                            \"description\": \"Do not randomize intervals between samples.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopSampling\",\n                    \"description\": \"Stop collecting native memory profile.\"\n                },\n                {\n                    \"name\": \"getAllTimeSamplingProfile\",\n                    \"description\": \"Retrieve native memory allocations profile\\ncollected since renderer process startup.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"profile\",\n                            \"$ref\": \"SamplingProfile\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getBrowserSamplingProfile\",\n                    \"description\": \"Retrieve native memory allocations profile\\ncollected since browser process startup.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"profile\",\n                            \"$ref\": \"SamplingProfile\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSamplingProfile\",\n                    \"description\": \"Retrieve native memory allocations profile collected since last\\n`startSampling` call.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"profile\",\n                            \"$ref\": \"SamplingProfile\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Network\",\n            \"description\": \"Network domain allows tracking network activities of the page. It exposes information about http,\\nfile, data and other requests and responses, their headers, bodies, timing, etc.\",\n            \"dependencies\": [\n                \"Debugger\",\n                \"Runtime\",\n                \"Security\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"ResourceType\",\n                    \"description\": \"Resource type as it was perceived by the rendering engine.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Document\",\n                        \"Stylesheet\",\n                        \"Image\",\n                        \"Media\",\n                        \"Font\",\n                        \"Script\",\n                        \"TextTrack\",\n                        \"XHR\",\n                        \"Fetch\",\n                        \"Prefetch\",\n                        \"EventSource\",\n                        \"WebSocket\",\n                        \"Manifest\",\n                        \"SignedExchange\",\n                        \"Ping\",\n                        \"CSPViolationReport\",\n                        \"Preflight\",\n                        \"FedCM\",\n                        \"Other\"\n                    ]\n                },\n                {\n                    \"id\": \"LoaderId\",\n                    \"description\": \"Unique loader identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"RequestId\",\n                    \"description\": \"Unique network request identifier.\\nNote that this does not identify individual HTTP requests that are part of\\na network request.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"InterceptionId\",\n                    \"description\": \"Unique intercepted request identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ErrorReason\",\n                    \"description\": \"Network level fetch failure reason.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Failed\",\n                        \"Aborted\",\n                        \"TimedOut\",\n                        \"AccessDenied\",\n                        \"ConnectionClosed\",\n                        \"ConnectionReset\",\n                        \"ConnectionRefused\",\n                        \"ConnectionAborted\",\n                        \"ConnectionFailed\",\n                        \"NameNotResolved\",\n                        \"InternetDisconnected\",\n                        \"AddressUnreachable\",\n                        \"BlockedByClient\",\n                        \"BlockedByResponse\"\n                    ]\n                },\n                {\n                    \"id\": \"TimeSinceEpoch\",\n                    \"description\": \"UTC time in seconds, counted from January 1, 1970.\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"id\": \"MonotonicTime\",\n                    \"description\": \"Monotonically increasing time in seconds since an arbitrary point in the past.\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"id\": \"Headers\",\n                    \"description\": \"Request / response headers as keys / values of JSON object.\",\n                    \"type\": \"object\"\n                },\n                {\n                    \"id\": \"ConnectionType\",\n                    \"description\": \"The underlying connection technology that the browser is supposedly using.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"none\",\n                        \"cellular2g\",\n                        \"cellular3g\",\n                        \"cellular4g\",\n                        \"bluetooth\",\n                        \"ethernet\",\n                        \"wifi\",\n                        \"wimax\",\n                        \"other\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieSameSite\",\n                    \"description\": \"Represents the cookie's 'SameSite' status:\\nhttps://tools.ietf.org/html/draft-west-first-party-cookies\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Strict\",\n                        \"Lax\",\n                        \"None\"\n                    ]\n                },\n                {\n                    \"id\": \"CookiePriority\",\n                    \"description\": \"Represents the cookie's 'Priority' status:\\nhttps://tools.ietf.org/html/draft-west-cookie-priority-00\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Low\",\n                        \"Medium\",\n                        \"High\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieSourceScheme\",\n                    \"description\": \"Represents the source scheme of the origin that originally set the cookie.\\nA value of \\\"Unset\\\" allows protocol clients to emulate legacy cookie scope for the scheme.\\nThis is a temporary ability and it will be removed in the future.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Unset\",\n                        \"NonSecure\",\n                        \"Secure\"\n                    ]\n                },\n                {\n                    \"id\": \"ResourceTiming\",\n                    \"description\": \"Timing information for the request.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"requestTime\",\n                            \"description\": \"Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"proxyStart\",\n                            \"description\": \"Started resolving proxy.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"proxyEnd\",\n                            \"description\": \"Finished resolving proxy.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"dnsStart\",\n                            \"description\": \"Started DNS address resolve.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"dnsEnd\",\n                            \"description\": \"Finished DNS address resolve.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"connectStart\",\n                            \"description\": \"Started connecting to the remote host.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"connectEnd\",\n                            \"description\": \"Connected to the remote host.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sslStart\",\n                            \"description\": \"Started SSL handshake.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sslEnd\",\n                            \"description\": \"Finished SSL handshake.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"workerStart\",\n                            \"description\": \"Started running ServiceWorker.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"workerReady\",\n                            \"description\": \"Finished Starting ServiceWorker.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"workerFetchStart\",\n                            \"description\": \"Started fetch event.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"workerRespondWithSettled\",\n                            \"description\": \"Settled fetch event respondWith promise.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"workerRouterEvaluationStart\",\n                            \"description\": \"Started ServiceWorker static routing source evaluation.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"workerCacheLookupStart\",\n                            \"description\": \"Started cache lookup when the source was evaluated to `cache`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sendStart\",\n                            \"description\": \"Started sending request.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sendEnd\",\n                            \"description\": \"Finished sending request.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pushStart\",\n                            \"description\": \"Time the server started pushing request.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pushEnd\",\n                            \"description\": \"Time the server finished pushing request.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"receiveHeadersStart\",\n                            \"description\": \"Started receiving response headers.\",\n                            \"experimental\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"receiveHeadersEnd\",\n                            \"description\": \"Finished receiving response headers.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ResourcePriority\",\n                    \"description\": \"Loading priority of a resource request.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"VeryLow\",\n                        \"Low\",\n                        \"Medium\",\n                        \"High\",\n                        \"VeryHigh\"\n                    ]\n                },\n                {\n                    \"id\": \"RenderBlockingBehavior\",\n                    \"description\": \"The render-blocking behavior of a resource request.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Blocking\",\n                        \"InBodyParserBlocking\",\n                        \"NonBlocking\",\n                        \"NonBlockingDynamic\",\n                        \"PotentiallyBlocking\"\n                    ]\n                },\n                {\n                    \"id\": \"PostDataEntry\",\n                    \"description\": \"Post data entry for HTTP request\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"bytes\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Request\",\n                    \"description\": \"HTTP request data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Request URL (without fragment).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"urlFragment\",\n                            \"description\": \"Fragment of the requested URL starting with hash, if present.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"method\",\n                            \"description\": \"HTTP request method.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"HTTP request headers.\",\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"postData\",\n                            \"description\": \"HTTP POST request data.\\nUse postDataEntries instead.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"hasPostData\",\n                            \"description\": \"True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"postDataEntries\",\n                            \"description\": \"Request body elements (post data broken into individual entries).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PostDataEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"mixedContentType\",\n                            \"description\": \"The mixed content type of the request.\",\n                            \"optional\": true,\n                            \"$ref\": \"Security.MixedContentType\"\n                        },\n                        {\n                            \"name\": \"initialPriority\",\n                            \"description\": \"Priority of the resource request at the time request is sent.\",\n                            \"$ref\": \"ResourcePriority\"\n                        },\n                        {\n                            \"name\": \"referrerPolicy\",\n                            \"description\": \"The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"unsafe-url\",\n                                \"no-referrer-when-downgrade\",\n                                \"no-referrer\",\n                                \"origin\",\n                                \"origin-when-cross-origin\",\n                                \"same-origin\",\n                                \"strict-origin\",\n                                \"strict-origin-when-cross-origin\"\n                            ]\n                        },\n                        {\n                            \"name\": \"isLinkPreload\",\n                            \"description\": \"Whether is loaded via link preload.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"trustTokenParams\",\n                            \"description\": \"Set for requests when the TrustToken API is used. Contains the parameters\\npassed by the developer (e.g. via \\\"fetch\\\") as understood by the backend.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TrustTokenParams\"\n                        },\n                        {\n                            \"name\": \"isSameSite\",\n                            \"description\": \"True if this resource request is considered to be the 'same site' as the\\nrequest corresponding to the main frame.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isAdRelated\",\n                            \"description\": \"True when the resource request is ad-related.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SignedCertificateTimestamp\",\n                    \"description\": \"Details of a signed certificate timestamp (SCT).\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"status\",\n                            \"description\": \"Validation status.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"logDescription\",\n                            \"description\": \"Log name / description.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"logId\",\n                            \"description\": \"Log ID.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Issuance date. Unlike TimeSinceEpoch, this contains the number of\\nmilliseconds since January 1, 1970, UTC, not the number of seconds.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"hashAlgorithm\",\n                            \"description\": \"Hash algorithm.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"signatureAlgorithm\",\n                            \"description\": \"Signature algorithm.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"signatureData\",\n                            \"description\": \"Signature data.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SecurityDetails\",\n                    \"description\": \"Security details about a request.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"protocol\",\n                            \"description\": \"Protocol name (e.g. \\\"TLS 1.2\\\" or \\\"QUIC\\\").\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyExchange\",\n                            \"description\": \"Key Exchange used by the connection, or the empty string if not applicable.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyExchangeGroup\",\n                            \"description\": \"(EC)DH group used by the connection, if applicable.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cipher\",\n                            \"description\": \"Cipher name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mac\",\n                            \"description\": \"TLS MAC. Note that AEAD ciphers do not have separate MACs.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"certificateId\",\n                            \"description\": \"Certificate ID value.\",\n                            \"$ref\": \"Security.CertificateId\"\n                        },\n                        {\n                            \"name\": \"subjectName\",\n                            \"description\": \"Certificate subject name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sanList\",\n                            \"description\": \"Subject Alternative Name (SAN) DNS names and IP addresses.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"issuer\",\n                            \"description\": \"Name of the issuing CA.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"validFrom\",\n                            \"description\": \"Certificate valid from date.\",\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"validTo\",\n                            \"description\": \"Certificate valid to (expiration) date\",\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"signedCertificateTimestampList\",\n                            \"description\": \"List of signed certificate timestamps (SCTs).\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SignedCertificateTimestamp\"\n                            }\n                        },\n                        {\n                            \"name\": \"certificateTransparencyCompliance\",\n                            \"description\": \"Whether the request complied with Certificate Transparency policy\",\n                            \"$ref\": \"CertificateTransparencyCompliance\"\n                        },\n                        {\n                            \"name\": \"serverSignatureAlgorithm\",\n                            \"description\": \"The signature algorithm used by the server in the TLS server signature,\\nrepresented as a TLS SignatureScheme code point. Omitted if not\\napplicable or not known.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"encryptedClientHello\",\n                            \"description\": \"Whether the connection used Encrypted ClientHello\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CertificateTransparencyCompliance\",\n                    \"description\": \"Whether the request complied with Certificate Transparency policy.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"unknown\",\n                        \"not-compliant\",\n                        \"compliant\"\n                    ]\n                },\n                {\n                    \"id\": \"BlockedReason\",\n                    \"description\": \"The reason why request was blocked.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"other\",\n                        \"csp\",\n                        \"mixed-content\",\n                        \"origin\",\n                        \"inspector\",\n                        \"integrity\",\n                        \"subresource-filter\",\n                        \"content-type\",\n                        \"coep-frame-resource-needs-coep-header\",\n                        \"coop-sandboxed-iframe-cannot-navigate-to-coop-page\",\n                        \"corp-not-same-origin\",\n                        \"corp-not-same-origin-after-defaulted-to-same-origin-by-coep\",\n                        \"corp-not-same-origin-after-defaulted-to-same-origin-by-dip\",\n                        \"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip\",\n                        \"corp-not-same-site\",\n                        \"sri-message-signature-mismatch\"\n                    ]\n                },\n                {\n                    \"id\": \"CorsError\",\n                    \"description\": \"The reason why request was blocked.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"DisallowedByMode\",\n                        \"InvalidResponse\",\n                        \"WildcardOriginNotAllowed\",\n                        \"MissingAllowOriginHeader\",\n                        \"MultipleAllowOriginValues\",\n                        \"InvalidAllowOriginValue\",\n                        \"AllowOriginMismatch\",\n                        \"InvalidAllowCredentials\",\n                        \"CorsDisabledScheme\",\n                        \"PreflightInvalidStatus\",\n                        \"PreflightDisallowedRedirect\",\n                        \"PreflightWildcardOriginNotAllowed\",\n                        \"PreflightMissingAllowOriginHeader\",\n                        \"PreflightMultipleAllowOriginValues\",\n                        \"PreflightInvalidAllowOriginValue\",\n                        \"PreflightAllowOriginMismatch\",\n                        \"PreflightInvalidAllowCredentials\",\n                        \"PreflightMissingAllowExternal\",\n                        \"PreflightInvalidAllowExternal\",\n                        \"InvalidAllowMethodsPreflightResponse\",\n                        \"InvalidAllowHeadersPreflightResponse\",\n                        \"MethodDisallowedByPreflightResponse\",\n                        \"HeaderDisallowedByPreflightResponse\",\n                        \"RedirectContainsCredentials\",\n                        \"InsecureLocalNetwork\",\n                        \"InvalidLocalNetworkAccess\",\n                        \"NoCorsRedirectModeNotFollow\",\n                        \"LocalNetworkAccessPermissionDenied\"\n                    ]\n                },\n                {\n                    \"id\": \"CorsErrorStatus\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"corsError\",\n                            \"$ref\": \"CorsError\"\n                        },\n                        {\n                            \"name\": \"failedParameter\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerResponseSource\",\n                    \"description\": \"Source of serviceworker response.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"cache-storage\",\n                        \"http-cache\",\n                        \"fallback-code\",\n                        \"network\"\n                    ]\n                },\n                {\n                    \"id\": \"TrustTokenParams\",\n                    \"description\": \"Determines what type of Trust Token operation is executed and\\ndepending on the type, some additional parameters. The values\\nare specified in third_party/blink/renderer/core/fetch/trust_token.idl.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"operation\",\n                            \"$ref\": \"TrustTokenOperationType\"\n                        },\n                        {\n                            \"name\": \"refreshPolicy\",\n                            \"description\": \"Only set for \\\"token-redemption\\\" operation and determine whether\\nto request a fresh SRR or use a still valid cached SRR.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"UseCached\",\n                                \"Refresh\"\n                            ]\n                        },\n                        {\n                            \"name\": \"issuers\",\n                            \"description\": \"Origins of issuers from whom to request tokens or redemption\\nrecords.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"TrustTokenOperationType\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Issuance\",\n                        \"Redemption\",\n                        \"Signing\"\n                    ]\n                },\n                {\n                    \"id\": \"AlternateProtocolUsage\",\n                    \"description\": \"The reason why Chrome uses a specific transport protocol for HTTP semantics.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"alternativeJobWonWithoutRace\",\n                        \"alternativeJobWonRace\",\n                        \"mainJobWonRace\",\n                        \"mappingMissing\",\n                        \"broken\",\n                        \"dnsAlpnH3JobWonWithoutRace\",\n                        \"dnsAlpnH3JobWonRace\",\n                        \"unspecifiedReason\"\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerRouterSource\",\n                    \"description\": \"Source of service worker router.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"network\",\n                        \"cache\",\n                        \"fetch-event\",\n                        \"race-network-and-fetch-handler\",\n                        \"race-network-and-cache\"\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerRouterInfo\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"ruleIdMatched\",\n                            \"description\": \"ID of the rule matched. If there is a matched rule, this field will\\nbe set, otherwiser no value will be set.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"matchedSourceType\",\n                            \"description\": \"The router source of the matched rule. If there is a matched rule, this\\nfield will be set, otherwise no value will be set.\",\n                            \"optional\": true,\n                            \"$ref\": \"ServiceWorkerRouterSource\"\n                        },\n                        {\n                            \"name\": \"actualSourceType\",\n                            \"description\": \"The actual router source used.\",\n                            \"optional\": true,\n                            \"$ref\": \"ServiceWorkerRouterSource\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Response\",\n                    \"description\": \"HTTP response data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Response URL. This URL can be different from CachedResource.url in case of redirect.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"description\": \"HTTP response status code.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"statusText\",\n                            \"description\": \"HTTP response status text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"HTTP response headers.\",\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"headersText\",\n                            \"description\": \"HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mimeType\",\n                            \"description\": \"Resource mimeType as determined by the browser.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"charset\",\n                            \"description\": \"Resource charset as determined by the browser (if applicable).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestHeaders\",\n                            \"description\": \"Refined HTTP request headers that were actually transmitted over the network.\",\n                            \"optional\": true,\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"requestHeadersText\",\n                            \"description\": \"HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"connectionReused\",\n                            \"description\": \"Specifies whether physical connection was actually reused for this request.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"connectionId\",\n                            \"description\": \"Physical connection id that was actually used for this request.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"remoteIPAddress\",\n                            \"description\": \"Remote IP address.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remotePort\",\n                            \"description\": \"Remote port.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"fromDiskCache\",\n                            \"description\": \"Specifies that the request was served from the disk cache.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"fromServiceWorker\",\n                            \"description\": \"Specifies that the request was served from the ServiceWorker.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"fromPrefetchCache\",\n                            \"description\": \"Specifies that the request was served from the prefetch cache.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"fromEarlyHints\",\n                            \"description\": \"Specifies that the request was served from the prefetch cache.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"serviceWorkerRouterInfo\",\n                            \"description\": \"Information about how ServiceWorker Static Router API was used. If this\\nfield is set with `matchedSourceType` field, a matching rule is found.\\nIf this field is set without `matchedSource`, no matching rule is found.\\nOtherwise, the API is not used.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"ServiceWorkerRouterInfo\"\n                        },\n                        {\n                            \"name\": \"encodedDataLength\",\n                            \"description\": \"Total number of bytes received for this request so far.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"timing\",\n                            \"description\": \"Timing information for the given request.\",\n                            \"optional\": true,\n                            \"$ref\": \"ResourceTiming\"\n                        },\n                        {\n                            \"name\": \"serviceWorkerResponseSource\",\n                            \"description\": \"Response source of response from ServiceWorker.\",\n                            \"optional\": true,\n                            \"$ref\": \"ServiceWorkerResponseSource\"\n                        },\n                        {\n                            \"name\": \"responseTime\",\n                            \"description\": \"The time at which the returned response was generated.\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"cacheStorageCacheName\",\n                            \"description\": \"Cache Storage Cache Name.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"protocol\",\n                            \"description\": \"Protocol used to fetch this request.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"alternateProtocolUsage\",\n                            \"description\": \"The reason why Chrome uses a specific transport protocol for HTTP semantics.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"AlternateProtocolUsage\"\n                        },\n                        {\n                            \"name\": \"securityState\",\n                            \"description\": \"Security state of the request resource.\",\n                            \"$ref\": \"Security.SecurityState\"\n                        },\n                        {\n                            \"name\": \"securityDetails\",\n                            \"description\": \"Security details for the request.\",\n                            \"optional\": true,\n                            \"$ref\": \"SecurityDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WebSocketRequest\",\n                    \"description\": \"WebSocket request data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"HTTP request headers.\",\n                            \"$ref\": \"Headers\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WebSocketResponse\",\n                    \"description\": \"WebSocket response data.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"status\",\n                            \"description\": \"HTTP response status code.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"statusText\",\n                            \"description\": \"HTTP response status text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"HTTP response headers.\",\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"headersText\",\n                            \"description\": \"HTTP response headers text.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestHeaders\",\n                            \"description\": \"HTTP request headers.\",\n                            \"optional\": true,\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"requestHeadersText\",\n                            \"description\": \"HTTP request headers text.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WebSocketFrame\",\n                    \"description\": \"WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"opcode\",\n                            \"description\": \"WebSocket message opcode.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"mask\",\n                            \"description\": \"WebSocket message mask.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"payloadData\",\n                            \"description\": \"WebSocket message payload data.\\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\\nIf the opcode isn't 1, then payloadData is a base64 encoded string representing binary data.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CachedResource\",\n                    \"description\": \"Information about the cached resource.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Resource URL. This is the url of the original network request.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of this resource.\",\n                            \"$ref\": \"ResourceType\"\n                        },\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"Cached response data.\",\n                            \"optional\": true,\n                            \"$ref\": \"Response\"\n                        },\n                        {\n                            \"name\": \"bodySize\",\n                            \"description\": \"Cached response body size.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Initiator\",\n                    \"description\": \"Information about the request initiator.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of this initiator.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"parser\",\n                                \"script\",\n                                \"preload\",\n                                \"SignedExchange\",\n                                \"preflight\",\n                                \"FedCM\",\n                                \"other\"\n                            ]\n                        },\n                        {\n                            \"name\": \"stack\",\n                            \"description\": \"Initiator JavaScript stack trace, set for Script only.\\nRequires the Debugger domain to be enabled.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Initiator line number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Initiator column number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Set if another request triggered this request (e.g. preflight).\",\n                            \"optional\": true,\n                            \"$ref\": \"RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CookiePartitionKey\",\n                    \"description\": \"cookiePartitionKey object\\nThe representation of the components of the key that are created by the cookiePartitionKey class contained in net/cookies/cookie_partition_key.h.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"topLevelSite\",\n                            \"description\": \"The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"hasCrossSiteAncestor\",\n                            \"description\": \"Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Cookie\",\n                    \"description\": \"Cookie object\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Cookie name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Cookie value.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domain\",\n                            \"description\": \"Cookie domain.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"Cookie path.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"expires\",\n                            \"description\": \"Cookie expiration date as the number of seconds since the UNIX epoch.\\nThe value is set to -1 if the expiry date is not set.\\nThe value can be null for values that cannot be represented in\\nJSON (\\u00b1Inf).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"Cookie size.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"httpOnly\",\n                            \"description\": \"True if cookie is http-only.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"secure\",\n                            \"description\": \"True if cookie is secure.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"session\",\n                            \"description\": \"True in case of session cookie.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"sameSite\",\n                            \"description\": \"Cookie SameSite type.\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieSameSite\"\n                        },\n                        {\n                            \"name\": \"priority\",\n                            \"description\": \"Cookie Priority\",\n                            \"experimental\": true,\n                            \"$ref\": \"CookiePriority\"\n                        },\n                        {\n                            \"name\": \"sourceScheme\",\n                            \"description\": \"Cookie source scheme type.\",\n                            \"experimental\": true,\n                            \"$ref\": \"CookieSourceScheme\"\n                        },\n                        {\n                            \"name\": \"sourcePort\",\n                            \"description\": \"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.\",\n                            \"experimental\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"partitionKey\",\n                            \"description\": \"Cookie partition key.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePartitionKey\"\n                        },\n                        {\n                            \"name\": \"partitionKeyOpaque\",\n                            \"description\": \"True if cookie partition key is opaque.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SetCookieBlockedReason\",\n                    \"description\": \"Types of reasons why a cookie may not be stored from a response.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SecureOnly\",\n                        \"SameSiteStrict\",\n                        \"SameSiteLax\",\n                        \"SameSiteUnspecifiedTreatedAsLax\",\n                        \"SameSiteNoneInsecure\",\n                        \"UserPreferences\",\n                        \"ThirdPartyPhaseout\",\n                        \"ThirdPartyBlockedInFirstPartySet\",\n                        \"SyntaxError\",\n                        \"SchemeNotSupported\",\n                        \"OverwriteSecure\",\n                        \"InvalidDomain\",\n                        \"InvalidPrefix\",\n                        \"UnknownError\",\n                        \"SchemefulSameSiteStrict\",\n                        \"SchemefulSameSiteLax\",\n                        \"SchemefulSameSiteUnspecifiedTreatedAsLax\",\n                        \"NameValuePairExceedsMaxSize\",\n                        \"DisallowedCharacter\",\n                        \"NoCookieContent\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieBlockedReason\",\n                    \"description\": \"Types of reasons why a cookie may not be sent with a request.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SecureOnly\",\n                        \"NotOnPath\",\n                        \"DomainMismatch\",\n                        \"SameSiteStrict\",\n                        \"SameSiteLax\",\n                        \"SameSiteUnspecifiedTreatedAsLax\",\n                        \"SameSiteNoneInsecure\",\n                        \"UserPreferences\",\n                        \"ThirdPartyPhaseout\",\n                        \"ThirdPartyBlockedInFirstPartySet\",\n                        \"UnknownError\",\n                        \"SchemefulSameSiteStrict\",\n                        \"SchemefulSameSiteLax\",\n                        \"SchemefulSameSiteUnspecifiedTreatedAsLax\",\n                        \"NameValuePairExceedsMaxSize\",\n                        \"PortMismatch\",\n                        \"SchemeMismatch\",\n                        \"AnonymousContext\"\n                    ]\n                },\n                {\n                    \"id\": \"CookieExemptionReason\",\n                    \"description\": \"Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"None\",\n                        \"UserSetting\",\n                        \"TPCDMetadata\",\n                        \"TPCDDeprecationTrial\",\n                        \"TopLevelTPCDDeprecationTrial\",\n                        \"TPCDHeuristics\",\n                        \"EnterprisePolicy\",\n                        \"StorageAccess\",\n                        \"TopLevelStorageAccess\",\n                        \"Scheme\",\n                        \"SameSiteNoneCookiesInSandbox\"\n                    ]\n                },\n                {\n                    \"id\": \"BlockedSetCookieWithReason\",\n                    \"description\": \"A cookie which was not stored from a response with the corresponding reason.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"blockedReasons\",\n                            \"description\": \"The reason(s) this cookie was blocked.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SetCookieBlockedReason\"\n                            }\n                        },\n                        {\n                            \"name\": \"cookieLine\",\n                            \"description\": \"The string representing this individual cookie as it would appear in the header.\\nThis is not the entire \\\"cookie\\\" or \\\"set-cookie\\\" header which could have multiple cookies.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cookie\",\n                            \"description\": \"The cookie object which represents the cookie which was not stored. It is optional because\\nsometimes complete cookie information is not available, such as in the case of parsing\\nerrors.\",\n                            \"optional\": true,\n                            \"$ref\": \"Cookie\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ExemptedSetCookieWithReason\",\n                    \"description\": \"A cookie should have been blocked by 3PCD but is exempted and stored from a response with the\\ncorresponding reason. A cookie could only have at most one exemption reason.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"exemptionReason\",\n                            \"description\": \"The reason the cookie was exempted.\",\n                            \"$ref\": \"CookieExemptionReason\"\n                        },\n                        {\n                            \"name\": \"cookieLine\",\n                            \"description\": \"The string representing this individual cookie as it would appear in the header.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cookie\",\n                            \"description\": \"The cookie object representing the cookie.\",\n                            \"$ref\": \"Cookie\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AssociatedCookie\",\n                    \"description\": \"A cookie associated with the request which may or may not be sent with it.\\nIncludes the cookies itself and reasons for blocking or exemption.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"cookie\",\n                            \"description\": \"The cookie object representing the cookie which was not sent.\",\n                            \"$ref\": \"Cookie\"\n                        },\n                        {\n                            \"name\": \"blockedReasons\",\n                            \"description\": \"The reason(s) the cookie was blocked. If empty means the cookie is included.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CookieBlockedReason\"\n                            }\n                        },\n                        {\n                            \"name\": \"exemptionReason\",\n                            \"description\": \"The reason the cookie should have been blocked by 3PCD but is exempted. A cookie could\\nonly have at most one exemption reason.\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieExemptionReason\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CookieParam\",\n                    \"description\": \"Cookie parameter object\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Cookie name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Cookie value.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain, path, source port, and source scheme values of the created cookie.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domain\",\n                            \"description\": \"Cookie domain.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"Cookie path.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"secure\",\n                            \"description\": \"True if cookie is secure.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"httpOnly\",\n                            \"description\": \"True if cookie is http-only.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"sameSite\",\n                            \"description\": \"Cookie SameSite type.\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieSameSite\"\n                        },\n                        {\n                            \"name\": \"expires\",\n                            \"description\": \"Cookie expiration date, session cookie if not set\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"priority\",\n                            \"description\": \"Cookie Priority.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePriority\"\n                        },\n                        {\n                            \"name\": \"sourceScheme\",\n                            \"description\": \"Cookie source scheme type.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookieSourceScheme\"\n                        },\n                        {\n                            \"name\": \"sourcePort\",\n                            \"description\": \"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"partitionKey\",\n                            \"description\": \"Cookie partition key. If not set, the cookie will be set as not partitioned.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePartitionKey\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AuthChallenge\",\n                    \"description\": \"Authorization challenge for HTTP status code 401 or 407.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"source\",\n                            \"description\": \"Source of the authentication challenge.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Server\",\n                                \"Proxy\"\n                            ]\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin of the challenger.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scheme\",\n                            \"description\": \"The authentication scheme used, such as basic or digest\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"realm\",\n                            \"description\": \"The realm of the challenge. May be empty.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AuthChallengeResponse\",\n                    \"description\": \"Response to an AuthChallenge.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"The decision on what to do in response to the authorization challenge.  Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Default\",\n                                \"CancelAuth\",\n                                \"ProvideCredentials\"\n                            ]\n                        },\n                        {\n                            \"name\": \"username\",\n                            \"description\": \"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"password\",\n                            \"description\": \"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InterceptionStage\",\n                    \"description\": \"Stages of the interception to begin intercepting. Request will intercept before the request is\\nsent. Response will intercept after the response is received.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Request\",\n                        \"HeadersReceived\"\n                    ]\n                },\n                {\n                    \"id\": \"RequestPattern\",\n                    \"description\": \"Request pattern for interception.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"urlPattern\",\n                            \"description\": \"Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to `\\\"*\\\"`.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"resourceType\",\n                            \"description\": \"If set, only requests for matching resource types will be intercepted.\",\n                            \"optional\": true,\n                            \"$ref\": \"ResourceType\"\n                        },\n                        {\n                            \"name\": \"interceptionStage\",\n                            \"description\": \"Stage at which to begin intercepting requests. Default is Request.\",\n                            \"optional\": true,\n                            \"$ref\": \"InterceptionStage\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SignedExchangeSignature\",\n                    \"description\": \"Information about a signed exchange signature.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"label\",\n                            \"description\": \"Signed exchange signature label.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"signature\",\n                            \"description\": \"The hex string of signed exchange signature.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"integrity\",\n                            \"description\": \"Signed exchange signature integrity.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"certUrl\",\n                            \"description\": \"Signed exchange signature cert Url.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"certSha256\",\n                            \"description\": \"The hex string of signed exchange signature cert sha256.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"validityUrl\",\n                            \"description\": \"Signed exchange signature validity Url.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"date\",\n                            \"description\": \"Signed exchange signature date.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"expires\",\n                            \"description\": \"Signed exchange signature expires.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"certificates\",\n                            \"description\": \"The encoded certificates.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SignedExchangeHeader\",\n                    \"description\": \"Information about a signed exchange header.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"requestUrl\",\n                            \"description\": \"Signed exchange request URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"responseCode\",\n                            \"description\": \"Signed exchange response code.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responseHeaders\",\n                            \"description\": \"Signed exchange response headers.\",\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"signatures\",\n                            \"description\": \"Signed exchange response signature.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SignedExchangeSignature\"\n                            }\n                        },\n                        {\n                            \"name\": \"headerIntegrity\",\n                            \"description\": \"Signed exchange header integrity hash in the form of `sha256-<base64-hash-value>`.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SignedExchangeErrorField\",\n                    \"description\": \"Field type for a signed exchange related error.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"signatureSig\",\n                        \"signatureIntegrity\",\n                        \"signatureCertUrl\",\n                        \"signatureCertSha256\",\n                        \"signatureValidityUrl\",\n                        \"signatureTimestamps\"\n                    ]\n                },\n                {\n                    \"id\": \"SignedExchangeError\",\n                    \"description\": \"Information about a signed exchange response.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"message\",\n                            \"description\": \"Error message.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"signatureIndex\",\n                            \"description\": \"The index of the signature which caused the error.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"errorField\",\n                            \"description\": \"The field which caused the error.\",\n                            \"optional\": true,\n                            \"$ref\": \"SignedExchangeErrorField\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SignedExchangeInfo\",\n                    \"description\": \"Information about a signed exchange response.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"outerResponse\",\n                            \"description\": \"The outer response of signed HTTP exchange which was received from network.\",\n                            \"$ref\": \"Response\"\n                        },\n                        {\n                            \"name\": \"hasExtraInfo\",\n                            \"description\": \"Whether network response for the signed exchange was accompanied by\\nextra headers.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"header\",\n                            \"description\": \"Information about the signed exchange header.\",\n                            \"optional\": true,\n                            \"$ref\": \"SignedExchangeHeader\"\n                        },\n                        {\n                            \"name\": \"securityDetails\",\n                            \"description\": \"Security details for the signed exchange header.\",\n                            \"optional\": true,\n                            \"$ref\": \"SecurityDetails\"\n                        },\n                        {\n                            \"name\": \"errors\",\n                            \"description\": \"Errors occurred while handling the signed exchange.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SignedExchangeError\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContentEncoding\",\n                    \"description\": \"List of content encodings supported by the backend.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"deflate\",\n                        \"gzip\",\n                        \"br\",\n                        \"zstd\"\n                    ]\n                },\n                {\n                    \"id\": \"NetworkConditions\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"urlPattern\",\n                            \"description\": \"Only matching requests will be affected by these conditions. Patterns use the URLPattern constructor string\\nsyntax (https://urlpattern.spec.whatwg.org/) and must be absolute. If the pattern is empty, all requests are\\nmatched (including p2p connections).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"latency\",\n                            \"description\": \"Minimum latency from request sent to response headers received (ms).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"downloadThroughput\",\n                            \"description\": \"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"uploadThroughput\",\n                            \"description\": \"Maximal aggregated upload throughput (bytes/sec).  -1 disables upload throttling.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"connectionType\",\n                            \"description\": \"Connection type if known.\",\n                            \"optional\": true,\n                            \"$ref\": \"ConnectionType\"\n                        },\n                        {\n                            \"name\": \"packetLoss\",\n                            \"description\": \"WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"packetQueueLength\",\n                            \"description\": \"WebRTC packet queue length (packet). 0 removes any queue length limitations.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"packetReordering\",\n                            \"description\": \"WebRTC packetReordering feature.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BlockPattern\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"urlPattern\",\n                            \"description\": \"URL pattern to match. Patterns use the URLPattern constructor string syntax\\n(https://urlpattern.spec.whatwg.org/) and must be absolute. Example: `*://*:*/*.css`.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"block\",\n                            \"description\": \"Whether or not to block the pattern. If false, a matching request will not be blocked even if it matches a later\\n`BlockPattern`.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DirectSocketDnsQueryType\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ipv4\",\n                        \"ipv6\"\n                    ]\n                },\n                {\n                    \"id\": \"DirectTCPSocketOptions\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"noDelay\",\n                            \"description\": \"TCP_NODELAY option\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"keepAliveDelay\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sendBufferSize\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"receiveBufferSize\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"dnsQueryType\",\n                            \"optional\": true,\n                            \"$ref\": \"DirectSocketDnsQueryType\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DirectUDPSocketOptions\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"remoteAddr\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remotePort\",\n                            \"description\": \"Unsigned int 16.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"localAddr\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"localPort\",\n                            \"description\": \"Unsigned int 16.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"dnsQueryType\",\n                            \"optional\": true,\n                            \"$ref\": \"DirectSocketDnsQueryType\"\n                        },\n                        {\n                            \"name\": \"sendBufferSize\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"receiveBufferSize\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"multicastLoopback\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"multicastTimeToLive\",\n                            \"description\": \"Unsigned int 8.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"multicastAllowAddressSharing\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DirectUDPMessage\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remoteAddr\",\n                            \"description\": \"Null for connected mode.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remotePort\",\n                            \"description\": \"Null for connected mode.\\nExpected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LocalNetworkAccessRequestPolicy\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Allow\",\n                        \"BlockFromInsecureToMorePrivate\",\n                        \"WarnFromInsecureToMorePrivate\",\n                        \"PermissionBlock\",\n                        \"PermissionWarn\"\n                    ]\n                },\n                {\n                    \"id\": \"IPAddressSpace\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Loopback\",\n                        \"Local\",\n                        \"Public\",\n                        \"Unknown\"\n                    ]\n                },\n                {\n                    \"id\": \"ConnectTiming\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"requestTime\",\n                            \"description\": \"Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime. Matches ResourceTiming's requestTime for\\nthe same request (but not for redirected requests).\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ClientSecurityState\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"initiatorIsSecureContext\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"initiatorIPAddressSpace\",\n                            \"$ref\": \"IPAddressSpace\"\n                        },\n                        {\n                            \"name\": \"localNetworkAccessRequestPolicy\",\n                            \"$ref\": \"LocalNetworkAccessRequestPolicy\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CrossOriginOpenerPolicyValue\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SameOrigin\",\n                        \"SameOriginAllowPopups\",\n                        \"RestrictProperties\",\n                        \"UnsafeNone\",\n                        \"SameOriginPlusCoep\",\n                        \"RestrictPropertiesPlusCoep\",\n                        \"NoopenerAllowPopups\"\n                    ]\n                },\n                {\n                    \"id\": \"CrossOriginOpenerPolicyStatus\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"value\",\n                            \"$ref\": \"CrossOriginOpenerPolicyValue\"\n                        },\n                        {\n                            \"name\": \"reportOnlyValue\",\n                            \"$ref\": \"CrossOriginOpenerPolicyValue\"\n                        },\n                        {\n                            \"name\": \"reportingEndpoint\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"reportOnlyReportingEndpoint\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CrossOriginEmbedderPolicyValue\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"None\",\n                        \"Credentialless\",\n                        \"RequireCorp\"\n                    ]\n                },\n                {\n                    \"id\": \"CrossOriginEmbedderPolicyStatus\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"value\",\n                            \"$ref\": \"CrossOriginEmbedderPolicyValue\"\n                        },\n                        {\n                            \"name\": \"reportOnlyValue\",\n                            \"$ref\": \"CrossOriginEmbedderPolicyValue\"\n                        },\n                        {\n                            \"name\": \"reportingEndpoint\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"reportOnlyReportingEndpoint\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContentSecurityPolicySource\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"HTTP\",\n                        \"Meta\"\n                    ]\n                },\n                {\n                    \"id\": \"ContentSecurityPolicyStatus\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"effectiveDirectives\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isEnforced\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"source\",\n                            \"$ref\": \"ContentSecurityPolicySource\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SecurityIsolationStatus\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"coop\",\n                            \"optional\": true,\n                            \"$ref\": \"CrossOriginOpenerPolicyStatus\"\n                        },\n                        {\n                            \"name\": \"coep\",\n                            \"optional\": true,\n                            \"$ref\": \"CrossOriginEmbedderPolicyStatus\"\n                        },\n                        {\n                            \"name\": \"csp\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ContentSecurityPolicyStatus\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ReportStatus\",\n                    \"description\": \"The status of a Reporting API report.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Queued\",\n                        \"Pending\",\n                        \"MarkedForRemoval\",\n                        \"Success\"\n                    ]\n                },\n                {\n                    \"id\": \"ReportId\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ReportingApiReport\",\n                    \"description\": \"An object representing a report generated by the Reporting API.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"ReportId\"\n                        },\n                        {\n                            \"name\": \"initiatorUrl\",\n                            \"description\": \"The URL of the document that triggered the report.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"destination\",\n                            \"description\": \"The name of the endpoint group that should be used to deliver the report.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"The type of the report (specifies the set of data that is contained in the report body).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"When the report was generated.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"depth\",\n                            \"description\": \"How many uploads deep the related request was.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"completedAttempts\",\n                            \"description\": \"The number of delivery attempts made so far, not including an active attempt.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"body\",\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"ReportStatus\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ReportingApiEndpoint\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The URL of the endpoint to which reports may be delivered.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"groupName\",\n                            \"description\": \"Name of the endpoint group.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionKey\",\n                    \"description\": \"Unique identifier for a device bound session.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"site\",\n                            \"description\": \"The site the session is set up for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"The id of the session.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionWithUsage\",\n                    \"description\": \"How a device bound session was used during a request.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"sessionKey\",\n                            \"description\": \"The key for the session.\",\n                            \"$ref\": \"DeviceBoundSessionKey\"\n                        },\n                        {\n                            \"name\": \"usage\",\n                            \"description\": \"How the session was used (or not used).\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"NotInScope\",\n                                \"InScopeRefreshNotYetNeeded\",\n                                \"InScopeRefreshNotAllowed\",\n                                \"ProactiveRefreshNotPossible\",\n                                \"ProactiveRefreshAttempted\",\n                                \"Deferred\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionCookieCraving\",\n                    \"description\": \"A device bound session's cookie craving.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"The name of the craving.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domain\",\n                            \"description\": \"The domain of the craving.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"The path of the craving.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"secure\",\n                            \"description\": \"The `Secure` attribute of the craving attributes.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"httpOnly\",\n                            \"description\": \"The `HttpOnly` attribute of the craving attributes.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"sameSite\",\n                            \"description\": \"The `SameSite` attribute of the craving attributes.\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieSameSite\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionUrlRule\",\n                    \"description\": \"A device bound session's inclusion URL rule.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"ruleType\",\n                            \"description\": \"See comments on `net::device_bound_sessions::SessionInclusionRules::UrlRule::rule_type`.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Exclude\",\n                                \"Include\"\n                            ]\n                        },\n                        {\n                            \"name\": \"hostPattern\",\n                            \"description\": \"See comments on `net::device_bound_sessions::SessionInclusionRules::UrlRule::host_pattern`.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"pathPrefix\",\n                            \"description\": \"See comments on `net::device_bound_sessions::SessionInclusionRules::UrlRule::path_prefix`.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionInclusionRules\",\n                    \"description\": \"A device bound session's inclusion rules.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"See comments on `net::device_bound_sessions::SessionInclusionRules::origin_`.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"includeSite\",\n                            \"description\": \"Whether the whole site is included. See comments on\\n`net::device_bound_sessions::SessionInclusionRules::include_site_` for more\\ndetails; this boolean is true if that value is populated.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"urlRules\",\n                            \"description\": \"See comments on `net::device_bound_sessions::SessionInclusionRules::url_rules_`.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DeviceBoundSessionUrlRule\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSession\",\n                    \"description\": \"A device bound session.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"description\": \"The site and session ID of the session.\",\n                            \"$ref\": \"DeviceBoundSessionKey\"\n                        },\n                        {\n                            \"name\": \"refreshUrl\",\n                            \"description\": \"See comments on `net::device_bound_sessions::Session::refresh_url_`.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"inclusionRules\",\n                            \"description\": \"See comments on `net::device_bound_sessions::Session::inclusion_rules_`.\",\n                            \"$ref\": \"DeviceBoundSessionInclusionRules\"\n                        },\n                        {\n                            \"name\": \"cookieCravings\",\n                            \"description\": \"See comments on `net::device_bound_sessions::Session::cookie_cravings_`.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DeviceBoundSessionCookieCraving\"\n                            }\n                        },\n                        {\n                            \"name\": \"expiryDate\",\n                            \"description\": \"See comments on `net::device_bound_sessions::Session::expiry_date_`.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"cachedChallenge\",\n                            \"description\": \"See comments on `net::device_bound_sessions::Session::cached_challenge__`.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"allowedRefreshInitiators\",\n                            \"description\": \"See comments on `net::device_bound_sessions::Session::allowed_refresh_initiators_`.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionEventId\",\n                    \"description\": \"A unique identifier for a device bound session event.\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"DeviceBoundSessionFetchResult\",\n                    \"description\": \"A fetch result for a device bound session creation or refresh.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Success\",\n                        \"KeyError\",\n                        \"SigningError\",\n                        \"ServerRequestedTermination\",\n                        \"InvalidSessionId\",\n                        \"InvalidChallenge\",\n                        \"TooManyChallenges\",\n                        \"InvalidFetcherUrl\",\n                        \"InvalidRefreshUrl\",\n                        \"TransientHttpError\",\n                        \"ScopeOriginSameSiteMismatch\",\n                        \"RefreshUrlSameSiteMismatch\",\n                        \"MismatchedSessionId\",\n                        \"MissingScope\",\n                        \"NoCredentials\",\n                        \"SubdomainRegistrationWellKnownUnavailable\",\n                        \"SubdomainRegistrationUnauthorized\",\n                        \"SubdomainRegistrationWellKnownMalformed\",\n                        \"SessionProviderWellKnownUnavailable\",\n                        \"RelyingPartyWellKnownUnavailable\",\n                        \"FederatedKeyThumbprintMismatch\",\n                        \"InvalidFederatedSessionUrl\",\n                        \"InvalidFederatedKey\",\n                        \"TooManyRelyingOriginLabels\",\n                        \"BoundCookieSetForbidden\",\n                        \"NetError\",\n                        \"ProxyError\",\n                        \"EmptySessionConfig\",\n                        \"InvalidCredentialsConfig\",\n                        \"InvalidCredentialsType\",\n                        \"InvalidCredentialsEmptyName\",\n                        \"InvalidCredentialsCookie\",\n                        \"PersistentHttpError\",\n                        \"RegistrationAttemptedChallenge\",\n                        \"InvalidScopeOrigin\",\n                        \"ScopeOriginContainsPath\",\n                        \"RefreshInitiatorNotString\",\n                        \"RefreshInitiatorInvalidHostPattern\",\n                        \"InvalidScopeSpecification\",\n                        \"MissingScopeSpecificationType\",\n                        \"EmptyScopeSpecificationDomain\",\n                        \"EmptyScopeSpecificationPath\",\n                        \"InvalidScopeSpecificationType\",\n                        \"InvalidScopeIncludeSite\",\n                        \"MissingScopeIncludeSite\",\n                        \"FederatedNotAuthorizedByProvider\",\n                        \"FederatedNotAuthorizedByRelyingParty\",\n                        \"SessionProviderWellKnownMalformed\",\n                        \"SessionProviderWellKnownHasProviderOrigin\",\n                        \"RelyingPartyWellKnownMalformed\",\n                        \"RelyingPartyWellKnownHasRelyingOrigins\",\n                        \"InvalidFederatedSessionProviderSessionMissing\",\n                        \"InvalidFederatedSessionWrongProviderOrigin\",\n                        \"InvalidCredentialsCookieCreationTime\",\n                        \"InvalidCredentialsCookieName\",\n                        \"InvalidCredentialsCookieParsing\",\n                        \"InvalidCredentialsCookieUnpermittedAttribute\",\n                        \"InvalidCredentialsCookieInvalidDomain\",\n                        \"InvalidCredentialsCookiePrefix\",\n                        \"InvalidScopeRulePath\",\n                        \"InvalidScopeRuleHostPattern\",\n                        \"ScopeRuleOriginScopedHostPatternMismatch\",\n                        \"ScopeRuleSiteScopedHostPatternMismatch\",\n                        \"SigningQuotaExceeded\",\n                        \"InvalidConfigJson\",\n                        \"InvalidFederatedSessionProviderFailedToRestoreKey\",\n                        \"FailedToUnwrapKey\",\n                        \"SessionDeletedDuringRefresh\"\n                    ]\n                },\n                {\n                    \"id\": \"DeviceBoundSessionFailedRequest\",\n                    \"description\": \"Details about a failed device bound session network request.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"requestUrl\",\n                            \"description\": \"The failed request URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"netError\",\n                            \"description\": \"The net error of the response if it was not OK.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"responseError\",\n                            \"description\": \"The response code if the net error was OK and the response code was not\\n200.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responseErrorBody\",\n                            \"description\": \"The body of the response if the net error was OK, the response code was\\nnot 200, and the response body was not empty.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CreationEventDetails\",\n                    \"description\": \"Session event details specific to creation.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"fetchResult\",\n                            \"description\": \"The result of the fetch attempt.\",\n                            \"$ref\": \"DeviceBoundSessionFetchResult\"\n                        },\n                        {\n                            \"name\": \"newSession\",\n                            \"description\": \"The session if there was a newly created session. This is populated for\\nall successful creation events.\",\n                            \"optional\": true,\n                            \"$ref\": \"DeviceBoundSession\"\n                        },\n                        {\n                            \"name\": \"failedRequest\",\n                            \"description\": \"Details about a failed device bound session network request if there was\\none.\",\n                            \"optional\": true,\n                            \"$ref\": \"DeviceBoundSessionFailedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RefreshEventDetails\",\n                    \"description\": \"Session event details specific to refresh.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"refreshResult\",\n                            \"description\": \"The result of a refresh.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Refreshed\",\n                                \"InitializedService\",\n                                \"Unreachable\",\n                                \"ServerError\",\n                                \"RefreshQuotaExceeded\",\n                                \"FatalError\",\n                                \"SigningQuotaExceeded\"\n                            ]\n                        },\n                        {\n                            \"name\": \"fetchResult\",\n                            \"description\": \"If there was a fetch attempt, the result of that.\",\n                            \"optional\": true,\n                            \"$ref\": \"DeviceBoundSessionFetchResult\"\n                        },\n                        {\n                            \"name\": \"newSession\",\n                            \"description\": \"The session display if there was a newly created session. This is populated\\nfor any refresh event that modifies the session config.\",\n                            \"optional\": true,\n                            \"$ref\": \"DeviceBoundSession\"\n                        },\n                        {\n                            \"name\": \"wasFullyProactiveRefresh\",\n                            \"description\": \"See comments on `net::device_bound_sessions::RefreshEventResult::was_fully_proactive_refresh`.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"failedRequest\",\n                            \"description\": \"Details about a failed device bound session network request if there was\\none.\",\n                            \"optional\": true,\n                            \"$ref\": \"DeviceBoundSessionFailedRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"TerminationEventDetails\",\n                    \"description\": \"Session event details specific to termination.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"deletionReason\",\n                            \"description\": \"The reason for a session being deleted.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Expired\",\n                                \"FailedToRestoreKey\",\n                                \"FailedToUnwrapKey\",\n                                \"StoragePartitionCleared\",\n                                \"ClearBrowsingData\",\n                                \"ServerRequested\",\n                                \"InvalidSessionParams\",\n                                \"RefreshFatalError\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ChallengeEventDetails\",\n                    \"description\": \"Session event details specific to challenges.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"challengeResult\",\n                            \"description\": \"The result of a challenge.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Success\",\n                                \"NoSessionId\",\n                                \"NoSessionMatch\",\n                                \"CantSetBoundCookie\"\n                            ]\n                        },\n                        {\n                            \"name\": \"challenge\",\n                            \"description\": \"The challenge set.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LoadNetworkResourcePageResult\",\n                    \"description\": \"An object providing the result of a network resource load.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"success\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"netError\",\n                            \"description\": \"Optional values used for error reporting.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"netErrorName\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"httpStatusCode\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"stream\",\n                            \"description\": \"If successful, one of the following two fields holds the result.\",\n                            \"optional\": true,\n                            \"$ref\": \"IO.StreamHandle\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"Response headers.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.Headers\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LoadNetworkResourceOptions\",\n                    \"description\": \"An options object that may be extended later to better support CORS,\\nCORB and streaming.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"disableCache\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeCredentials\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"setAcceptedEncodings\",\n                    \"description\": \"Sets a list of content encodings that will be accepted. Empty list means no encoding is accepted.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"encodings\",\n                            \"description\": \"List of accepted content encodings.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ContentEncoding\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearAcceptedEncodingsOverride\",\n                    \"description\": \"Clears accepted encodings set by setAcceptedEncodings\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"canClearBrowserCache\",\n                    \"description\": \"Tells whether clearing browser cache is supported.\",\n                    \"deprecated\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"True if browser cache can be cleared.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"canClearBrowserCookies\",\n                    \"description\": \"Tells whether clearing browser cookies is supported.\",\n                    \"deprecated\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"True if browser cookies can be cleared.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"canEmulateNetworkConditions\",\n                    \"description\": \"Tells whether emulation of network conditions is supported.\",\n                    \"deprecated\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"True if emulation of network conditions is supported.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearBrowserCache\",\n                    \"description\": \"Clears browser cache.\"\n                },\n                {\n                    \"name\": \"clearBrowserCookies\",\n                    \"description\": \"Clears browser cookies.\"\n                },\n                {\n                    \"name\": \"continueInterceptedRequest\",\n                    \"description\": \"Response to Network.requestIntercepted which either modifies the request to continue with any\\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\\nevent will be sent with the same InterceptionId.\\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"interceptionId\",\n                            \"$ref\": \"InterceptionId\"\n                        },\n                        {\n                            \"name\": \"errorReason\",\n                            \"description\": \"If set this causes the request to fail with the given reason. Passing `Aborted` for requests\\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\\nto an authChallenge.\",\n                            \"optional\": true,\n                            \"$ref\": \"ErrorReason\"\n                        },\n                        {\n                            \"name\": \"rawResponse\",\n                            \"description\": \"If set the requests completes using with the provided base64 encoded raw response, including\\nHTTP status line and headers etc... Must not be set in response to an authChallenge. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"If set the request url will be modified in a way that's not observable by page. Must not be\\nset in response to an authChallenge.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"method\",\n                            \"description\": \"If set this allows the request method to be overridden. Must not be set in response to an\\nauthChallenge.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"postData\",\n                            \"description\": \"If set this allows postData to be set. Must not be set in response to an authChallenge.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"If set this allows the request headers to be changed. Must not be set in response to an\\nauthChallenge.\",\n                            \"optional\": true,\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"authChallengeResponse\",\n                            \"description\": \"Response to a requestIntercepted with an authChallenge. Must not be set otherwise.\",\n                            \"optional\": true,\n                            \"$ref\": \"AuthChallengeResponse\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteCookies\",\n                    \"description\": \"Deletes browser cookies with matching name and url or domain/path/partitionKey pair.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name of the cookies to remove.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"If specified, deletes all the cookies with the given name where domain and path match\\nprovided URL.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domain\",\n                            \"description\": \"If specified, deletes only cookies with the exact domain.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"If specified, deletes only cookies with the exact path.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"partitionKey\",\n                            \"description\": \"If specified, deletes only cookies with the the given name and partitionKey where\\nall partition key attributes match the cookie partition key attribute.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePartitionKey\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables network tracking, prevents network events from being sent to the client.\"\n                },\n                {\n                    \"name\": \"emulateNetworkConditions\",\n                    \"description\": \"Activates emulation of network conditions. This command is deprecated in favor of the emulateNetworkConditionsByRule\\nand overrideNetworkState commands, which can be used together to the same effect.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"offline\",\n                            \"description\": \"True to emulate internet disconnection.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"latency\",\n                            \"description\": \"Minimum latency from request sent to response headers received (ms).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"downloadThroughput\",\n                            \"description\": \"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"uploadThroughput\",\n                            \"description\": \"Maximal aggregated upload throughput (bytes/sec).  -1 disables upload throttling.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"connectionType\",\n                            \"description\": \"Connection type if known.\",\n                            \"optional\": true,\n                            \"$ref\": \"ConnectionType\"\n                        },\n                        {\n                            \"name\": \"packetLoss\",\n                            \"description\": \"WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"packetQueueLength\",\n                            \"description\": \"WebRTC packet queue length (packet). 0 removes any queue length limitations.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"packetReordering\",\n                            \"description\": \"WebRTC packetReordering feature.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"emulateNetworkConditionsByRule\",\n                    \"description\": \"Activates emulation of network conditions for individual requests using URL match patterns. Unlike the deprecated\\nNetwork.emulateNetworkConditions this method does not affect `navigator` state. Use Network.overrideNetworkState to\\nexplicitly modify `navigator` behavior.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"offline\",\n                            \"description\": \"True to emulate internet disconnection.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"matchedNetworkConditions\",\n                            \"description\": \"Configure conditions for matching requests. If multiple entries match a request, the first entry wins.  Global\\nconditions can be configured by leaving the urlPattern for the conditions empty. These global conditions are\\nalso applied for throttling of p2p connections.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NetworkConditions\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"ruleIds\",\n                            \"description\": \"An id for each entry in matchedNetworkConditions. The id will be included in the requestWillBeSentExtraInfo for\\nrequests affected by a rule.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"overrideNetworkState\",\n                    \"description\": \"Override the state of navigator.onLine and navigator.connection.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"offline\",\n                            \"description\": \"True to emulate internet disconnection.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"latency\",\n                            \"description\": \"Minimum latency from request sent to response headers received (ms).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"downloadThroughput\",\n                            \"description\": \"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"uploadThroughput\",\n                            \"description\": \"Maximal aggregated upload throughput (bytes/sec).  -1 disables upload throttling.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"connectionType\",\n                            \"description\": \"Connection type if known.\",\n                            \"optional\": true,\n                            \"$ref\": \"ConnectionType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables network tracking, network events will now be delivered to the client.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"maxTotalBufferSize\",\n                            \"description\": \"Buffer size in bytes to use when preserving network payloads (XHRs, etc).\\nThis is the maximum number of bytes that will be collected by this\\nDevTools session.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxResourceBufferSize\",\n                            \"description\": \"Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxPostDataSize\",\n                            \"description\": \"Longest post body size (in bytes) that would be included in requestWillBeSent notification\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"reportDirectSocketTraffic\",\n                            \"description\": \"Whether DirectSocket chunk send/receive events should be reported.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"enableDurableMessages\",\n                            \"description\": \"Enable storing response bodies outside of renderer, so that these survive\\na cross-process navigation. Requires maxTotalBufferSize to be set.\\nCurrently defaults to false. This field is being deprecated in favor of the dedicated\\nconfigureDurableMessages command, due to the possibility of deadlocks when awaiting\\nNetwork.enable before issuing Runtime.runIfWaitingForDebugger.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"configureDurableMessages\",\n                    \"description\": \"Configures storing response bodies outside of renderer, so that these survive\\na cross-process navigation.\\nIf maxTotalBufferSize is not set, durable messages are disabled.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"maxTotalBufferSize\",\n                            \"description\": \"Buffer size in bytes to use when preserving network payloads (XHRs, etc).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxResourceBufferSize\",\n                            \"description\": \"Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAllCookies\",\n                    \"description\": \"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.\\nDeprecated. Use Storage.getCookies instead.\",\n                    \"deprecated\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"cookies\",\n                            \"description\": \"Array of cookie objects.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Cookie\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getCertificate\",\n                    \"description\": \"Returns the DER-encoded certificate.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin to get certificate for.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"tableNames\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getCookies\",\n                    \"description\": \"Returns all browser cookies for the current URL. Depending on the backend support, will return\\ndetailed cookie information in the `cookies` field.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"urls\",\n                            \"description\": \"The list of URLs for which applicable cookies will be fetched.\\nIf not specified, it's assumed to be set to the list containing\\nthe URLs of the page and all of its subframes.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"cookies\",\n                            \"description\": \"Array of cookie objects.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Cookie\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getResponseBody\",\n                    \"description\": \"Returns content served for the given request.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier of the network request to get content for.\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"body\",\n                            \"description\": \"Response body.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"base64Encoded\",\n                            \"description\": \"True, if content was sent as base64.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getRequestPostData\",\n                    \"description\": \"Returns post data sent with the request. Returns an error when no data was sent with the request.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier of the network request to get content for.\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"postData\",\n                            \"description\": \"Request body string, omitting files from multipart requests\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"base64Encoded\",\n                            \"description\": \"True, if content was sent as base64.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getResponseBodyForInterception\",\n                    \"description\": \"Returns content served for the given currently intercepted request.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"interceptionId\",\n                            \"description\": \"Identifier for the intercepted request to get body for.\",\n                            \"$ref\": \"InterceptionId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"body\",\n                            \"description\": \"Response body.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"base64Encoded\",\n                            \"description\": \"True, if content was sent as base64.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"takeResponseBodyForInterceptionAsStream\",\n                    \"description\": \"Returns a handle to the stream representing the response body. Note that after this command,\\nthe intercepted request can't be continued as is -- you either need to cancel it or to provide\\nthe response body. The stream only supports sequential read, IO.read will fail if the position\\nis specified.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"interceptionId\",\n                            \"$ref\": \"InterceptionId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"stream\",\n                            \"$ref\": \"IO.StreamHandle\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"replayXHR\",\n                    \"description\": \"This method sends a new XMLHttpRequest which is identical to the original one. The following\\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\\nattribute, user, password.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier of XHR to replay.\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"searchInResponseBody\",\n                    \"description\": \"Searches for given string in response content.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier of the network response to search.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"query\",\n                            \"description\": \"String to search for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"caseSensitive\",\n                            \"description\": \"If true, search is case sensitive.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isRegex\",\n                            \"description\": \"If true, treats string parameter as regex.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"List of search matches.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Debugger.SearchMatch\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBlockedURLs\",\n                    \"description\": \"Blocks URLs from loading.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"urlPatterns\",\n                            \"description\": \"Patterns to match in the order in which they are given. These patterns\\nalso take precedence over any wildcard patterns defined in `urls`.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BlockPattern\"\n                            }\n                        },\n                        {\n                            \"name\": \"urls\",\n                            \"description\": \"URL patterns to block. Wildcards ('*') are allowed.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBypassServiceWorker\",\n                    \"description\": \"Toggles ignoring of service worker for each request.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"bypass\",\n                            \"description\": \"Bypass service worker and load from network.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCacheDisabled\",\n                    \"description\": \"Toggles ignoring cache for each request. If `true`, cache will not be used.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cacheDisabled\",\n                            \"description\": \"Cache disabled state.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCookie\",\n                    \"description\": \"Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Cookie name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Cookie value.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain, path, source port, and source scheme values of the created cookie.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domain\",\n                            \"description\": \"Cookie domain.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"path\",\n                            \"description\": \"Cookie path.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"secure\",\n                            \"description\": \"True if cookie is secure.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"httpOnly\",\n                            \"description\": \"True if cookie is http-only.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"sameSite\",\n                            \"description\": \"Cookie SameSite type.\",\n                            \"optional\": true,\n                            \"$ref\": \"CookieSameSite\"\n                        },\n                        {\n                            \"name\": \"expires\",\n                            \"description\": \"Cookie expiration date, session cookie if not set\",\n                            \"optional\": true,\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"priority\",\n                            \"description\": \"Cookie Priority type.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePriority\"\n                        },\n                        {\n                            \"name\": \"sourceScheme\",\n                            \"description\": \"Cookie source scheme type.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookieSourceScheme\"\n                        },\n                        {\n                            \"name\": \"sourcePort\",\n                            \"description\": \"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"partitionKey\",\n                            \"description\": \"Cookie partition key. If not set, the cookie will be set as not partitioned.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePartitionKey\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"success\",\n                            \"description\": \"Always set to true. If an error occurs, the response indicates protocol error.\",\n                            \"deprecated\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCookies\",\n                    \"description\": \"Sets given cookies.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cookies\",\n                            \"description\": \"Cookies to be set.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CookieParam\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setExtraHTTPHeaders\",\n                    \"description\": \"Specifies whether to always send extra HTTP headers with the requests from this page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"Map with extra HTTP headers.\",\n                            \"$ref\": \"Headers\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAttachDebugStack\",\n                    \"description\": \"Specifies whether to attach a page script stack id in requests\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether to attach a page script stack for debugging purpose.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setRequestInterception\",\n                    \"description\": \"Sets the requests to intercept that match the provided patterns and optionally resource types.\\nDeprecated, please use Fetch.enable instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"patterns\",\n                            \"description\": \"Requests matching any of these patterns will be forwarded and wait for the corresponding\\ncontinueInterceptedRequest call.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RequestPattern\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setUserAgentOverride\",\n                    \"description\": \"Allows overriding user agent with the given string.\",\n                    \"redirect\": \"Emulation\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"userAgent\",\n                            \"description\": \"User agent to use.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"acceptLanguage\",\n                            \"description\": \"Browser language to emulate.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"platform\",\n                            \"description\": \"The platform navigator.platform should return.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"userAgentMetadata\",\n                            \"description\": \"To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Emulation.UserAgentMetadata\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"streamResourceContent\",\n                    \"description\": \"Enables streaming of the response for the given requestId.\\nIf enabled, the dataReceived event contains the data that was received during streaming.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Identifier of the request to stream.\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"bufferedData\",\n                            \"description\": \"Data that has been buffered until streaming is enabled. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSecurityIsolationStatus\",\n                    \"description\": \"Returns information about the COEP/COOP isolation status.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"If no frameId is provided, the status of the target is provided.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"SecurityIsolationStatus\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"enableReportingApi\",\n                    \"description\": \"Enables tracking for the Reporting API, events generated by the Reporting API will now be delivered to the client.\\nEnabling triggers 'reportingApiReportAdded' for all existing reports.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"description\": \"Whether to enable or disable events for the Reporting API\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"enableDeviceBoundSessions\",\n                    \"description\": \"Sets up tracking device bound sessions and fetching of initial set of sessions.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"description\": \"Whether to enable or disable events.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"fetchSchemefulSite\",\n                    \"description\": \"Fetches the schemeful site for a specific origin.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"The URL origin.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"schemefulSite\",\n                            \"description\": \"The corresponding schemeful site.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"loadNetworkResource\",\n                    \"description\": \"Fetches the resource and returns the content.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id to get the resource for. Mandatory for frame targets, and\\nshould be omitted for worker targets.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resource to get content for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"options\",\n                            \"description\": \"Options for the request.\",\n                            \"$ref\": \"LoadNetworkResourceOptions\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"resource\",\n                            \"$ref\": \"LoadNetworkResourcePageResult\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCookieControls\",\n                    \"description\": \"Sets Controls for third-party cookie access\\nPage reload is required before the new cookie behavior will be observed\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enableThirdPartyCookieRestriction\",\n                            \"description\": \"Whether 3pc restriction is enabled.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disableThirdPartyCookieMetadata\",\n                            \"description\": \"Whether 3pc grace period exception should be enabled; false by default.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disableThirdPartyCookieHeuristics\",\n                            \"description\": \"Whether 3pc heuristics exceptions should be enabled; false by default.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"dataReceived\",\n                    \"description\": \"Fired when data chunk was received over the network.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"dataLength\",\n                            \"description\": \"Data chunk length.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"encodedDataLength\",\n                            \"description\": \"Actual bytes received (might be less than dataLength for compressed encodings).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Data that was received. (Encoded as a base64 string when passed over JSON)\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"eventSourceMessageReceived\",\n                    \"description\": \"Fired when EventSource message is received.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"eventName\",\n                            \"description\": \"Message type.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"eventId\",\n                            \"description\": \"Message identifier.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Message content.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"loadingFailed\",\n                    \"description\": \"Fired when HTTP request has failed to load.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Resource type.\",\n                            \"$ref\": \"ResourceType\"\n                        },\n                        {\n                            \"name\": \"errorText\",\n                            \"description\": \"Error message. List of network errors: https://cs.chromium.org/chromium/src/net/base/net_error_list.h\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"canceled\",\n                            \"description\": \"True if loading was canceled.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"blockedReason\",\n                            \"description\": \"The reason why loading was blocked, if any.\",\n                            \"optional\": true,\n                            \"$ref\": \"BlockedReason\"\n                        },\n                        {\n                            \"name\": \"corsErrorStatus\",\n                            \"description\": \"The reason why loading was blocked by CORS, if any.\",\n                            \"optional\": true,\n                            \"$ref\": \"CorsErrorStatus\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"loadingFinished\",\n                    \"description\": \"Fired when HTTP request has finished loading.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"encodedDataLength\",\n                            \"description\": \"Total number of bytes received for this request.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestIntercepted\",\n                    \"description\": \"Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\\nmocked.\\nDeprecated, use Fetch.requestPaused instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"interceptionId\",\n                            \"description\": \"Each request the page makes will have a unique id, however if any redirects are encountered\\nwhile processing that fetch, they will be reported with the same id as the original fetch.\\nLikewise if HTTP authentication is needed then the same fetch id will be used.\",\n                            \"$ref\": \"InterceptionId\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"$ref\": \"Request\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The id of the frame that initiated the request.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"resourceType\",\n                            \"description\": \"How the requested resource will be used.\",\n                            \"$ref\": \"ResourceType\"\n                        },\n                        {\n                            \"name\": \"isNavigationRequest\",\n                            \"description\": \"Whether this is a navigation request, which can abort the navigation completely.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isDownload\",\n                            \"description\": \"Set if the request is a navigation that will result in a download.\\nOnly present after response is received from the server (i.e. HeadersReceived stage).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"redirectUrl\",\n                            \"description\": \"Redirect location, only sent if a redirect was intercepted.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"authChallenge\",\n                            \"description\": \"Details of the Authorization Challenge encountered. If this is set then\\ncontinueInterceptedRequest must contain an authChallengeResponse.\",\n                            \"optional\": true,\n                            \"$ref\": \"AuthChallenge\"\n                        },\n                        {\n                            \"name\": \"responseErrorReason\",\n                            \"description\": \"Response error if intercepted at response stage or if redirect occurred while intercepting\\nrequest.\",\n                            \"optional\": true,\n                            \"$ref\": \"ErrorReason\"\n                        },\n                        {\n                            \"name\": \"responseStatusCode\",\n                            \"description\": \"Response code if intercepted at response stage or if redirect occurred while intercepting\\nrequest or auth retry occurred.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"responseHeaders\",\n                            \"description\": \"Response headers if intercepted at the response stage or if redirect occurred while\\nintercepting request or auth retry occurred.\",\n                            \"optional\": true,\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"If the intercepted request had a corresponding requestWillBeSent event fired for it, then\\nthis requestId will be the same as the requestId present in the requestWillBeSent event.\",\n                            \"optional\": true,\n                            \"$ref\": \"RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestServedFromCache\",\n                    \"description\": \"Fired if request ended up loading from cache.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestWillBeSent\",\n                    \"description\": \"Fired when page is about to send HTTP request.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Loader identifier. Empty string if the request is fetched from worker.\",\n                            \"$ref\": \"LoaderId\"\n                        },\n                        {\n                            \"name\": \"documentURL\",\n                            \"description\": \"URL of the document this request is loaded for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"Request data.\",\n                            \"$ref\": \"Request\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"wallTime\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"initiator\",\n                            \"description\": \"Request initiator.\",\n                            \"$ref\": \"Initiator\"\n                        },\n                        {\n                            \"name\": \"redirectHasExtraInfo\",\n                            \"description\": \"In the case that redirectResponse is populated, this flag indicates whether\\nrequestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted\\nfor the request which was just redirected.\",\n                            \"experimental\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"redirectResponse\",\n                            \"description\": \"Redirect response data.\",\n                            \"optional\": true,\n                            \"$ref\": \"Response\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of this resource.\",\n                            \"optional\": true,\n                            \"$ref\": \"ResourceType\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame identifier.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"hasUserGesture\",\n                            \"description\": \"Whether the request is initiated by a user gesture. Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"renderBlockingBehavior\",\n                            \"description\": \"The render-blocking behavior of the request.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"RenderBlockingBehavior\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resourceChangedPriority\",\n                    \"description\": \"Fired when resource loading priority is changed\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"newPriority\",\n                            \"description\": \"New priority\",\n                            \"$ref\": \"ResourcePriority\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"signedExchangeReceived\",\n                    \"description\": \"Fired when a signed exchange was received over the network\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"info\",\n                            \"description\": \"Information about the signed exchange response.\",\n                            \"$ref\": \"SignedExchangeInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"responseReceived\",\n                    \"description\": \"Fired when HTTP response is available.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Loader identifier. Empty string if the request is fetched from worker.\",\n                            \"$ref\": \"LoaderId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Resource type.\",\n                            \"$ref\": \"ResourceType\"\n                        },\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"Response data.\",\n                            \"$ref\": \"Response\"\n                        },\n                        {\n                            \"name\": \"hasExtraInfo\",\n                            \"description\": \"Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be\\nor were emitted for this request.\",\n                            \"experimental\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame identifier.\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketClosed\",\n                    \"description\": \"Fired when WebSocket is closed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketCreated\",\n                    \"description\": \"Fired upon WebSocket creation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"WebSocket request URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"initiator\",\n                            \"description\": \"Request initiator.\",\n                            \"optional\": true,\n                            \"$ref\": \"Initiator\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketFrameError\",\n                    \"description\": \"Fired when WebSocket message error occurs.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"errorMessage\",\n                            \"description\": \"WebSocket error message.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketFrameReceived\",\n                    \"description\": \"Fired when WebSocket message is received.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"WebSocket response data.\",\n                            \"$ref\": \"WebSocketFrame\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketFrameSent\",\n                    \"description\": \"Fired when WebSocket message is sent.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"WebSocket response data.\",\n                            \"$ref\": \"WebSocketFrame\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketHandshakeResponseReceived\",\n                    \"description\": \"Fired when WebSocket handshake response becomes available.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"response\",\n                            \"description\": \"WebSocket response data.\",\n                            \"$ref\": \"WebSocketResponse\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webSocketWillSendHandshakeRequest\",\n                    \"description\": \"Fired when WebSocket is about to initiate handshake.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"wallTime\",\n                            \"description\": \"UTC Timestamp.\",\n                            \"$ref\": \"TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"request\",\n                            \"description\": \"WebSocket request data.\",\n                            \"$ref\": \"WebSocketRequest\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webTransportCreated\",\n                    \"description\": \"Fired upon WebTransport creation.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"transportId\",\n                            \"description\": \"WebTransport identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"WebTransport request URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"initiator\",\n                            \"description\": \"Request initiator.\",\n                            \"optional\": true,\n                            \"$ref\": \"Initiator\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webTransportConnectionEstablished\",\n                    \"description\": \"Fired when WebTransport handshake is finished.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"transportId\",\n                            \"description\": \"WebTransport identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"webTransportClosed\",\n                    \"description\": \"Fired when WebTransport is disposed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"transportId\",\n                            \"description\": \"WebTransport identifier.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp.\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directTCPSocketCreated\",\n                    \"description\": \"Fired upon direct_socket.TCPSocket creation.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"remoteAddr\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remotePort\",\n                            \"description\": \"Unsigned int 16.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"options\",\n                            \"$ref\": \"DirectTCPSocketOptions\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"initiator\",\n                            \"optional\": true,\n                            \"$ref\": \"Initiator\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directTCPSocketOpened\",\n                    \"description\": \"Fired when direct_socket.TCPSocket connection is opened.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"remoteAddr\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remotePort\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"localAddr\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"localPort\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directTCPSocketAborted\",\n                    \"description\": \"Fired when direct_socket.TCPSocket is aborted.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"errorMessage\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directTCPSocketClosed\",\n                    \"description\": \"Fired when direct_socket.TCPSocket is closed.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directTCPSocketChunkSent\",\n                    \"description\": \"Fired when data is sent to tcp direct socket stream.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directTCPSocketChunkReceived\",\n                    \"description\": \"Fired when data is received from tcp direct socket stream.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketJoinedMulticastGroup\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"IPAddress\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketLeftMulticastGroup\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"IPAddress\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketCreated\",\n                    \"description\": \"Fired upon direct_socket.UDPSocket creation.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"options\",\n                            \"$ref\": \"DirectUDPSocketOptions\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"initiator\",\n                            \"optional\": true,\n                            \"$ref\": \"Initiator\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketOpened\",\n                    \"description\": \"Fired when direct_socket.UDPSocket connection is opened.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"localAddr\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"localPort\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        },\n                        {\n                            \"name\": \"remoteAddr\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"remotePort\",\n                            \"description\": \"Expected to be unsigned integer.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketAborted\",\n                    \"description\": \"Fired when direct_socket.UDPSocket is aborted.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"errorMessage\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketClosed\",\n                    \"description\": \"Fired when direct_socket.UDPSocket is closed.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketChunkSent\",\n                    \"description\": \"Fired when message is sent to udp direct socket stream.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"message\",\n                            \"$ref\": \"DirectUDPMessage\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"directUDPSocketChunkReceived\",\n                    \"description\": \"Fired when message is received from udp direct socket stream.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"message\",\n                            \"$ref\": \"DirectUDPMessage\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestWillBeSentExtraInfo\",\n                    \"description\": \"Fired when additional information about a requestWillBeSent event is available from the\\nnetwork stack. Not every requestWillBeSent event will have an additional\\nrequestWillBeSentExtraInfo fired for it, and there is no guarantee whether requestWillBeSent\\nor requestWillBeSentExtraInfo will be fired first for the same request.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier. Used to match this information to an existing requestWillBeSent event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"associatedCookies\",\n                            \"description\": \"A list of cookies potentially associated to the requested URL. This includes both cookies sent with\\nthe request and the ones not sent; the latter are distinguished by having blockedReasons field set.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AssociatedCookie\"\n                            }\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"Raw request headers as they will be sent over the wire.\",\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"connectTiming\",\n                            \"description\": \"Connection timing information for the request.\",\n                            \"experimental\": true,\n                            \"$ref\": \"ConnectTiming\"\n                        },\n                        {\n                            \"name\": \"deviceBoundSessionUsages\",\n                            \"description\": \"How the request site's device bound sessions were used during this request.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DeviceBoundSessionWithUsage\"\n                            }\n                        },\n                        {\n                            \"name\": \"clientSecurityState\",\n                            \"description\": \"The client security state set for the request.\",\n                            \"optional\": true,\n                            \"$ref\": \"ClientSecurityState\"\n                        },\n                        {\n                            \"name\": \"siteHasCookieInOtherPartition\",\n                            \"description\": \"Whether the site has partitioned cookies stored in a partition different than the current one.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"appliedNetworkConditionsId\",\n                            \"description\": \"The network conditions id if this request was affected by network conditions configured via\\nemulateNetworkConditionsByRule.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"responseReceivedExtraInfo\",\n                    \"description\": \"Fired when additional information about a responseReceived event is available from the network\\nstack. Not every responseReceived event will have an additional responseReceivedExtraInfo for\\nit, and responseReceivedExtraInfo may be fired before or after responseReceived.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier. Used to match this information to another responseReceived event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"blockedCookies\",\n                            \"description\": \"A list of cookies which were not stored from the response along with the corresponding\\nreasons for blocking. The cookies here may not be valid due to syntax errors, which\\nare represented by the invalid cookie line string instead of a proper cookie.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BlockedSetCookieWithReason\"\n                            }\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"Raw response headers as they were received over the wire.\\nDuplicate headers in the response are represented as a single key with their values\\nconcatentated using `\\\\n` as the separator.\\nSee also `headersText` that contains verbatim text for HTTP/1.*.\",\n                            \"$ref\": \"Headers\"\n                        },\n                        {\n                            \"name\": \"resourceIPAddressSpace\",\n                            \"description\": \"The IP address space of the resource. The address space can only be determined once the transport\\nestablished the connection, so we can't send it in `requestWillBeSentExtraInfo`.\",\n                            \"$ref\": \"IPAddressSpace\"\n                        },\n                        {\n                            \"name\": \"statusCode\",\n                            \"description\": \"The status code of the response. This is useful in cases the request failed and no responseReceived\\nevent is triggered, which is the case for, e.g., CORS errors. This is also the correct status code\\nfor cached requests, where the status in responseReceived is a 200 and this will be 304.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"headersText\",\n                            \"description\": \"Raw response header text as it was received over the wire. The raw text may not always be\\navailable, such as in the case of HTTP/2 or QUIC.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cookiePartitionKey\",\n                            \"description\": \"The cookie partition key that will be used to store partitioned cookies set in this response.\\nOnly sent when partitioned cookies are enabled.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CookiePartitionKey\"\n                        },\n                        {\n                            \"name\": \"cookiePartitionKeyOpaque\",\n                            \"description\": \"True if partitioned cookies are enabled, but the partition key is not serializable to string.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"exemptedCookies\",\n                            \"description\": \"A list of cookies which should have been blocked by 3PCD but are exempted and stored from\\nthe response with the corresponding reason.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ExemptedSetCookieWithReason\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"responseReceivedEarlyHints\",\n                    \"description\": \"Fired when 103 Early Hints headers is received in addition to the common response.\\nNot every responseReceived event will have an responseReceivedEarlyHints fired.\\nOnly one responseReceivedEarlyHints may be fired for eached responseReceived event.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"description\": \"Request identifier. Used to match this information to another responseReceived event.\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"headers\",\n                            \"description\": \"Raw response headers as they were received over the wire.\\nDuplicate headers in the response are represented as a single key with their values\\nconcatentated using `\\\\n` as the separator.\\nSee also `headersText` that contains verbatim text for HTTP/1.*.\",\n                            \"$ref\": \"Headers\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trustTokenOperationDone\",\n                    \"description\": \"Fired exactly once for each Trust Token operation. Depending on\\nthe type of the operation and whether the operation succeeded or\\nfailed, the event is fired before the corresponding request was sent\\nor after the response was received.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"status\",\n                            \"description\": \"Detailed success or error status of the operation.\\n'AlreadyExists' also signifies a successful operation, as the result\\nof the operation already exists und thus, the operation was abort\\npreemptively (e.g. a cache hit).\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Ok\",\n                                \"InvalidArgument\",\n                                \"MissingIssuerKeys\",\n                                \"FailedPrecondition\",\n                                \"ResourceExhausted\",\n                                \"AlreadyExists\",\n                                \"ResourceLimited\",\n                                \"Unauthorized\",\n                                \"BadResponse\",\n                                \"InternalError\",\n                                \"UnknownError\",\n                                \"FulfilledLocally\",\n                                \"SiteIssuerLimit\"\n                            ]\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"TrustTokenOperationType\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"$ref\": \"RequestId\"\n                        },\n                        {\n                            \"name\": \"topLevelOrigin\",\n                            \"description\": \"Top level origin. The context in which the operation was attempted.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"issuerOrigin\",\n                            \"description\": \"Origin of the issuer in case of a \\\"Issuance\\\" or \\\"Redemption\\\" operation.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"issuedTokenCount\",\n                            \"description\": \"The number of obtained Trust Tokens on a successful \\\"Issuance\\\" operation.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"policyUpdated\",\n                    \"description\": \"Fired once security policy has been updated.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"reportingApiReportAdded\",\n                    \"description\": \"Is sent whenever a new report is added.\\nAnd after 'enableReportingApi' for all existing reports.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"report\",\n                            \"$ref\": \"ReportingApiReport\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportingApiReportUpdated\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"report\",\n                            \"$ref\": \"ReportingApiReport\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportingApiEndpointsChangedForOrigin\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin of the document(s) which configured the endpoints.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"endpoints\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ReportingApiEndpoint\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deviceBoundSessionsAdded\",\n                    \"description\": \"Triggered when the initial set of device bound sessions is added.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"sessions\",\n                            \"description\": \"The device bound sessions.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DeviceBoundSession\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deviceBoundSessionEventOccurred\",\n                    \"description\": \"Triggered when a device bound session event occurs.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventId\",\n                            \"description\": \"A unique identifier for this session event.\",\n                            \"$ref\": \"DeviceBoundSessionEventId\"\n                        },\n                        {\n                            \"name\": \"site\",\n                            \"description\": \"The site this session event is associated with.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"succeeded\",\n                            \"description\": \"Whether this event was considered successful.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"The session ID this event is associated with. May not be populated for\\nfailed events.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"creationEventDetails\",\n                            \"description\": \"The below are the different session event type details. Exactly one is populated.\",\n                            \"optional\": true,\n                            \"$ref\": \"CreationEventDetails\"\n                        },\n                        {\n                            \"name\": \"refreshEventDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"RefreshEventDetails\"\n                        },\n                        {\n                            \"name\": \"terminationEventDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"TerminationEventDetails\"\n                        },\n                        {\n                            \"name\": \"challengeEventDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"ChallengeEventDetails\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Overlay\",\n            \"description\": \"This domain provides various functionality related to drawing atop the inspected page.\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"DOM\",\n                \"Page\",\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"SourceOrderConfig\",\n                    \"description\": \"Configuration data for drawing the source order of an elements children.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"parentOutlineColor\",\n                            \"description\": \"the color to outline the given element in.\",\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"childOutlineColor\",\n                            \"description\": \"the color to outline the child elements in.\",\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"GridHighlightConfig\",\n                    \"description\": \"Configuration data for the highlighting of Grid elements.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"showGridExtensionLines\",\n                            \"description\": \"Whether the extension lines from grid cells to the rulers should be shown (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showPositiveLineNumbers\",\n                            \"description\": \"Show Positive line number labels (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showNegativeLineNumbers\",\n                            \"description\": \"Show Negative line number labels (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showAreaNames\",\n                            \"description\": \"Show area name labels (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showLineNames\",\n                            \"description\": \"Show line name labels (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showTrackSizes\",\n                            \"description\": \"Show track size labels (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"gridBorderColor\",\n                            \"description\": \"The grid container border highlight color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"cellBorderColor\",\n                            \"description\": \"The cell border color (default: transparent). Deprecated, please use rowLineColor and columnLineColor instead.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"rowLineColor\",\n                            \"description\": \"The row line color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"columnLineColor\",\n                            \"description\": \"The column line color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"gridBorderDash\",\n                            \"description\": \"Whether the grid border is dashed (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"cellBorderDash\",\n                            \"description\": \"Whether the cell border is dashed (default: false). Deprecated, please us rowLineDash and columnLineDash instead.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"rowLineDash\",\n                            \"description\": \"Whether row lines are dashed (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"columnLineDash\",\n                            \"description\": \"Whether column lines are dashed (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"rowGapColor\",\n                            \"description\": \"The row gap highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"rowHatchColor\",\n                            \"description\": \"The row gap hatching fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"columnGapColor\",\n                            \"description\": \"The column gap highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"columnHatchColor\",\n                            \"description\": \"The column gap hatching fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"areaBorderColor\",\n                            \"description\": \"The named grid areas border color (Default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"gridBackgroundColor\",\n                            \"description\": \"The grid container background color (Default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FlexContainerHighlightConfig\",\n                    \"description\": \"Configuration data for the highlighting of Flex container elements.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"containerBorder\",\n                            \"description\": \"The style of the container border\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"lineSeparator\",\n                            \"description\": \"The style of the separator between lines\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"itemSeparator\",\n                            \"description\": \"The style of the separator between items\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"mainDistributedSpace\",\n                            \"description\": \"Style of content-distribution space on the main axis (justify-content).\",\n                            \"optional\": true,\n                            \"$ref\": \"BoxStyle\"\n                        },\n                        {\n                            \"name\": \"crossDistributedSpace\",\n                            \"description\": \"Style of content-distribution space on the cross axis (align-content).\",\n                            \"optional\": true,\n                            \"$ref\": \"BoxStyle\"\n                        },\n                        {\n                            \"name\": \"rowGapSpace\",\n                            \"description\": \"Style of empty space caused by row gaps (gap/row-gap).\",\n                            \"optional\": true,\n                            \"$ref\": \"BoxStyle\"\n                        },\n                        {\n                            \"name\": \"columnGapSpace\",\n                            \"description\": \"Style of empty space caused by columns gaps (gap/column-gap).\",\n                            \"optional\": true,\n                            \"$ref\": \"BoxStyle\"\n                        },\n                        {\n                            \"name\": \"crossAlignment\",\n                            \"description\": \"Style of the self-alignment line (align-items).\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FlexItemHighlightConfig\",\n                    \"description\": \"Configuration data for the highlighting of Flex item elements.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"baseSizeBox\",\n                            \"description\": \"Style of the box representing the item's base size\",\n                            \"optional\": true,\n                            \"$ref\": \"BoxStyle\"\n                        },\n                        {\n                            \"name\": \"baseSizeBorder\",\n                            \"description\": \"Style of the border around the box representing the item's base size\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"flexibilityArrow\",\n                            \"description\": \"Style of the arrow representing if the item grew or shrank\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LineStyle\",\n                    \"description\": \"Style information for drawing a line.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"color\",\n                            \"description\": \"The color of the line (default: transparent)\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"pattern\",\n                            \"description\": \"The line pattern (default: solid)\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"dashed\",\n                                \"dotted\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BoxStyle\",\n                    \"description\": \"Style information for drawing a box.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"fillColor\",\n                            \"description\": \"The background color for the box (default: transparent)\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"hatchColor\",\n                            \"description\": \"The hatching color for the box (default: transparent)\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContrastAlgorithm\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"aa\",\n                        \"aaa\",\n                        \"apca\"\n                    ]\n                },\n                {\n                    \"id\": \"HighlightConfig\",\n                    \"description\": \"Configuration data for the highlighting of page elements.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"showInfo\",\n                            \"description\": \"Whether the node info tooltip should be shown (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showStyles\",\n                            \"description\": \"Whether the node styles in the tooltip (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showRulers\",\n                            \"description\": \"Whether the rulers should be shown (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showAccessibilityInfo\",\n                            \"description\": \"Whether the a11y info should be shown (default: true).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"showExtensionLines\",\n                            \"description\": \"Whether the extension lines from node to the rulers should be shown (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"contentColor\",\n                            \"description\": \"The content box highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"paddingColor\",\n                            \"description\": \"The padding highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"borderColor\",\n                            \"description\": \"The border highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"marginColor\",\n                            \"description\": \"The margin highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"eventTargetColor\",\n                            \"description\": \"The event target element highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"shapeColor\",\n                            \"description\": \"The shape outside fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"shapeMarginColor\",\n                            \"description\": \"The shape margin fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"cssGridColor\",\n                            \"description\": \"The grid layout color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"colorFormat\",\n                            \"description\": \"The color format used to format color styles (default: hex).\",\n                            \"optional\": true,\n                            \"$ref\": \"ColorFormat\"\n                        },\n                        {\n                            \"name\": \"gridHighlightConfig\",\n                            \"description\": \"The grid layout highlight configuration (default: all transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"GridHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"flexContainerHighlightConfig\",\n                            \"description\": \"The flex container highlight configuration (default: all transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"FlexContainerHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"flexItemHighlightConfig\",\n                            \"description\": \"The flex item highlight configuration (default: all transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"FlexItemHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"contrastAlgorithm\",\n                            \"description\": \"The contrast algorithm to use for the contrast ratio (default: aa).\",\n                            \"optional\": true,\n                            \"$ref\": \"ContrastAlgorithm\"\n                        },\n                        {\n                            \"name\": \"containerQueryContainerHighlightConfig\",\n                            \"description\": \"The container query container highlight configuration (default: all transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"ContainerQueryContainerHighlightConfig\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ColorFormat\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"rgb\",\n                        \"hsl\",\n                        \"hwb\",\n                        \"hex\"\n                    ]\n                },\n                {\n                    \"id\": \"GridNodeHighlightConfig\",\n                    \"description\": \"Configurations for Persistent Grid Highlight\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"gridHighlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance.\",\n                            \"$ref\": \"GridHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to highlight.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FlexNodeHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"flexContainerHighlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance of flex containers.\",\n                            \"$ref\": \"FlexContainerHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to highlight.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScrollSnapContainerHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"snapportBorder\",\n                            \"description\": \"The style of the snapport border (default: transparent)\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"snapAreaBorder\",\n                            \"description\": \"The style of the snap area border (default: transparent)\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"scrollMarginColor\",\n                            \"description\": \"The margin highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"scrollPaddingColor\",\n                            \"description\": \"The padding highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScrollSnapHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scrollSnapContainerHighlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance of scroll snap containers.\",\n                            \"$ref\": \"ScrollSnapContainerHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to highlight.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"HingeConfig\",\n                    \"description\": \"Configuration for dual screen hinge\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"rect\",\n                            \"description\": \"A rectangle represent hinge\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"contentColor\",\n                            \"description\": \"The content box highlight fill color (default: a dark color).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"outlineColor\",\n                            \"description\": \"The content box highlight outline color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WindowControlsOverlayConfig\",\n                    \"description\": \"Configuration for Window Controls Overlay\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"showCSS\",\n                            \"description\": \"Whether the title bar CSS should be shown when emulating the Window Controls Overlay.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"selectedPlatform\",\n                            \"description\": \"Selected platforms to show the overlay.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"themeColor\",\n                            \"description\": \"The theme color defined in app manifest.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContainerQueryHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"containerQueryContainerHighlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance of container query containers.\",\n                            \"$ref\": \"ContainerQueryContainerHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the container node to highlight.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ContainerQueryContainerHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"containerBorder\",\n                            \"description\": \"The style of the container border.\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        },\n                        {\n                            \"name\": \"descendantBorder\",\n                            \"description\": \"The style of the descendants' borders.\",\n                            \"optional\": true,\n                            \"$ref\": \"LineStyle\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"IsolatedElementHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"isolationModeHighlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance of an element in isolation mode.\",\n                            \"$ref\": \"IsolationModeHighlightConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the isolated element to highlight.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"IsolationModeHighlightConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"resizerColor\",\n                            \"description\": \"The fill color of the resizers (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"resizerHandleColor\",\n                            \"description\": \"The fill color for resizer handles (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"maskColor\",\n                            \"description\": \"The fill color for the mask covering non-isolated elements (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InspectMode\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"searchForNode\",\n                        \"searchForUAShadowDOM\",\n                        \"captureAreaScreenshot\",\n                        \"none\"\n                    ]\n                },\n                {\n                    \"id\": \"InspectedElementAnchorConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to highlight.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node to highlight.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables domain notifications.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables domain notifications.\"\n                },\n                {\n                    \"name\": \"getHighlightObjectForTest\",\n                    \"description\": \"For testing.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to get highlight object for.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"includeDistance\",\n                            \"description\": \"Whether to include distance info.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeStyle\",\n                            \"description\": \"Whether to include style info.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"colorFormat\",\n                            \"description\": \"The color format to get config with (default: hex).\",\n                            \"optional\": true,\n                            \"$ref\": \"ColorFormat\"\n                        },\n                        {\n                            \"name\": \"showAccessibilityInfo\",\n                            \"description\": \"Whether to show accessibility info (default: true).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"highlight\",\n                            \"description\": \"Highlight data for the node.\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getGridHighlightObjectsForTest\",\n                    \"description\": \"For Persistent Grid testing.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeIds\",\n                            \"description\": \"Ids of the node to get highlight object for.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOM.NodeId\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"highlights\",\n                            \"description\": \"Grid Highlight data for the node ids provided.\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSourceOrderHighlightObjectForTest\",\n                    \"description\": \"For Source Order Viewer testing.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the node to highlight.\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"highlight\",\n                            \"description\": \"Source order highlight data for the node id provided.\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"hideHighlight\",\n                    \"description\": \"Hides any highlight.\"\n                },\n                {\n                    \"name\": \"highlightFrame\",\n                    \"description\": \"Highlights owner element of the frame with given id.\\nDeprecated: Doesn't work reliably and cannot be fixed due to process\\nseparation (the owner node might be in a different process). Determine\\nthe owner node in the client and use highlightNode.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Identifier of the frame to highlight.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"contentColor\",\n                            \"description\": \"The content box highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"contentOutlineColor\",\n                            \"description\": \"The content box highlight outline color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"highlightNode\",\n                    \"description\": \"Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\\nobjectId must be specified.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"highlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance.\",\n                            \"$ref\": \"HighlightConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to highlight.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node to highlight.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node to be highlighted.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"selector\",\n                            \"description\": \"Selectors to highlight relevant nodes.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"highlightQuad\",\n                    \"description\": \"Highlights given quad. Coordinates are absolute with respect to the main frame viewport.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"quad\",\n                            \"description\": \"Quad to highlight\",\n                            \"$ref\": \"DOM.Quad\"\n                        },\n                        {\n                            \"name\": \"color\",\n                            \"description\": \"The highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"outlineColor\",\n                            \"description\": \"The highlight outline color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"highlightRect\",\n                    \"description\": \"Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.\\nIssue: the method does not handle device pixel ratio (DPR) correctly.\\nThe coordinates currently have to be adjusted by the client\\nif DPR is not 1 (see crbug.com/437807128).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X coordinate\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y coordinate\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Rectangle width\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Rectangle height\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"color\",\n                            \"description\": \"The highlight fill color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        },\n                        {\n                            \"name\": \"outlineColor\",\n                            \"description\": \"The highlight outline color (default: transparent).\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.RGBA\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"highlightSourceOrder\",\n                    \"description\": \"Highlights the source order of the children of the DOM node with given id or with the given\\nJavaScript object wrapper. Either nodeId or objectId must be specified.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sourceOrderConfig\",\n                            \"description\": \"A descriptor for the appearance of the overlay drawing.\",\n                            \"$ref\": \"SourceOrderConfig\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Identifier of the node to highlight.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.NodeId\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Identifier of the backend node to highlight.\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"JavaScript object id of the node to be highlighted.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInspectMode\",\n                    \"description\": \"Enters the 'inspect' mode. In this mode, elements that user is hovering over are highlighted.\\nBackend then generates 'inspectNodeRequested' event upon element selection.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"mode\",\n                            \"description\": \"Set an inspection mode.\",\n                            \"$ref\": \"InspectMode\"\n                        },\n                        {\n                            \"name\": \"highlightConfig\",\n                            \"description\": \"A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\\n== false`.\",\n                            \"optional\": true,\n                            \"$ref\": \"HighlightConfig\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowAdHighlights\",\n                    \"description\": \"Highlights owner element of all frames detected to be ads.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"description\": \"True for showing ad highlights\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPausedInDebuggerMessage\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"message\",\n                            \"description\": \"The message to display, also triggers resume and step over controls.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowDebugBorders\",\n                    \"description\": \"Requests that backend shows debug borders on layers\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"description\": \"True for showing debug borders\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowFPSCounter\",\n                    \"description\": \"Requests that backend shows the FPS counter\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"description\": \"True for showing the FPS counter\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowGridOverlays\",\n                    \"description\": \"Highlight multiple elements with the CSS Grid overlay.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"gridNodeHighlightConfigs\",\n                            \"description\": \"An array of node identifiers and descriptors for the highlight appearance.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"GridNodeHighlightConfig\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowFlexOverlays\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"flexNodeHighlightConfigs\",\n                            \"description\": \"An array of node identifiers and descriptors for the highlight appearance.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FlexNodeHighlightConfig\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowScrollSnapOverlays\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scrollSnapHighlightConfigs\",\n                            \"description\": \"An array of node identifiers and descriptors for the highlight appearance.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScrollSnapHighlightConfig\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowContainerQueryOverlays\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"containerQueryHighlightConfigs\",\n                            \"description\": \"An array of node identifiers and descriptors for the highlight appearance.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ContainerQueryHighlightConfig\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowInspectedElementAnchor\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"inspectedElementAnchorConfig\",\n                            \"description\": \"Node identifier for which to show an anchor for.\",\n                            \"$ref\": \"InspectedElementAnchorConfig\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowPaintRects\",\n                    \"description\": \"Requests that backend shows paint rectangles\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"True for showing paint rectangles\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowLayoutShiftRegions\",\n                    \"description\": \"Requests that backend shows layout shift regions\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"True for showing layout shift regions\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowScrollBottleneckRects\",\n                    \"description\": \"Requests that backend shows scroll bottleneck rects\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"description\": \"True for showing scroll bottleneck rects\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowHitTestBorders\",\n                    \"description\": \"Deprecated, no longer has any effect.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"description\": \"True for showing hit-test borders\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowWebVitals\",\n                    \"description\": \"Deprecated, no longer has any effect.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowViewportSizeOnResize\",\n                    \"description\": \"Paints viewport size upon main frame resize.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"show\",\n                            \"description\": \"Whether to paint size or not.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowHinge\",\n                    \"description\": \"Add a dual screen device hinge\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"hingeConfig\",\n                            \"description\": \"hinge data, null means hideHinge\",\n                            \"optional\": true,\n                            \"$ref\": \"HingeConfig\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowIsolatedElements\",\n                    \"description\": \"Show elements in isolation mode with overlays.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"isolatedElementHighlightConfigs\",\n                            \"description\": \"An array of node identifiers and descriptors for the highlight appearance.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"IsolatedElementHighlightConfig\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setShowWindowControlsOverlay\",\n                    \"description\": \"Show Window Controls Overlay for PWA\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"windowControlsOverlayConfig\",\n                            \"description\": \"Window Controls Overlay data, null means hide Window Controls Overlay\",\n                            \"optional\": true,\n                            \"$ref\": \"WindowControlsOverlayConfig\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"inspectNodeRequested\",\n                    \"description\": \"Fired when the node should be inspected. This happens after call to `setInspectMode` or when\\nuser manually inspects an element.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Id of the node to inspect.\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nodeHighlightRequested\",\n                    \"description\": \"Fired when the node should be highlighted. This happens after call to `setInspectMode`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"DOM.NodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"screenshotRequested\",\n                    \"description\": \"Fired when user asks to capture screenshot of some area on the page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"viewport\",\n                            \"description\": \"Viewport to capture, in device independent pixels (dip).\",\n                            \"$ref\": \"Page.Viewport\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"inspectPanelShowRequested\",\n                    \"description\": \"Fired when user asks to show the Inspect panel.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Id of the node to show in the panel.\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"inspectedElementWindowRestored\",\n                    \"description\": \"Fired when user asks to restore the Inspected Element floating window.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Id of the node to restore the floating window for.\",\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"inspectModeCanceled\",\n                    \"description\": \"Fired when user cancels the inspect mode.\"\n                }\n            ]\n        },\n        {\n            \"domain\": \"PWA\",\n            \"description\": \"This domain allows interacting with the browser to control PWAs.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"FileHandlerAccept\",\n                    \"description\": \"The following types are the replica of\\nhttps://crsrc.org/c/chrome/browser/web_applications/proto/web_app_os_integration_state.proto;drc=9910d3be894c8f142c977ba1023f30a656bc13fc;l=67\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"mediaType\",\n                            \"description\": \"New name of the mimetype according to\\nhttps://www.iana.org/assignments/media-types/media-types.xhtml\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fileExtensions\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FileHandler\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"action\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"accepts\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FileHandlerAccept\"\n                            }\n                        },\n                        {\n                            \"name\": \"displayName\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DisplayMode\",\n                    \"description\": \"If user prefers opening the app in browser or an app window.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"standalone\",\n                        \"browser\"\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getOsAppState\",\n                    \"description\": \"Returns the following OS state for the given manifest id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"description\": \"The id from the webapp's manifest file, commonly it's the url of the\\nsite installing the webapp. See\\nhttps://web.dev/learn/pwa/web-app-manifest.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"badgeCount\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"fileHandlers\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FileHandler\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"install\",\n                    \"description\": \"Installs the given manifest identity, optionally using the given installUrlOrBundleUrl\\n\\nIWA-specific install description:\\nmanifestId corresponds to isolated-app:// + web_package::SignedWebBundleId\\n\\nFile installation mode:\\nThe installUrlOrBundleUrl can be either file:// or http(s):// pointing\\nto a signed web bundle (.swbn). In this case SignedWebBundleId must correspond to\\nThe .swbn file's signing key.\\n\\nDev proxy installation mode:\\ninstallUrlOrBundleUrl must be http(s):// that serves dev mode IWA.\\nweb_package::SignedWebBundleId must be of type dev proxy.\\n\\nThe advantage of dev proxy mode is that all changes to IWA\\nautomatically will be reflected in the running app without\\nreinstallation.\\n\\nTo generate bundle id for proxy mode:\\n1. Generate 32 random bytes.\\n2. Add a specific suffix at the end following the documentation\\n   https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md#suffix\\n3. Encode the entire sequence using Base32 without padding.\\n\\nIf Chrome is not in IWA dev\\nmode, the installation will fail, regardless of the state of the allowlist.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"installUrlOrBundleUrl\",\n                            \"description\": \"The location of the app or bundle overriding the one derived from the\\nmanifestId.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"uninstall\",\n                    \"description\": \"Uninstalls the given manifest_id and closes any opened app windows.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"launch\",\n                    \"description\": \"Launches the installed web app, or an url in the same web app instead of the\\ndefault start url if it is provided. Returns a page Target.TargetID which\\ncan be used to attach to via Target.attachToTarget or similar APIs.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"ID of the tab target created as a result.\",\n                            \"$ref\": \"Target.TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"launchFilesInApp\",\n                    \"description\": \"Opens one or more local files from an installed web app identified by its\\nmanifestId. The web app needs to have file handlers registered to process\\nthe files. The API returns one or more page Target.TargetIDs which can be\\nused to attach to via Target.attachToTarget or similar APIs.\\nIf some files in the parameters cannot be handled by the web app, they will\\nbe ignored. If none of the files can be handled, this API returns an error.\\nIf no files are provided as the parameter, this API also returns an error.\\n\\nAccording to the definition of the file handlers in the manifest file, one\\nTarget.TargetID may represent a page handling one or more files. The order\\nof the returned Target.TargetIDs is not guaranteed.\\n\\nTODO(crbug.com/339454034): Check the existences of the input files.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"files\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetIds\",\n                            \"description\": \"IDs of the tab targets created as the result.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Target.TargetID\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"openCurrentPageInApp\",\n                    \"description\": \"Opens the current page in its web app identified by the manifest id, needs\\nto be called on a page target. This function returns immediately without\\nwaiting for the app to finish loading.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"changeAppUserSettings\",\n                    \"description\": \"Changes user settings of the web app identified by its manifestId. If the\\napp was not installed, this command returns an error. Unset parameters will\\nbe ignored; unrecognized values will cause an error.\\n\\nUnlike the ones defined in the manifest files of the web apps, these\\nsettings are provided by the browser and controlled by the users, they\\nimpact the way the browser handling the web apps.\\n\\nSee the comment of each parameter.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"linkCapturing\",\n                            \"description\": \"If user allows the links clicked on by the user in the app's scope, or\\nextended scope if the manifest has scope extensions and the flags\\n`DesktopPWAsLinkCapturingWithScopeExtensions` and\\n`WebAppEnableScopeExtensions` are enabled.\\n\\nNote, the API does not support resetting the linkCapturing to the\\ninitial value, uninstalling and installing the web app again will reset\\nit.\\n\\nTODO(crbug.com/339453269): Setting this value on ChromeOS is not\\nsupported yet.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"displayMode\",\n                            \"optional\": true,\n                            \"$ref\": \"DisplayMode\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Page\",\n            \"description\": \"Actions and events related to the inspected page belong to the page domain.\",\n            \"dependencies\": [\n                \"Debugger\",\n                \"DOM\",\n                \"IO\",\n                \"Network\",\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"FrameId\",\n                    \"description\": \"Unique frame identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"AdFrameType\",\n                    \"description\": \"Indicates whether a frame has been identified as an ad.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"none\",\n                        \"child\",\n                        \"root\"\n                    ]\n                },\n                {\n                    \"id\": \"AdFrameExplanation\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ParentIsAd\",\n                        \"CreatedByAdScript\",\n                        \"MatchedBlockingRule\"\n                    ]\n                },\n                {\n                    \"id\": \"AdFrameStatus\",\n                    \"description\": \"Indicates whether a frame has been identified as an ad and why.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"adFrameType\",\n                            \"$ref\": \"AdFrameType\"\n                        },\n                        {\n                            \"name\": \"explanations\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AdFrameExplanation\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AdScriptId\",\n                    \"description\": \"Identifies the script which caused a script or frame to be labelled as an\\nad.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Script Id of the script which caused a script or frame to be labelled as\\nan ad.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"debuggerId\",\n                            \"description\": \"Id of scriptId's debugger.\",\n                            \"$ref\": \"Runtime.UniqueDebuggerId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AdScriptAncestry\",\n                    \"description\": \"Encapsulates the script ancestry and the root script filterlist rule that\\ncaused the frame to be labelled as an ad. Only created when `ancestryChain`\\nis not empty.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"ancestryChain\",\n                            \"description\": \"A chain of `AdScriptId`s representing the ancestry of an ad script that\\nled to the creation of a frame. The chain is ordered from the script\\nitself (lower level) up to its root ancestor that was flagged by\\nfilterlist.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AdScriptId\"\n                            }\n                        },\n                        {\n                            \"name\": \"rootScriptFilterlistRule\",\n                            \"description\": \"The filterlist rule that caused the root (last) script in\\n`ancestryChain` to be ad-tagged. Only populated if the rule is\\navailable.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SecureContextType\",\n                    \"description\": \"Indicates whether the frame is a secure context and why it is the case.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Secure\",\n                        \"SecureLocalhost\",\n                        \"InsecureScheme\",\n                        \"InsecureAncestor\"\n                    ]\n                },\n                {\n                    \"id\": \"CrossOriginIsolatedContextType\",\n                    \"description\": \"Indicates whether the frame is cross-origin isolated and why it is the case.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Isolated\",\n                        \"NotIsolated\",\n                        \"NotIsolatedFeatureDisabled\"\n                    ]\n                },\n                {\n                    \"id\": \"GatedAPIFeatures\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SharedArrayBuffers\",\n                        \"SharedArrayBuffersTransferAllowed\",\n                        \"PerformanceMeasureMemory\",\n                        \"PerformanceProfile\"\n                    ]\n                },\n                {\n                    \"id\": \"PermissionsPolicyFeature\",\n                    \"description\": \"All Permissions Policy features. This enum should match the one defined\\nin services/network/public/cpp/permissions_policy/permissions_policy_features.json5.\\nLINT.IfChange(PermissionsPolicyFeature)\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"accelerometer\",\n                        \"all-screens-capture\",\n                        \"ambient-light-sensor\",\n                        \"aria-notify\",\n                        \"attribution-reporting\",\n                        \"autofill\",\n                        \"autoplay\",\n                        \"bluetooth\",\n                        \"browsing-topics\",\n                        \"camera\",\n                        \"captured-surface-control\",\n                        \"ch-dpr\",\n                        \"ch-device-memory\",\n                        \"ch-downlink\",\n                        \"ch-ect\",\n                        \"ch-prefers-color-scheme\",\n                        \"ch-prefers-reduced-motion\",\n                        \"ch-prefers-reduced-transparency\",\n                        \"ch-rtt\",\n                        \"ch-save-data\",\n                        \"ch-ua\",\n                        \"ch-ua-arch\",\n                        \"ch-ua-bitness\",\n                        \"ch-ua-high-entropy-values\",\n                        \"ch-ua-platform\",\n                        \"ch-ua-model\",\n                        \"ch-ua-mobile\",\n                        \"ch-ua-form-factors\",\n                        \"ch-ua-full-version\",\n                        \"ch-ua-full-version-list\",\n                        \"ch-ua-platform-version\",\n                        \"ch-ua-wow64\",\n                        \"ch-viewport-height\",\n                        \"ch-viewport-width\",\n                        \"ch-width\",\n                        \"clipboard-read\",\n                        \"clipboard-write\",\n                        \"compute-pressure\",\n                        \"controlled-frame\",\n                        \"cross-origin-isolated\",\n                        \"deferred-fetch\",\n                        \"deferred-fetch-minimal\",\n                        \"device-attributes\",\n                        \"digital-credentials-create\",\n                        \"digital-credentials-get\",\n                        \"direct-sockets\",\n                        \"direct-sockets-multicast\",\n                        \"direct-sockets-private\",\n                        \"display-capture\",\n                        \"document-domain\",\n                        \"encrypted-media\",\n                        \"execution-while-out-of-viewport\",\n                        \"execution-while-not-rendered\",\n                        \"fenced-unpartitioned-storage-read\",\n                        \"focus-without-user-activation\",\n                        \"fullscreen\",\n                        \"frobulate\",\n                        \"gamepad\",\n                        \"geolocation\",\n                        \"gyroscope\",\n                        \"hid\",\n                        \"identity-credentials-get\",\n                        \"idle-detection\",\n                        \"interest-cohort\",\n                        \"join-ad-interest-group\",\n                        \"keyboard-map\",\n                        \"language-detector\",\n                        \"language-model\",\n                        \"local-fonts\",\n                        \"local-network\",\n                        \"local-network-access\",\n                        \"loopback-network\",\n                        \"magnetometer\",\n                        \"manual-text\",\n                        \"media-playback-while-not-visible\",\n                        \"microphone\",\n                        \"midi\",\n                        \"on-device-speech-recognition\",\n                        \"otp-credentials\",\n                        \"payment\",\n                        \"picture-in-picture\",\n                        \"private-aggregation\",\n                        \"private-state-token-issuance\",\n                        \"private-state-token-redemption\",\n                        \"publickey-credentials-create\",\n                        \"publickey-credentials-get\",\n                        \"record-ad-auction-events\",\n                        \"rewriter\",\n                        \"run-ad-auction\",\n                        \"screen-wake-lock\",\n                        \"serial\",\n                        \"shared-storage\",\n                        \"shared-storage-select-url\",\n                        \"smart-card\",\n                        \"speaker-selection\",\n                        \"storage-access\",\n                        \"sub-apps\",\n                        \"summarizer\",\n                        \"sync-xhr\",\n                        \"translator\",\n                        \"unload\",\n                        \"usb\",\n                        \"usb-unrestricted\",\n                        \"vertical-scroll\",\n                        \"web-app-installation\",\n                        \"web-printing\",\n                        \"web-share\",\n                        \"window-management\",\n                        \"writer\",\n                        \"xr-spatial-tracking\"\n                    ]\n                },\n                {\n                    \"id\": \"PermissionsPolicyBlockReason\",\n                    \"description\": \"Reason for a permissions policy feature to be disabled.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Header\",\n                        \"IframeAttribute\",\n                        \"InFencedFrameTree\",\n                        \"InIsolatedApp\"\n                    ]\n                },\n                {\n                    \"id\": \"PermissionsPolicyBlockLocator\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"blockReason\",\n                            \"$ref\": \"PermissionsPolicyBlockReason\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PermissionsPolicyFeatureState\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"feature\",\n                            \"$ref\": \"PermissionsPolicyFeature\"\n                        },\n                        {\n                            \"name\": \"allowed\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"locator\",\n                            \"optional\": true,\n                            \"$ref\": \"PermissionsPolicyBlockLocator\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"OriginTrialTokenStatus\",\n                    \"description\": \"Origin Trial(https://www.chromium.org/blink/origin-trials) support.\\nStatus for an Origin Trial token.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Success\",\n                        \"NotSupported\",\n                        \"Insecure\",\n                        \"Expired\",\n                        \"WrongOrigin\",\n                        \"InvalidSignature\",\n                        \"Malformed\",\n                        \"WrongVersion\",\n                        \"FeatureDisabled\",\n                        \"TokenDisabled\",\n                        \"FeatureDisabledForUser\",\n                        \"UnknownTrial\"\n                    ]\n                },\n                {\n                    \"id\": \"OriginTrialStatus\",\n                    \"description\": \"Status for an Origin Trial.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Enabled\",\n                        \"ValidTokenNotProvided\",\n                        \"OSNotSupported\",\n                        \"TrialNotAllowed\"\n                    ]\n                },\n                {\n                    \"id\": \"OriginTrialUsageRestriction\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"None\",\n                        \"Subset\"\n                    ]\n                },\n                {\n                    \"id\": \"OriginTrialToken\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"origin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"matchSubDomains\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"trialName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"expiryTime\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"isThirdParty\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"usageRestriction\",\n                            \"$ref\": \"OriginTrialUsageRestriction\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"OriginTrialTokenWithStatus\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"rawTokenText\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"parsedToken\",\n                            \"description\": \"`parsedToken` is present only when the token is extractable and\\nparsable.\",\n                            \"optional\": true,\n                            \"$ref\": \"OriginTrialToken\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"OriginTrialTokenStatus\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"OriginTrial\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"trialName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"OriginTrialStatus\"\n                        },\n                        {\n                            \"name\": \"tokensWithStatus\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"OriginTrialTokenWithStatus\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SecurityOriginDetails\",\n                    \"description\": \"Additional information about the frame document's security origin.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"isLocalhost\",\n                            \"description\": \"Indicates whether the frame document's security origin is one\\nof the local hostnames (e.g. \\\"localhost\\\") or IP addresses (IPv4\\n127.0.0.0/8 or IPv6 ::1).\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Frame\",\n                    \"description\": \"Information about the Frame on the page.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Frame unique identifier.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"Parent frame identifier.\",\n                            \"optional\": true,\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Identifier of the loader associated with this frame.\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Frame's name as specified in the tag.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Frame document's URL without fragment.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"urlFragment\",\n                            \"description\": \"Frame document's URL fragment including the '#'.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"domainAndRegistry\",\n                            \"description\": \"Frame document's registered domain, taking the public suffixes list into account.\\nExtracted from the Frame's url.\\nExample URLs: http://www.google.com/file.html -> \\\"google.com\\\"\\n              http://a.b.co.uk/file.html      -> \\\"b.co.uk\\\"\",\n                            \"experimental\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"securityOrigin\",\n                            \"description\": \"Frame document's security origin.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"securityOriginDetails\",\n                            \"description\": \"Additional details about the frame document's security origin.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"SecurityOriginDetails\"\n                        },\n                        {\n                            \"name\": \"mimeType\",\n                            \"description\": \"Frame document's mimeType as determined by the browser.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"unreachableUrl\",\n                            \"description\": \"If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"adFrameStatus\",\n                            \"description\": \"Indicates whether this frame was tagged as an ad and why.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"AdFrameStatus\"\n                        },\n                        {\n                            \"name\": \"secureContextType\",\n                            \"description\": \"Indicates whether the main document is a secure context and explains why that is the case.\",\n                            \"experimental\": true,\n                            \"$ref\": \"SecureContextType\"\n                        },\n                        {\n                            \"name\": \"crossOriginIsolatedContextType\",\n                            \"description\": \"Indicates whether this is a cross origin isolated context.\",\n                            \"experimental\": true,\n                            \"$ref\": \"CrossOriginIsolatedContextType\"\n                        },\n                        {\n                            \"name\": \"gatedAPIFeatures\",\n                            \"description\": \"Indicated which gated APIs / features are available.\",\n                            \"experimental\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"GatedAPIFeatures\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FrameResource\",\n                    \"description\": \"Information about the Resource on the page.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Resource URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of this resource.\",\n                            \"$ref\": \"Network.ResourceType\"\n                        },\n                        {\n                            \"name\": \"mimeType\",\n                            \"description\": \"Resource mimeType as determined by the browser.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lastModified\",\n                            \"description\": \"last-modified timestamp as reported by server.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"contentSize\",\n                            \"description\": \"Resource content size.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"failed\",\n                            \"description\": \"True if the resource failed to load.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"canceled\",\n                            \"description\": \"True if the resource was canceled during loading.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FrameResourceTree\",\n                    \"description\": \"Information about the Frame hierarchy along with their cached resources.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"frame\",\n                            \"description\": \"Frame information for this tree item.\",\n                            \"$ref\": \"Frame\"\n                        },\n                        {\n                            \"name\": \"childFrames\",\n                            \"description\": \"Child frames.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FrameResourceTree\"\n                            }\n                        },\n                        {\n                            \"name\": \"resources\",\n                            \"description\": \"Information about frame resources.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FrameResource\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FrameTree\",\n                    \"description\": \"Information about the Frame hierarchy.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"frame\",\n                            \"description\": \"Frame information for this tree item.\",\n                            \"$ref\": \"Frame\"\n                        },\n                        {\n                            \"name\": \"childFrames\",\n                            \"description\": \"Child frames.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FrameTree\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScriptIdentifier\",\n                    \"description\": \"Unique script identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"TransitionType\",\n                    \"description\": \"Transition type.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"link\",\n                        \"typed\",\n                        \"address_bar\",\n                        \"auto_bookmark\",\n                        \"auto_subframe\",\n                        \"manual_subframe\",\n                        \"generated\",\n                        \"auto_toplevel\",\n                        \"form_submit\",\n                        \"reload\",\n                        \"keyword\",\n                        \"keyword_generated\",\n                        \"other\"\n                    ]\n                },\n                {\n                    \"id\": \"NavigationEntry\",\n                    \"description\": \"Navigation history entry.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Unique id of the navigation history entry.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the navigation history entry.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"userTypedURL\",\n                            \"description\": \"URL that the user typed in the url bar.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Title of the navigation history entry.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"transitionType\",\n                            \"description\": \"Transition type.\",\n                            \"$ref\": \"TransitionType\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScreencastFrameMetadata\",\n                    \"description\": \"Screencast frame metadata.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"offsetTop\",\n                            \"description\": \"Top offset in DIP.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pageScaleFactor\",\n                            \"description\": \"Page scale factor.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"deviceWidth\",\n                            \"description\": \"Device screen width in DIP.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"deviceHeight\",\n                            \"description\": \"Device screen height in DIP.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scrollOffsetX\",\n                            \"description\": \"Position of horizontal scroll in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scrollOffsetY\",\n                            \"description\": \"Position of vertical scroll in CSS pixels.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Frame swap timestamp.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DialogType\",\n                    \"description\": \"Javascript dialog type.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"alert\",\n                        \"confirm\",\n                        \"prompt\",\n                        \"beforeunload\"\n                    ]\n                },\n                {\n                    \"id\": \"AppManifestError\",\n                    \"description\": \"Error while paring app manifest.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"message\",\n                            \"description\": \"Error message.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"critical\",\n                            \"description\": \"If critical, this is a non-recoverable parse error.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"line\",\n                            \"description\": \"Error line.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"column\",\n                            \"description\": \"Error column.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AppManifestParsedProperties\",\n                    \"description\": \"Parsed app manifest properties.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scope\",\n                            \"description\": \"Computed scope value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LayoutViewport\",\n                    \"description\": \"Layout viewport position and dimensions.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"pageX\",\n                            \"description\": \"Horizontal offset relative to the document (CSS pixels).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"pageY\",\n                            \"description\": \"Vertical offset relative to the document (CSS pixels).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"clientWidth\",\n                            \"description\": \"Width (CSS pixels), excludes scrollbar if present.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"clientHeight\",\n                            \"description\": \"Height (CSS pixels), excludes scrollbar if present.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"VisualViewport\",\n                    \"description\": \"Visual viewport position, dimensions, and scale.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"offsetX\",\n                            \"description\": \"Horizontal offset relative to the layout viewport (CSS pixels).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"offsetY\",\n                            \"description\": \"Vertical offset relative to the layout viewport (CSS pixels).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pageX\",\n                            \"description\": \"Horizontal offset relative to the document (CSS pixels).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pageY\",\n                            \"description\": \"Vertical offset relative to the document (CSS pixels).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"clientWidth\",\n                            \"description\": \"Width (CSS pixels), excludes scrollbar if present.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"clientHeight\",\n                            \"description\": \"Height (CSS pixels), excludes scrollbar if present.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scale\",\n                            \"description\": \"Scale relative to the ideal viewport (size at width=device-width).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"zoom\",\n                            \"description\": \"Page zoom factor (CSS to device independent pixels ratio).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Viewport\",\n                    \"description\": \"Viewport for capturing screenshot.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"x\",\n                            \"description\": \"X offset in device independent pixels (dip).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"y\",\n                            \"description\": \"Y offset in device independent pixels (dip).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Rectangle width in device independent pixels (dip).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Rectangle height in device independent pixels (dip).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scale\",\n                            \"description\": \"Page scale factor.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FontFamilies\",\n                    \"description\": \"Generic font families collection.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"standard\",\n                            \"description\": \"The standard font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fixed\",\n                            \"description\": \"The fixed font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"serif\",\n                            \"description\": \"The serif font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sansSerif\",\n                            \"description\": \"The sansSerif font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cursive\",\n                            \"description\": \"The cursive font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fantasy\",\n                            \"description\": \"The fantasy font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"math\",\n                            \"description\": \"The math font-family.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScriptFontFamilies\",\n                    \"description\": \"Font families collection for a script.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"script\",\n                            \"description\": \"Name of the script which these font families are defined for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"fontFamilies\",\n                            \"description\": \"Generic font families collection for the script.\",\n                            \"$ref\": \"FontFamilies\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FontSizes\",\n                    \"description\": \"Default font sizes.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"standard\",\n                            \"description\": \"Default standard font size.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"fixed\",\n                            \"description\": \"Default fixed font size.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ClientNavigationReason\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"anchorClick\",\n                        \"formSubmissionGet\",\n                        \"formSubmissionPost\",\n                        \"httpHeaderRefresh\",\n                        \"initialFrameNavigation\",\n                        \"metaTagRefresh\",\n                        \"other\",\n                        \"pageBlockInterstitial\",\n                        \"reload\",\n                        \"scriptInitiated\"\n                    ]\n                },\n                {\n                    \"id\": \"ClientNavigationDisposition\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"currentTab\",\n                        \"newTab\",\n                        \"newWindow\",\n                        \"download\"\n                    ]\n                },\n                {\n                    \"id\": \"InstallabilityErrorArgument\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Argument name (e.g. name:'minimum-icon-size-in-pixels').\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Argument value (e.g. value:'64').\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InstallabilityError\",\n                    \"description\": \"The installability error\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"errorId\",\n                            \"description\": \"The error id (e.g. 'manifest-missing-suitable-icon').\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"errorArguments\",\n                            \"description\": \"The list of error arguments (e.g. {name:'minimum-icon-size-in-pixels', value:'64'}).\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InstallabilityErrorArgument\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ReferrerPolicy\",\n                    \"description\": \"The referring-policy used for the navigation.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"noReferrer\",\n                        \"noReferrerWhenDowngrade\",\n                        \"origin\",\n                        \"originWhenCrossOrigin\",\n                        \"sameOrigin\",\n                        \"strictOrigin\",\n                        \"strictOriginWhenCrossOrigin\",\n                        \"unsafeUrl\"\n                    ]\n                },\n                {\n                    \"id\": \"CompilationCacheParams\",\n                    \"description\": \"Per-script compilation cache parameters for `Page.produceCompilationCache`\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The URL of the script to produce a compilation cache entry for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"eager\",\n                            \"description\": \"A hint to the backend whether eager compilation is recommended.\\n(the actual compilation mode used is upon backend discretion).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FileFilter\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"accepts\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FileHandler\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"action\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"icons\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ImageResource\"\n                            }\n                        },\n                        {\n                            \"name\": \"accepts\",\n                            \"description\": \"Mimic a map, name is the key, accepts is the value.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FileFilter\"\n                            }\n                        },\n                        {\n                            \"name\": \"launchType\",\n                            \"description\": \"Won't repeat the enums, using string for easy comparison. Same as the\\nother enums below.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ImageResource\",\n                    \"description\": \"The image definition used in both icon and screenshot.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The src field in the definition, but changing to url in favor of\\nconsistency.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sizes\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LaunchHandler\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"clientMode\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ProtocolHandler\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"protocol\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RelatedApplication\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScopeExtension\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Instead of using tuple, this field always returns the serialized string\\nfor easy understanding and comparison.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"hasOriginWildcard\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Screenshot\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"image\",\n                            \"$ref\": \"ImageResource\"\n                        },\n                        {\n                            \"name\": \"formFactor\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"label\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ShareTarget\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"action\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"method\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"enctype\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Embed the ShareTargetParams\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"files\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FileFilter\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Shortcut\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WebAppManifest\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"backgroundColor\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"description\",\n                            \"description\": \"The extra description provided by the manifest.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"dir\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"display\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"displayOverrides\",\n                            \"description\": \"The overrided display mode controlled by the user.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"fileHandlers\",\n                            \"description\": \"The handlers to open files.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FileHandler\"\n                            }\n                        },\n                        {\n                            \"name\": \"icons\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ImageResource\"\n                            }\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lang\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"launchHandler\",\n                            \"description\": \"TODO(crbug.com/1231886): This field is non-standard and part of a Chrome\\nexperiment. See:\\nhttps://github.com/WICG/web-app-launch/blob/main/launch_handler.md\",\n                            \"optional\": true,\n                            \"$ref\": \"LaunchHandler\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"orientation\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"preferRelatedApplications\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"protocolHandlers\",\n                            \"description\": \"The handlers to open protocols.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ProtocolHandler\"\n                            }\n                        },\n                        {\n                            \"name\": \"relatedApplications\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RelatedApplication\"\n                            }\n                        },\n                        {\n                            \"name\": \"scope\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scopeExtensions\",\n                            \"description\": \"Non-standard, see\\nhttps://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-explainer.md\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScopeExtension\"\n                            }\n                        },\n                        {\n                            \"name\": \"screenshots\",\n                            \"description\": \"The screenshots used by chromium.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Screenshot\"\n                            }\n                        },\n                        {\n                            \"name\": \"shareTarget\",\n                            \"optional\": true,\n                            \"$ref\": \"ShareTarget\"\n                        },\n                        {\n                            \"name\": \"shortName\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"shortcuts\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Shortcut\"\n                            }\n                        },\n                        {\n                            \"name\": \"startUrl\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"themeColor\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"NavigationType\",\n                    \"description\": \"The type of a frameNavigated event.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Navigation\",\n                        \"BackForwardCacheRestore\"\n                    ]\n                },\n                {\n                    \"id\": \"BackForwardCacheNotRestoredReason\",\n                    \"description\": \"List of not restored reasons for back-forward cache.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"NotPrimaryMainFrame\",\n                        \"BackForwardCacheDisabled\",\n                        \"RelatedActiveContentsExist\",\n                        \"HTTPStatusNotOK\",\n                        \"SchemeNotHTTPOrHTTPS\",\n                        \"Loading\",\n                        \"WasGrantedMediaAccess\",\n                        \"DisableForRenderFrameHostCalled\",\n                        \"DomainNotAllowed\",\n                        \"HTTPMethodNotGET\",\n                        \"SubframeIsNavigating\",\n                        \"Timeout\",\n                        \"CacheLimit\",\n                        \"JavaScriptExecution\",\n                        \"RendererProcessKilled\",\n                        \"RendererProcessCrashed\",\n                        \"SchedulerTrackedFeatureUsed\",\n                        \"ConflictingBrowsingInstance\",\n                        \"CacheFlushed\",\n                        \"ServiceWorkerVersionActivation\",\n                        \"SessionRestored\",\n                        \"ServiceWorkerPostMessage\",\n                        \"EnteredBackForwardCacheBeforeServiceWorkerHostAdded\",\n                        \"RenderFrameHostReused_SameSite\",\n                        \"RenderFrameHostReused_CrossSite\",\n                        \"ServiceWorkerClaim\",\n                        \"IgnoreEventAndEvict\",\n                        \"HaveInnerContents\",\n                        \"TimeoutPuttingInCache\",\n                        \"BackForwardCacheDisabledByLowMemory\",\n                        \"BackForwardCacheDisabledByCommandLine\",\n                        \"NetworkRequestDatapipeDrainedAsBytesConsumer\",\n                        \"NetworkRequestRedirected\",\n                        \"NetworkRequestTimeout\",\n                        \"NetworkExceedsBufferLimit\",\n                        \"NavigationCancelledWhileRestoring\",\n                        \"NotMostRecentNavigationEntry\",\n                        \"BackForwardCacheDisabledForPrerender\",\n                        \"UserAgentOverrideDiffers\",\n                        \"ForegroundCacheLimit\",\n                        \"BrowsingInstanceNotSwapped\",\n                        \"BackForwardCacheDisabledForDelegate\",\n                        \"UnloadHandlerExistsInMainFrame\",\n                        \"UnloadHandlerExistsInSubFrame\",\n                        \"ServiceWorkerUnregistration\",\n                        \"CacheControlNoStore\",\n                        \"CacheControlNoStoreCookieModified\",\n                        \"CacheControlNoStoreHTTPOnlyCookieModified\",\n                        \"NoResponseHead\",\n                        \"Unknown\",\n                        \"ActivationNavigationsDisallowedForBug1234857\",\n                        \"ErrorDocument\",\n                        \"FencedFramesEmbedder\",\n                        \"CookieDisabled\",\n                        \"HTTPAuthRequired\",\n                        \"CookieFlushed\",\n                        \"BroadcastChannelOnMessage\",\n                        \"WebViewSettingsChanged\",\n                        \"WebViewJavaScriptObjectChanged\",\n                        \"WebViewMessageListenerInjected\",\n                        \"WebViewSafeBrowsingAllowlistChanged\",\n                        \"WebViewDocumentStartJavascriptChanged\",\n                        \"WebSocket\",\n                        \"WebTransport\",\n                        \"WebRTC\",\n                        \"MainResourceHasCacheControlNoStore\",\n                        \"MainResourceHasCacheControlNoCache\",\n                        \"SubresourceHasCacheControlNoStore\",\n                        \"SubresourceHasCacheControlNoCache\",\n                        \"ContainsPlugins\",\n                        \"DocumentLoaded\",\n                        \"OutstandingNetworkRequestOthers\",\n                        \"RequestedMIDIPermission\",\n                        \"RequestedAudioCapturePermission\",\n                        \"RequestedVideoCapturePermission\",\n                        \"RequestedBackForwardCacheBlockedSensors\",\n                        \"RequestedBackgroundWorkPermission\",\n                        \"BroadcastChannel\",\n                        \"WebXR\",\n                        \"SharedWorker\",\n                        \"SharedWorkerMessage\",\n                        \"SharedWorkerWithNoActiveClient\",\n                        \"WebLocks\",\n                        \"WebLocksContention\",\n                        \"WebHID\",\n                        \"WebBluetooth\",\n                        \"WebShare\",\n                        \"RequestedStorageAccessGrant\",\n                        \"WebNfc\",\n                        \"OutstandingNetworkRequestFetch\",\n                        \"OutstandingNetworkRequestXHR\",\n                        \"AppBanner\",\n                        \"Printing\",\n                        \"WebDatabase\",\n                        \"PictureInPicture\",\n                        \"SpeechRecognizer\",\n                        \"IdleManager\",\n                        \"PaymentManager\",\n                        \"SpeechSynthesis\",\n                        \"KeyboardLock\",\n                        \"WebOTPService\",\n                        \"OutstandingNetworkRequestDirectSocket\",\n                        \"InjectedJavascript\",\n                        \"InjectedStyleSheet\",\n                        \"KeepaliveRequest\",\n                        \"IndexedDBEvent\",\n                        \"Dummy\",\n                        \"JsNetworkRequestReceivedCacheControlNoStoreResource\",\n                        \"WebRTCUsedWithCCNS\",\n                        \"WebTransportUsedWithCCNS\",\n                        \"WebSocketUsedWithCCNS\",\n                        \"SmartCard\",\n                        \"LiveMediaStreamTrack\",\n                        \"UnloadHandler\",\n                        \"ParserAborted\",\n                        \"ContentSecurityHandler\",\n                        \"ContentWebAuthenticationAPI\",\n                        \"ContentFileChooser\",\n                        \"ContentSerial\",\n                        \"ContentFileSystemAccess\",\n                        \"ContentMediaDevicesDispatcherHost\",\n                        \"ContentWebBluetooth\",\n                        \"ContentWebUSB\",\n                        \"ContentMediaSessionService\",\n                        \"ContentScreenReader\",\n                        \"ContentDiscarded\",\n                        \"EmbedderPopupBlockerTabHelper\",\n                        \"EmbedderSafeBrowsingTriggeredPopupBlocker\",\n                        \"EmbedderSafeBrowsingThreatDetails\",\n                        \"EmbedderAppBannerManager\",\n                        \"EmbedderDomDistillerViewerSource\",\n                        \"EmbedderDomDistillerSelfDeletingRequestDelegate\",\n                        \"EmbedderOomInterventionTabHelper\",\n                        \"EmbedderOfflinePage\",\n                        \"EmbedderChromePasswordManagerClientBindCredentialManager\",\n                        \"EmbedderPermissionRequestManager\",\n                        \"EmbedderModalDialog\",\n                        \"EmbedderExtensions\",\n                        \"EmbedderExtensionMessaging\",\n                        \"EmbedderExtensionMessagingForOpenPort\",\n                        \"EmbedderExtensionSentMessageToCachedFrame\",\n                        \"RequestedByWebViewClient\",\n                        \"PostMessageByWebViewClient\",\n                        \"CacheControlNoStoreDeviceBoundSessionTerminated\",\n                        \"CacheLimitPrunedOnModerateMemoryPressure\",\n                        \"CacheLimitPrunedOnCriticalMemoryPressure\"\n                    ]\n                },\n                {\n                    \"id\": \"BackForwardCacheNotRestoredReasonType\",\n                    \"description\": \"Types of not restored reasons for back-forward cache.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SupportPending\",\n                        \"PageSupportNeeded\",\n                        \"Circumstantial\"\n                    ]\n                },\n                {\n                    \"id\": \"BackForwardCacheBlockingDetails\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Url of the file where blockage happened. Optional because of tests.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"function\",\n                            \"description\": \"Function name where blockage happened. Optional because of anonymous functions and tests.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number in the script (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Column number in the script (0-based).\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BackForwardCacheNotRestoredExplanation\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the reason\",\n                            \"$ref\": \"BackForwardCacheNotRestoredReasonType\"\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"Not restored reason\",\n                            \"$ref\": \"BackForwardCacheNotRestoredReason\"\n                        },\n                        {\n                            \"name\": \"context\",\n                            \"description\": \"Context associated with the reason. The meaning of this context is\\ndependent on the reason:\\n- EmbedderExtensionSentMessageToCachedFrame: the extension ID.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"details\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackForwardCacheBlockingDetails\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BackForwardCacheNotRestoredExplanationTree\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of each frame\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"explanations\",\n                            \"description\": \"Not restored reasons of each frame\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackForwardCacheNotRestoredExplanation\"\n                            }\n                        },\n                        {\n                            \"name\": \"children\",\n                            \"description\": \"Array of children frame\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackForwardCacheNotRestoredExplanationTree\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"addScriptToEvaluateOnLoad\",\n                    \"description\": \"Deprecated, please use addScriptToEvaluateOnNewDocument instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptSource\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"description\": \"Identifier of the added script.\",\n                            \"$ref\": \"ScriptIdentifier\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addScriptToEvaluateOnNewDocument\",\n                    \"description\": \"Evaluates given script in every frame upon creation (before loading frame's scripts).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"source\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"worldName\",\n                            \"description\": \"If specified, creates an isolated world with the given name and evaluates given script in it.\\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\\nevent is emitted.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"includeCommandLineAPI\",\n                            \"description\": \"Specifies whether command line API should be available to the script, defaults\\nto false.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"runImmediately\",\n                            \"description\": \"If true, runs the script immediately on existing execution contexts or worlds.\\nDefault: false.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"description\": \"Identifier of the added script.\",\n                            \"$ref\": \"ScriptIdentifier\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"bringToFront\",\n                    \"description\": \"Brings page to front (activates tab).\"\n                },\n                {\n                    \"name\": \"captureScreenshot\",\n                    \"description\": \"Capture page screenshot.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"format\",\n                            \"description\": \"Image compression format (defaults to png).\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"jpeg\",\n                                \"png\",\n                                \"webp\"\n                            ]\n                        },\n                        {\n                            \"name\": \"quality\",\n                            \"description\": \"Compression quality from range [0..100] (jpeg only).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"clip\",\n                            \"description\": \"Capture the screenshot of a given region only.\",\n                            \"optional\": true,\n                            \"$ref\": \"Viewport\"\n                        },\n                        {\n                            \"name\": \"fromSurface\",\n                            \"description\": \"Capture the screenshot from the surface, rather than the view. Defaults to true.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"captureBeyondViewport\",\n                            \"description\": \"Capture the screenshot beyond the viewport. Defaults to false.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"optimizeForSpeed\",\n                            \"description\": \"Optimize image encoding for speed, not for resulting size (defaults to false)\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Base64-encoded image data. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"captureSnapshot\",\n                    \"description\": \"Returns a snapshot of the page as a string. For MHTML format, the serialization includes\\niframes, shadow DOM, external resources, and element-inline styles.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"format\",\n                            \"description\": \"Format (defaults to mhtml).\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mhtml\"\n                            ]\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Serialized page data.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearDeviceMetricsOverride\",\n                    \"description\": \"Clears the overridden device metrics.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"Emulation\"\n                },\n                {\n                    \"name\": \"clearDeviceOrientationOverride\",\n                    \"description\": \"Clears the overridden Device Orientation.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"DeviceOrientation\"\n                },\n                {\n                    \"name\": \"clearGeolocationOverride\",\n                    \"description\": \"Clears the overridden Geolocation Position and Error.\",\n                    \"deprecated\": true,\n                    \"redirect\": \"Emulation\"\n                },\n                {\n                    \"name\": \"createIsolatedWorld\",\n                    \"description\": \"Creates an isolated world for the given frame.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame in which the isolated world should be created.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"worldName\",\n                            \"description\": \"An optional name which is reported in the Execution Context.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"grantUniveralAccess\",\n                            \"description\": \"Whether or not universal access should be granted to the isolated world. This is a powerful\\noption, use with caution.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Execution context of the isolated world.\",\n                            \"$ref\": \"Runtime.ExecutionContextId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteCookie\",\n                    \"description\": \"Deletes browser cookie with given name, domain and path.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"Network\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cookieName\",\n                            \"description\": \"Name of the cookie to remove.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL to match cooke domain and path.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables page domain notifications.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables page domain notifications.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enableFileChooserOpenedEvent\",\n                            \"description\": \"If true, the `Page.fileChooserOpened` event will be emitted regardless of the state set by\\n`Page.setInterceptFileChooserDialog` command (default: false).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAppManifest\",\n                    \"description\": \"Gets the processed manifest for this current document.\\n  This API always waits for the manifest to be loaded.\\n  If manifestId is provided, and it does not match the manifest of the\\n    current document, this API errors out.\\n  If there is not a loaded page, this API errors out immediately.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"manifestId\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Manifest location.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"errors\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AppManifestError\"\n                            }\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Manifest content.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"parsed\",\n                            \"description\": \"Parsed manifest properties. Deprecated, use manifest instead.\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"AppManifestParsedProperties\"\n                        },\n                        {\n                            \"name\": \"manifest\",\n                            \"experimental\": true,\n                            \"$ref\": \"WebAppManifest\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getInstallabilityErrors\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"installabilityErrors\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InstallabilityError\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getManifestIcons\",\n                    \"description\": \"Deprecated because it's not guaranteed that the returned icon is in fact the one used for PWA installation.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"primaryIcon\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAppId\",\n                    \"description\": \"Returns the unique (PWA) app id.\\nOnly returns values if the feature flag 'WebAppEnableManifestId' is enabled\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"appId\",\n                            \"description\": \"App id, either from manifest's id attribute or computed from start_url\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"recommendedId\",\n                            \"description\": \"Recommendation for manifest's id attribute to match current id computed from start_url\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAdScriptAncestry\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"adScriptAncestry\",\n                            \"description\": \"The ancestry chain of ad script identifiers leading to this frame's\\ncreation, along with the root script's filterlist rule. The ancestry\\nchain is ordered from the most immediate script (in the frame creation\\nstack) to more distant ancestors (that created the immediately preceding\\nscript). Only sent if frame is labelled as an ad and ids are available.\",\n                            \"optional\": true,\n                            \"$ref\": \"AdScriptAncestry\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getFrameTree\",\n                    \"description\": \"Returns present frame tree structure.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"frameTree\",\n                            \"description\": \"Present frame tree structure.\",\n                            \"$ref\": \"FrameTree\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getLayoutMetrics\",\n                    \"description\": \"Returns metrics relating to the layouting of the page, such as viewport bounds/scale.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"layoutViewport\",\n                            \"description\": \"Deprecated metrics relating to the layout viewport. Is in device pixels. Use `cssLayoutViewport` instead.\",\n                            \"deprecated\": true,\n                            \"$ref\": \"LayoutViewport\"\n                        },\n                        {\n                            \"name\": \"visualViewport\",\n                            \"description\": \"Deprecated metrics relating to the visual viewport. Is in device pixels. Use `cssVisualViewport` instead.\",\n                            \"deprecated\": true,\n                            \"$ref\": \"VisualViewport\"\n                        },\n                        {\n                            \"name\": \"contentSize\",\n                            \"description\": \"Deprecated size of scrollable area. Is in DP. Use `cssContentSize` instead.\",\n                            \"deprecated\": true,\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"cssLayoutViewport\",\n                            \"description\": \"Metrics relating to the layout viewport in CSS pixels.\",\n                            \"$ref\": \"LayoutViewport\"\n                        },\n                        {\n                            \"name\": \"cssVisualViewport\",\n                            \"description\": \"Metrics relating to the visual viewport in CSS pixels.\",\n                            \"$ref\": \"VisualViewport\"\n                        },\n                        {\n                            \"name\": \"cssContentSize\",\n                            \"description\": \"Size of scrollable area in CSS pixels.\",\n                            \"$ref\": \"DOM.Rect\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getNavigationHistory\",\n                    \"description\": \"Returns navigation history for the current page.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"currentIndex\",\n                            \"description\": \"Index of the current navigation history entry.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"entries\",\n                            \"description\": \"Array of navigation history entries.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"NavigationEntry\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resetNavigationHistory\",\n                    \"description\": \"Resets navigation history for the current page.\"\n                },\n                {\n                    \"name\": \"getResourceContent\",\n                    \"description\": \"Returns content of the given resource.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id to get resource for.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resource to get content for.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"content\",\n                            \"description\": \"Resource content.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"base64Encoded\",\n                            \"description\": \"True, if content was served as base64.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getResourceTree\",\n                    \"description\": \"Returns present frame / resource tree structure.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"frameTree\",\n                            \"description\": \"Present frame / resource tree structure.\",\n                            \"$ref\": \"FrameResourceTree\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"handleJavaScriptDialog\",\n                    \"description\": \"Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"accept\",\n                            \"description\": \"Whether to accept or dismiss the dialog.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"promptText\",\n                            \"description\": \"The text to enter into the dialog prompt before accepting. Used only if this is a prompt\\ndialog.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"navigate\",\n                    \"description\": \"Navigates current page to the given URL.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL to navigate the page to.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"referrer\",\n                            \"description\": \"Referrer URL.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"transitionType\",\n                            \"description\": \"Intended transition type.\",\n                            \"optional\": true,\n                            \"$ref\": \"TransitionType\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id to navigate, if not specified navigates the top frame.\",\n                            \"optional\": true,\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"referrerPolicy\",\n                            \"description\": \"Referrer-policy used for the navigation.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"ReferrerPolicy\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id that has navigated (or failed to navigate)\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Loader identifier. This is omitted in case of same-document navigation,\\nas the previously committed loaderId would not change.\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"errorText\",\n                            \"description\": \"User friendly error message, present if and only if navigation has failed.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isDownload\",\n                            \"description\": \"Whether the navigation resulted in a download.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"navigateToHistoryEntry\",\n                    \"description\": \"Navigates current page to the given history entry.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"entryId\",\n                            \"description\": \"Unique id of the entry to navigate to.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"printToPDF\",\n                    \"description\": \"Print page as PDF.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"landscape\",\n                            \"description\": \"Paper orientation. Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"displayHeaderFooter\",\n                            \"description\": \"Display header and footer. Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"printBackground\",\n                            \"description\": \"Print background graphics. Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"scale\",\n                            \"description\": \"Scale of the webpage rendering. Defaults to 1.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"paperWidth\",\n                            \"description\": \"Paper width in inches. Defaults to 8.5 inches.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"paperHeight\",\n                            \"description\": \"Paper height in inches. Defaults to 11 inches.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"marginTop\",\n                            \"description\": \"Top margin in inches. Defaults to 1cm (~0.4 inches).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"marginBottom\",\n                            \"description\": \"Bottom margin in inches. Defaults to 1cm (~0.4 inches).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"marginLeft\",\n                            \"description\": \"Left margin in inches. Defaults to 1cm (~0.4 inches).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"marginRight\",\n                            \"description\": \"Right margin in inches. Defaults to 1cm (~0.4 inches).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"pageRanges\",\n                            \"description\": \"Paper ranges to print, one based, e.g., '1-5, 8, 11-13'. Pages are\\nprinted in the document order, not in the order specified, and no\\nmore than once.\\nDefaults to empty string, which implies the entire document is printed.\\nThe page numbers are quietly capped to actual page count of the\\ndocument, and ranges beyond the end of the document are ignored.\\nIf this results in no pages to print, an error is reported.\\nIt is an error to specify a range with start greater than end.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"headerTemplate\",\n                            \"description\": \"HTML template for the print header. Should be valid HTML markup with following\\nclasses used to inject printing values into them:\\n- `date`: formatted print date\\n- `title`: document title\\n- `url`: document location\\n- `pageNumber`: current page number\\n- `totalPages`: total pages in the document\\n\\nFor example, `<span class=title></span>` would generate span containing the title.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"footerTemplate\",\n                            \"description\": \"HTML template for the print footer. Should use the same format as the `headerTemplate`.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"preferCSSPageSize\",\n                            \"description\": \"Whether or not to prefer page size as defined by css. Defaults to false,\\nin which case the content will be scaled to fit the paper size.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"transferMode\",\n                            \"description\": \"return as stream\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"ReturnAsBase64\",\n                                \"ReturnAsStream\"\n                            ]\n                        },\n                        {\n                            \"name\": \"generateTaggedPDF\",\n                            \"description\": \"Whether or not to generate tagged (accessible) PDF. Defaults to embedder choice.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generateDocumentOutline\",\n                            \"description\": \"Whether or not to embed the document outline into the PDF.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Base64-encoded pdf data. Empty if |returnAsStream| is specified. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"stream\",\n                            \"description\": \"A handle of the stream that holds resulting PDF data.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"IO.StreamHandle\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reload\",\n                    \"description\": \"Reloads given page optionally ignoring the cache.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"ignoreCache\",\n                            \"description\": \"If true, browser cache is ignored (as if the user pressed Shift+refresh).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"scriptToEvaluateOnLoad\",\n                            \"description\": \"If set, the script will be injected into all frames of the inspected page after reload.\\nArgument will be ignored if reloading dataURL origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"If set, an error will be thrown if the target page's main frame's\\nloader id does not match the provided id. This prevents accidentally\\nreloading an unintended target in case there's a racing navigation.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Network.LoaderId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeScriptToEvaluateOnLoad\",\n                    \"description\": \"Deprecated, please use removeScriptToEvaluateOnNewDocument instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"ScriptIdentifier\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeScriptToEvaluateOnNewDocument\",\n                    \"description\": \"Removes given script from the list.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"identifier\",\n                            \"$ref\": \"ScriptIdentifier\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"screencastFrameAck\",\n                    \"description\": \"Acknowledges that a screencast frame has been received by the frontend.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Frame number.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"searchInResource\",\n                    \"description\": \"Searches for given string in resource content.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id for resource to search in.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resource to search in.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"query\",\n                            \"description\": \"String to search for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"caseSensitive\",\n                            \"description\": \"If true, search is case sensitive.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isRegex\",\n                            \"description\": \"If true, treats string parameter as regex.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"List of search matches.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Debugger.SearchMatch\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAdBlockingEnabled\",\n                    \"description\": \"Enable Chrome's experimental ad filter on all sites.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether to block ads.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBypassCSP\",\n                    \"description\": \"Enable page Content Security Policy by-passing.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether to bypass page CSP.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getPermissionsPolicyState\",\n                    \"description\": \"Get Permissions Policy state on given frame.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"states\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PermissionsPolicyFeatureState\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getOriginTrials\",\n                    \"description\": \"Get Origin Trials on given frame.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"originTrials\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"OriginTrial\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDeviceMetricsOverride\",\n                    \"description\": \"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\\"device-width\\\"/\\\"device-height\\\"-related CSS media\\nquery results).\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"Emulation\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"deviceScaleFactor\",\n                            \"description\": \"Overriding device scale factor value. 0 disables the override.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"mobile\",\n                            \"description\": \"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"scale\",\n                            \"description\": \"Scale to apply to resulting view image.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"screenWidth\",\n                            \"description\": \"Overriding screen width value in pixels (minimum 0, maximum 10000000).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"screenHeight\",\n                            \"description\": \"Overriding screen height value in pixels (minimum 0, maximum 10000000).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"positionX\",\n                            \"description\": \"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"positionY\",\n                            \"description\": \"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"dontSetVisibleSize\",\n                            \"description\": \"Do not set visible view size, rely upon explicit setVisibleSize call.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"screenOrientation\",\n                            \"description\": \"Screen orientation override.\",\n                            \"optional\": true,\n                            \"$ref\": \"Emulation.ScreenOrientation\"\n                        },\n                        {\n                            \"name\": \"viewport\",\n                            \"description\": \"The viewport dimensions and scale. If not set, the override is cleared.\",\n                            \"optional\": true,\n                            \"$ref\": \"Viewport\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDeviceOrientationOverride\",\n                    \"description\": \"Overrides the Device Orientation.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"DeviceOrientation\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"alpha\",\n                            \"description\": \"Mock alpha\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"beta\",\n                            \"description\": \"Mock beta\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"gamma\",\n                            \"description\": \"Mock gamma\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setFontFamilies\",\n                    \"description\": \"Set generic font families.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"fontFamilies\",\n                            \"description\": \"Specifies font families to set. If a font family is not specified, it won't be changed.\",\n                            \"$ref\": \"FontFamilies\"\n                        },\n                        {\n                            \"name\": \"forScripts\",\n                            \"description\": \"Specifies font families to set for individual scripts.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScriptFontFamilies\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setFontSizes\",\n                    \"description\": \"Set default font sizes.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"fontSizes\",\n                            \"description\": \"Specifies font sizes to set. If a font size is not specified, it won't be changed.\",\n                            \"$ref\": \"FontSizes\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDocumentContent\",\n                    \"description\": \"Sets given markup as the document's HTML.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id to set HTML for.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"html\",\n                            \"description\": \"HTML content to set.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDownloadBehavior\",\n                    \"description\": \"Set the behavior when downloading a file.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"behavior\",\n                            \"description\": \"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny).\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"deny\",\n                                \"allow\",\n                                \"default\"\n                            ]\n                        },\n                        {\n                            \"name\": \"downloadPath\",\n                            \"description\": \"The default path to save downloaded files to. This is required if behavior is set to 'allow'\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setGeolocationOverride\",\n                    \"description\": \"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.\",\n                    \"deprecated\": true,\n                    \"redirect\": \"Emulation\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"latitude\",\n                            \"description\": \"Mock latitude\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"longitude\",\n                            \"description\": \"Mock longitude\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"accuracy\",\n                            \"description\": \"Mock accuracy\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setLifecycleEventsEnabled\",\n                    \"description\": \"Controls whether page will emit lifecycle events.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"If true, starts emitting lifecycle events.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setTouchEmulationEnabled\",\n                    \"description\": \"Toggles mouse event-based touch event emulation.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"redirect\": \"Emulation\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"Whether the touch event emulation should be enabled.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"configuration\",\n                            \"description\": \"Touch/gesture events configuration. Default: current platform.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"mobile\",\n                                \"desktop\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startScreencast\",\n                    \"description\": \"Starts sending each frame using the `screencastFrame` event.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"format\",\n                            \"description\": \"Image compression format.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"jpeg\",\n                                \"png\"\n                            ]\n                        },\n                        {\n                            \"name\": \"quality\",\n                            \"description\": \"Compression quality from range [0..100].\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxWidth\",\n                            \"description\": \"Maximum screenshot width.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxHeight\",\n                            \"description\": \"Maximum screenshot height.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"everyNthFrame\",\n                            \"description\": \"Send every n-th frame.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopLoading\",\n                    \"description\": \"Force the page stop all navigations and pending resource fetches.\"\n                },\n                {\n                    \"name\": \"crash\",\n                    \"description\": \"Crashes renderer on the IO thread, generates minidumps.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"close\",\n                    \"description\": \"Tries to close page, running its beforeunload hooks, if any.\"\n                },\n                {\n                    \"name\": \"setWebLifecycleState\",\n                    \"description\": \"Tries to update the web lifecycle state of the page.\\nIt will transition the page to the given state according to:\\nhttps://github.com/WICG/web-lifecycle/\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"state\",\n                            \"description\": \"Target lifecycle state\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"frozen\",\n                                \"active\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopScreencast\",\n                    \"description\": \"Stops sending each frame in the `screencastFrame`.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"produceCompilationCache\",\n                    \"description\": \"Requests backend to produce compilation cache for the specified scripts.\\n`scripts` are appended to the list of scripts for which the cache\\nwould be produced. The list may be reset during page navigation.\\nWhen script with a matching URL is encountered, the cache is optionally\\nproduced upon backend discretion, based on internal heuristics.\\nSee also: `Page.compilationCacheProduced`.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"scripts\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CompilationCacheParams\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addCompilationCache\",\n                    \"description\": \"Seeds compilation cache for given url. Compilation cache does not survive\\ncross-process navigation.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Base64-encoded data (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearCompilationCache\",\n                    \"description\": \"Clears seeded compilation cache.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"setSPCTransactionMode\",\n                    \"description\": \"Sets the Secure Payment Confirmation transaction mode.\\nhttps://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"mode\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"none\",\n                                \"autoAccept\",\n                                \"autoChooseToAuthAnotherWay\",\n                                \"autoReject\",\n                                \"autoOptOut\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setRPHRegistrationMode\",\n                    \"description\": \"Extensions for Custom Handlers API:\\nhttps://html.spec.whatwg.org/multipage/system-state.html#rph-automation\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"mode\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"none\",\n                                \"autoAccept\",\n                                \"autoReject\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"generateTestReport\",\n                    \"description\": \"Generates a report for testing.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"message\",\n                            \"description\": \"Message to be displayed in the report.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"group\",\n                            \"description\": \"Specifies the endpoint group to deliver the report to.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"waitForDebugger\",\n                    \"description\": \"Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"setInterceptFileChooserDialog\",\n                    \"description\": \"Intercept file chooser requests and transfer control to protocol clients.\\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\\nInstead, a protocol event `Page.fileChooserOpened` is emitted.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"cancel\",\n                            \"description\": \"If true, cancels the dialog by emitting relevant events (if any)\\nin addition to not showing it if the interception is enabled\\n(default: false).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPrerenderingAllowed\",\n                    \"description\": \"Enable/disable prerendering manually.\\n\\nThis command is a short-term solution for https://crbug.com/1440085.\\nSee https://docs.google.com/document/d/12HVmFxYj5Jc-eJr5OmWsa2bqTJsbgGLKI6ZIyx0_wpA\\nfor more details.\\n\\nTODO(https://crbug.com/1440085): Remove this once Puppeteer supports tab targets.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"isAllowed\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAnnotatedPageContent\",\n                    \"description\": \"Get the annotated page content for the main frame.\\nThis is an experimental command that is subject to change.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"includeActionableInformation\",\n                            \"description\": \"Whether to include actionable information. Defaults to true.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"content\",\n                            \"description\": \"The annotated page content as a base64 encoded protobuf.\\nThe format is defined by the `AnnotatedPageContent` message in\\ncomponents/optimization_guide/proto/features/common_quality_data.proto (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"domContentEventFired\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"Network.MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"fileChooserOpened\",\n                    \"description\": \"Emitted only when `page.interceptFileChooser` is enabled.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame containing input node.\",\n                            \"experimental\": true,\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"mode\",\n                            \"description\": \"Input mode.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"selectSingle\",\n                                \"selectMultiple\"\n                            ]\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"Input node id. Only present for file choosers opened via an `<input type=\\\"file\\\">` element.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameAttached\",\n                    \"description\": \"Fired when frame has been attached to its parent.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that has been attached.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"parentFrameId\",\n                            \"description\": \"Parent frame identifier.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"stack\",\n                            \"description\": \"JavaScript stack trace of when frame was attached, only set if frame initiated from script.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameClearedScheduledNavigation\",\n                    \"description\": \"Fired when frame no longer has a scheduled navigation.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that has cleared its scheduled navigation.\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameDetached\",\n                    \"description\": \"Fired when frame has been detached from its parent.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that has been detached.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"experimental\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"remove\",\n                                \"swap\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameSubtreeWillBeDetached\",\n                    \"description\": \"Fired before frame subtree is detached. Emitted before any frame of the\\nsubtree is actually detached.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that is the root of the subtree that will be detached.\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameNavigated\",\n                    \"description\": \"Fired once navigation of the frame has completed. Frame is now associated with the new loader.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frame\",\n                            \"description\": \"Frame object.\",\n                            \"$ref\": \"Frame\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"experimental\": true,\n                            \"$ref\": \"NavigationType\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"documentOpened\",\n                    \"description\": \"Fired when opening document to write to.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frame\",\n                            \"description\": \"Frame object.\",\n                            \"$ref\": \"Frame\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameResized\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"frameStartedNavigating\",\n                    \"description\": \"Fired when a navigation starts. This event is fired for both\\nrenderer-initiated and browser-initiated navigations. For renderer-initiated\\nnavigations, the event is fired after `frameRequestedNavigation`.\\nNavigation may still be cancelled after the event is issued. Multiple events\\ncan be fired for a single navigation, for example, when a same-document\\nnavigation becomes a cross-document navigation (such as in the case of a\\nframeset).\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"ID of the frame that is being navigated.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The URL the navigation started with. The final URL can be different.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Loader identifier. Even though it is present in case of same-document\\nnavigation, the previously committed loaderId would not change unless\\nthe navigation changes from a same-document to a cross-document\\nnavigation.\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"navigationType\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"reload\",\n                                \"reloadBypassingCache\",\n                                \"restore\",\n                                \"restoreWithPost\",\n                                \"historySameDocument\",\n                                \"historyDifferentDocument\",\n                                \"sameDocument\",\n                                \"differentDocument\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameRequestedNavigation\",\n                    \"description\": \"Fired when a renderer-initiated navigation is requested.\\nNavigation may still be cancelled after the event is issued.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that is being navigated.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"The reason for the navigation.\",\n                            \"$ref\": \"ClientNavigationReason\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The destination URL for the requested navigation.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"disposition\",\n                            \"description\": \"The disposition for the navigation.\",\n                            \"$ref\": \"ClientNavigationDisposition\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameScheduledNavigation\",\n                    \"description\": \"Fired when frame schedules a potential navigation.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that has scheduled a navigation.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"delay\",\n                            \"description\": \"Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\\nguaranteed to start.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"The reason for the navigation.\",\n                            \"$ref\": \"ClientNavigationReason\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The destination URL for the scheduled navigation.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameStartedLoading\",\n                    \"description\": \"Fired when frame has started loading.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that has started loading.\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"frameStoppedLoading\",\n                    \"description\": \"Fired when frame has stopped loading.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that has stopped loading.\",\n                            \"$ref\": \"FrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"downloadWillBegin\",\n                    \"description\": \"Fired when page is about to start a download.\\nDeprecated. Use Browser.downloadWillBegin instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame that caused download to begin.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"guid\",\n                            \"description\": \"Global unique identifier of the download.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resource being downloaded.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"suggestedFilename\",\n                            \"description\": \"Suggested file name of the resource (the actual name of the file saved on disk may differ).\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"downloadProgress\",\n                    \"description\": \"Fired when download makes progress. Last call has |done| == true.\\nDeprecated. Use Browser.downloadProgress instead.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"guid\",\n                            \"description\": \"Global unique identifier of the download.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"totalBytes\",\n                            \"description\": \"Total expected bytes to download.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"receivedBytes\",\n                            \"description\": \"Total bytes received.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"state\",\n                            \"description\": \"Download status.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"inProgress\",\n                                \"completed\",\n                                \"canceled\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"interstitialHidden\",\n                    \"description\": \"Fired when interstitial page was hidden\"\n                },\n                {\n                    \"name\": \"interstitialShown\",\n                    \"description\": \"Fired when interstitial page was shown\"\n                },\n                {\n                    \"name\": \"javascriptDialogClosed\",\n                    \"description\": \"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\\nclosed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id.\",\n                            \"experimental\": true,\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Whether dialog was confirmed.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"userInput\",\n                            \"description\": \"User input in case of prompt.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"javascriptDialogOpening\",\n                    \"description\": \"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\\nopen.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Frame url.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Frame id.\",\n                            \"experimental\": true,\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"message\",\n                            \"description\": \"Message that will be displayed by the dialog.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Dialog type.\",\n                            \"$ref\": \"DialogType\"\n                        },\n                        {\n                            \"name\": \"hasBrowserHandler\",\n                            \"description\": \"True iff browser is capable showing or acting on the given dialog. When browser has no\\ndialog handler for given target, calling alert while Page domain is engaged will stall\\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"defaultPrompt\",\n                            \"description\": \"Default dialog prompt.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"lifecycleEvent\",\n                    \"description\": \"Fired for lifecycle events (navigation, load, paint, etc) in the current\\ntarget (including local frames).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Loader identifier. Empty string if the request is fetched from worker.\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"Network.MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"backForwardCacheNotUsed\",\n                    \"description\": \"Fired for failed bfcache history navigations if BackForwardCache feature is enabled. Do\\nnot assume any ordering with the Page.frameNavigated event. This event is fired only for\\nmain-frame history navigation where the document changes (non-same-document navigations),\\nwhen bfcache navigation fails.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"The loader id for the associated navigation.\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"The frame id of the associated frame.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"notRestoredExplanations\",\n                            \"description\": \"Array of reasons why the page could not be cached. This must not be empty.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BackForwardCacheNotRestoredExplanation\"\n                            }\n                        },\n                        {\n                            \"name\": \"notRestoredExplanationsTree\",\n                            \"description\": \"Tree structure of reasons why the page could not be cached for each frame.\",\n                            \"optional\": true,\n                            \"$ref\": \"BackForwardCacheNotRestoredExplanationTree\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"loadEventFired\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"$ref\": \"Network.MonotonicTime\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"navigatedWithinDocument\",\n                    \"description\": \"Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Id of the frame.\",\n                            \"$ref\": \"FrameId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Frame's new url.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"navigationType\",\n                            \"description\": \"Navigation type\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"fragment\",\n                                \"historyApi\",\n                                \"other\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"screencastFrame\",\n                    \"description\": \"Compressed image data requested by the `startScreencast`.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Base64-encoded compressed image. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"metadata\",\n                            \"description\": \"Screencast frame metadata.\",\n                            \"$ref\": \"ScreencastFrameMetadata\"\n                        },\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Frame number.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"screencastVisibilityChanged\",\n                    \"description\": \"Fired when the page with currently enabled screencast was shown or hidden `.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"visible\",\n                            \"description\": \"True if the page is visible.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"windowOpen\",\n                    \"description\": \"Fired when a new window is going to be opened, via window.open(), link click, form submission,\\netc.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The URL for the new window.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"windowName\",\n                            \"description\": \"Window name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"windowFeatures\",\n                            \"description\": \"An array of enabled window features.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"userGesture\",\n                            \"description\": \"Whether or not it was triggered by user gesture.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"compilationCacheProduced\",\n                    \"description\": \"Issued for every compilation cache generated.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Base64-encoded data (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Performance\",\n            \"types\": [\n                {\n                    \"id\": \"Metric\",\n                    \"description\": \"Run-time execution metric.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Metric name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Metric value.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disable collecting and reporting metrics.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enable collecting and reporting metrics.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"timeDomain\",\n                            \"description\": \"Time domain to use for collecting and reporting duration metrics.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"timeTicks\",\n                                \"threadTicks\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setTimeDomain\",\n                    \"description\": \"Sets time domain to use for collecting and reporting duration metrics.\\nNote that this must be called before enabling metrics collection. Calling\\nthis method while metrics collection is enabled returns an error.\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"timeDomain\",\n                            \"description\": \"Time domain\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"timeTicks\",\n                                \"threadTicks\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getMetrics\",\n                    \"description\": \"Retrieve current values of run-time metrics.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"metrics\",\n                            \"description\": \"Current values for run-time metrics.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Metric\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"metrics\",\n                    \"description\": \"Current values of the metrics.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"metrics\",\n                            \"description\": \"Current values of the metrics.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Metric\"\n                            }\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Timestamp title.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"PerformanceTimeline\",\n            \"description\": \"Reporting of performance timeline events, as specified in\\nhttps://w3c.github.io/performance-timeline/#dom-performanceobserver.\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"DOM\",\n                \"Network\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"LargestContentfulPaint\",\n                    \"description\": \"See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"renderTime\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"loadTime\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"The number of pixels being painted.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"elementId\",\n                            \"description\": \"The id attribute of the element, if available.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The URL of the image (may be trimmed).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LayoutShiftAttribution\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"previousRect\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"currentRect\",\n                            \"$ref\": \"DOM.Rect\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LayoutShift\",\n                    \"description\": \"See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Score increment produced by this event.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"hadRecentInput\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"lastInputTime\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"sources\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"LayoutShiftAttribution\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"TimelineEvent\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"description\": \"Identifies the frame that this event is related to. Empty for non-frame targets.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\\nThis determines which of the optional \\\"details\\\" fields is present.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Name may be empty depending on the type.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"time\",\n                            \"description\": \"Time in seconds since Epoch, monotonically increasing within document lifetime.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"duration\",\n                            \"description\": \"Event duration, if applicable.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"lcpDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"LargestContentfulPaint\"\n                        },\n                        {\n                            \"name\": \"layoutShiftDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"LayoutShift\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Previously buffered events would be reported before method returns.\\nSee also: timelineEventAdded\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventTypes\",\n                            \"description\": \"The types of event to report, as specified in\\nhttps://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\\nThe specified filter overrides any previous filters, passing empty\\nfilter disables recording.\\nNote that not all types exposed to the web platform are currently supported.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"timelineEventAdded\",\n                    \"description\": \"Sent when a performance timeline event is added. See reportPerformanceTimeline method.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"event\",\n                            \"$ref\": \"TimelineEvent\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Preload\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"RuleSetId\",\n                    \"description\": \"Unique id\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"RuleSet\",\n                    \"description\": \"Corresponds to SpeculationRuleSet\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"RuleSetId\"\n                        },\n                        {\n                            \"name\": \"loaderId\",\n                            \"description\": \"Identifies a document which the rule set is associated with.\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"sourceText\",\n                            \"description\": \"Source text of JSON representing the rule set. If it comes from\\n`<script>` tag, it is the textContent of the node. Note that it is\\na JSON for valid case.\\n\\nSee also:\\n- https://wicg.github.io/nav-speculation/speculation-rules.html\\n- https://github.com/WICG/nav-speculation/blob/main/triggers.md\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"backendNodeId\",\n                            \"description\": \"A speculation rule set is either added through an inline\\n`<script>` tag or through an external resource via the\\n'Speculation-Rules' HTTP header. For the first case, we include\\nthe BackendNodeId of the relevant `<script>` tag. For the second\\ncase, we include the external URL where the rule set was loaded\\nfrom, and also RequestId if Network domain is enabled.\\n\\nSee also:\\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-script\\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-header\",\n                            \"optional\": true,\n                            \"$ref\": \"DOM.BackendNodeId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"optional\": true,\n                            \"$ref\": \"Network.RequestId\"\n                        },\n                        {\n                            \"name\": \"errorType\",\n                            \"description\": \"Error information\\n`errorMessage` is null iff `errorType` is null.\",\n                            \"optional\": true,\n                            \"$ref\": \"RuleSetErrorType\"\n                        },\n                        {\n                            \"name\": \"errorMessage\",\n                            \"description\": \"TODO(https://crbug.com/1425354): Replace this property with structured error.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"tag\",\n                            \"description\": \"For more details, see:\\nhttps://github.com/WICG/nav-speculation/blob/main/speculation-rules-tags.md\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RuleSetErrorType\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SourceIsNotJsonObject\",\n                        \"InvalidRulesSkipped\",\n                        \"InvalidRulesetLevelTag\"\n                    ]\n                },\n                {\n                    \"id\": \"SpeculationAction\",\n                    \"description\": \"The type of preloading attempted. It corresponds to\\nmojom::SpeculationAction (although PrefetchWithSubresources is omitted as it\\nisn't being used by clients).\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Prefetch\",\n                        \"Prerender\",\n                        \"PrerenderUntilScript\"\n                    ]\n                },\n                {\n                    \"id\": \"SpeculationTargetHint\",\n                    \"description\": \"Corresponds to mojom::SpeculationTargetHint.\\nSee https://github.com/WICG/nav-speculation/blob/main/triggers.md#window-name-targeting-hints\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Blank\",\n                        \"Self\"\n                    ]\n                },\n                {\n                    \"id\": \"PreloadingAttemptKey\",\n                    \"description\": \"A key that identifies a preloading attempt.\\n\\nThe url used is the url specified by the trigger (i.e. the initial URL), and\\nnot the final url that is navigated to. For example, prerendering allows\\nsame-origin main frame navigations during the attempt, but the attempt is\\nstill keyed with the initial URL.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"loaderId\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"action\",\n                            \"$ref\": \"SpeculationAction\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"targetHint\",\n                            \"optional\": true,\n                            \"$ref\": \"SpeculationTargetHint\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PreloadingAttemptSource\",\n                    \"description\": \"Lists sources for a preloading attempt, specifically the ids of rule sets\\nthat had a speculation rule that triggered the attempt, and the\\nBackendNodeIds of <a href> or <area href> elements that triggered the\\nattempt (in the case of attempts triggered by a document rule). It is\\npossible for multiple rule sets and links to trigger a single attempt.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"$ref\": \"PreloadingAttemptKey\"\n                        },\n                        {\n                            \"name\": \"ruleSetIds\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RuleSetId\"\n                            }\n                        },\n                        {\n                            \"name\": \"nodeIds\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"DOM.BackendNodeId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PreloadPipelineId\",\n                    \"description\": \"Chrome manages different types of preloads together using a\\nconcept of preloading pipeline. For example, if a site uses a\\nSpeculationRules for prerender, Chrome first starts a prefetch and\\nthen upgrades it to prerender.\\n\\nCDP events for them are emitted separately but they share\\n`PreloadPipelineId`.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"PrerenderFinalStatus\",\n                    \"description\": \"List of FinalStatus reasons for Prerender2.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Activated\",\n                        \"Destroyed\",\n                        \"LowEndDevice\",\n                        \"InvalidSchemeRedirect\",\n                        \"InvalidSchemeNavigation\",\n                        \"NavigationRequestBlockedByCsp\",\n                        \"MojoBinderPolicy\",\n                        \"RendererProcessCrashed\",\n                        \"RendererProcessKilled\",\n                        \"Download\",\n                        \"TriggerDestroyed\",\n                        \"NavigationNotCommitted\",\n                        \"NavigationBadHttpStatus\",\n                        \"ClientCertRequested\",\n                        \"NavigationRequestNetworkError\",\n                        \"CancelAllHostsForTesting\",\n                        \"DidFailLoad\",\n                        \"Stop\",\n                        \"SslCertificateError\",\n                        \"LoginAuthRequested\",\n                        \"UaChangeRequiresReload\",\n                        \"BlockedByClient\",\n                        \"AudioOutputDeviceRequested\",\n                        \"MixedContent\",\n                        \"TriggerBackgrounded\",\n                        \"MemoryLimitExceeded\",\n                        \"DataSaverEnabled\",\n                        \"TriggerUrlHasEffectiveUrl\",\n                        \"ActivatedBeforeStarted\",\n                        \"InactivePageRestriction\",\n                        \"StartFailed\",\n                        \"TimeoutBackgrounded\",\n                        \"CrossSiteRedirectInInitialNavigation\",\n                        \"CrossSiteNavigationInInitialNavigation\",\n                        \"SameSiteCrossOriginRedirectNotOptInInInitialNavigation\",\n                        \"SameSiteCrossOriginNavigationNotOptInInInitialNavigation\",\n                        \"ActivationNavigationParameterMismatch\",\n                        \"ActivatedInBackground\",\n                        \"EmbedderHostDisallowed\",\n                        \"ActivationNavigationDestroyedBeforeSuccess\",\n                        \"TabClosedByUserGesture\",\n                        \"TabClosedWithoutUserGesture\",\n                        \"PrimaryMainFrameRendererProcessCrashed\",\n                        \"PrimaryMainFrameRendererProcessKilled\",\n                        \"ActivationFramePolicyNotCompatible\",\n                        \"PreloadingDisabled\",\n                        \"BatterySaverEnabled\",\n                        \"ActivatedDuringMainFrameNavigation\",\n                        \"PreloadingUnsupportedByWebContents\",\n                        \"CrossSiteRedirectInMainFrameNavigation\",\n                        \"CrossSiteNavigationInMainFrameNavigation\",\n                        \"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation\",\n                        \"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation\",\n                        \"MemoryPressureOnTrigger\",\n                        \"MemoryPressureAfterTriggered\",\n                        \"PrerenderingDisabledByDevTools\",\n                        \"SpeculationRuleRemoved\",\n                        \"ActivatedWithAuxiliaryBrowsingContexts\",\n                        \"MaxNumOfRunningEagerPrerendersExceeded\",\n                        \"MaxNumOfRunningNonEagerPrerendersExceeded\",\n                        \"MaxNumOfRunningEmbedderPrerendersExceeded\",\n                        \"PrerenderingUrlHasEffectiveUrl\",\n                        \"RedirectedPrerenderingUrlHasEffectiveUrl\",\n                        \"ActivationUrlHasEffectiveUrl\",\n                        \"JavaScriptInterfaceAdded\",\n                        \"JavaScriptInterfaceRemoved\",\n                        \"AllPrerenderingCanceled\",\n                        \"WindowClosed\",\n                        \"SlowNetwork\",\n                        \"OtherPrerenderedPageActivated\",\n                        \"V8OptimizerDisabled\",\n                        \"PrerenderFailedDuringPrefetch\",\n                        \"BrowsingDataRemoved\",\n                        \"PrerenderHostReused\"\n                    ]\n                },\n                {\n                    \"id\": \"PreloadingStatus\",\n                    \"description\": \"Preloading status values, see also PreloadingTriggeringOutcome. This\\nstatus is shared by prefetchStatusUpdated and prerenderStatusUpdated.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Pending\",\n                        \"Running\",\n                        \"Ready\",\n                        \"Success\",\n                        \"Failure\",\n                        \"NotSupported\"\n                    ]\n                },\n                {\n                    \"id\": \"PrefetchStatus\",\n                    \"description\": \"TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and\\nfilter out the ones that aren't necessary to the developers.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"PrefetchAllowed\",\n                        \"PrefetchFailedIneligibleRedirect\",\n                        \"PrefetchFailedInvalidRedirect\",\n                        \"PrefetchFailedMIMENotSupported\",\n                        \"PrefetchFailedNetError\",\n                        \"PrefetchFailedNon2XX\",\n                        \"PrefetchEvictedAfterBrowsingDataRemoved\",\n                        \"PrefetchEvictedAfterCandidateRemoved\",\n                        \"PrefetchEvictedForNewerPrefetch\",\n                        \"PrefetchHeldback\",\n                        \"PrefetchIneligibleRetryAfter\",\n                        \"PrefetchIsPrivacyDecoy\",\n                        \"PrefetchIsStale\",\n                        \"PrefetchNotEligibleBrowserContextOffTheRecord\",\n                        \"PrefetchNotEligibleDataSaverEnabled\",\n                        \"PrefetchNotEligibleExistingProxy\",\n                        \"PrefetchNotEligibleHostIsNonUnique\",\n                        \"PrefetchNotEligibleNonDefaultStoragePartition\",\n                        \"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy\",\n                        \"PrefetchNotEligibleSchemeIsNotHttps\",\n                        \"PrefetchNotEligibleUserHasCookies\",\n                        \"PrefetchNotEligibleUserHasServiceWorker\",\n                        \"PrefetchNotEligibleUserHasServiceWorkerNoFetchHandler\",\n                        \"PrefetchNotEligibleRedirectFromServiceWorker\",\n                        \"PrefetchNotEligibleRedirectToServiceWorker\",\n                        \"PrefetchNotEligibleBatterySaverEnabled\",\n                        \"PrefetchNotEligiblePreloadingDisabled\",\n                        \"PrefetchNotFinishedInTime\",\n                        \"PrefetchNotStarted\",\n                        \"PrefetchNotUsedCookiesChanged\",\n                        \"PrefetchProxyNotAvailable\",\n                        \"PrefetchResponseUsed\",\n                        \"PrefetchSuccessfulButNotUsed\",\n                        \"PrefetchNotUsedProbeFailed\"\n                    ]\n                },\n                {\n                    \"id\": \"PrerenderMismatchedHeaders\",\n                    \"description\": \"Information of headers to be displayed when the header mismatch occurred.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"headerName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"initialValue\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"activationValue\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\"\n                },\n                {\n                    \"name\": \"disable\"\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"ruleSetUpdated\",\n                    \"description\": \"Upsert. Currently, it is only emitted when a rule set added.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"ruleSet\",\n                            \"$ref\": \"RuleSet\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"ruleSetRemoved\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"$ref\": \"RuleSetId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"preloadEnabledStateUpdated\",\n                    \"description\": \"Fired when a preload enabled state is updated.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"disabledByPreference\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disabledByDataSaver\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disabledByBatterySaver\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disabledByHoldbackPrefetchSpeculationRules\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"disabledByHoldbackPrerenderSpeculationRules\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"prefetchStatusUpdated\",\n                    \"description\": \"Fired when a prefetch attempt is updated.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"key\",\n                            \"$ref\": \"PreloadingAttemptKey\"\n                        },\n                        {\n                            \"name\": \"pipelineId\",\n                            \"$ref\": \"PreloadPipelineId\"\n                        },\n                        {\n                            \"name\": \"initiatingFrameId\",\n                            \"description\": \"The frame id of the frame initiating prefetch.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"prefetchUrl\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"PreloadingStatus\"\n                        },\n                        {\n                            \"name\": \"prefetchStatus\",\n                            \"$ref\": \"PrefetchStatus\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"$ref\": \"Network.RequestId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"prerenderStatusUpdated\",\n                    \"description\": \"Fired when a prerender attempt is updated.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"key\",\n                            \"$ref\": \"PreloadingAttemptKey\"\n                        },\n                        {\n                            \"name\": \"pipelineId\",\n                            \"$ref\": \"PreloadPipelineId\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"PreloadingStatus\"\n                        },\n                        {\n                            \"name\": \"prerenderStatus\",\n                            \"optional\": true,\n                            \"$ref\": \"PrerenderFinalStatus\"\n                        },\n                        {\n                            \"name\": \"disallowedMojoInterface\",\n                            \"description\": \"This is used to give users more information about the name of Mojo interface\\nthat is incompatible with prerender and has caused the cancellation of the attempt.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mismatchedHeaders\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PrerenderMismatchedHeaders\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"preloadingAttemptSourcesUpdated\",\n                    \"description\": \"Send a list of sources for all preloading attempts in a document.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"loaderId\",\n                            \"$ref\": \"Network.LoaderId\"\n                        },\n                        {\n                            \"name\": \"preloadingAttemptSources\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PreloadingAttemptSource\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Security\",\n            \"types\": [\n                {\n                    \"id\": \"CertificateId\",\n                    \"description\": \"An internal certificate ID value.\",\n                    \"type\": \"integer\"\n                },\n                {\n                    \"id\": \"MixedContentType\",\n                    \"description\": \"A description of mixed content (HTTP resources on HTTPS pages), as defined by\\nhttps://www.w3.org/TR/mixed-content/#categories\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"blockable\",\n                        \"optionally-blockable\",\n                        \"none\"\n                    ]\n                },\n                {\n                    \"id\": \"SecurityState\",\n                    \"description\": \"The security level of a page or resource.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"unknown\",\n                        \"neutral\",\n                        \"insecure\",\n                        \"secure\",\n                        \"info\",\n                        \"insecure-broken\"\n                    ]\n                },\n                {\n                    \"id\": \"CertificateSecurityState\",\n                    \"description\": \"Details about the security state of the page certificate.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"protocol\",\n                            \"description\": \"Protocol name (e.g. \\\"TLS 1.2\\\" or \\\"QUIC\\\").\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyExchange\",\n                            \"description\": \"Key Exchange used by the connection, or the empty string if not applicable.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keyExchangeGroup\",\n                            \"description\": \"(EC)DH group used by the connection, if applicable.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cipher\",\n                            \"description\": \"Cipher name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mac\",\n                            \"description\": \"TLS MAC. Note that AEAD ciphers do not have separate MACs.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"certificate\",\n                            \"description\": \"Page certificate.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"subjectName\",\n                            \"description\": \"Certificate subject name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"issuer\",\n                            \"description\": \"Name of the issuing CA.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"validFrom\",\n                            \"description\": \"Certificate valid from date.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"validTo\",\n                            \"description\": \"Certificate valid to (expiration) date\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"certificateNetworkError\",\n                            \"description\": \"The highest priority network error code, if the certificate has an error.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"certificateHasWeakSignature\",\n                            \"description\": \"True if the certificate uses a weak signature algorithm.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"certificateHasSha1Signature\",\n                            \"description\": \"True if the certificate has a SHA1 signature in the chain.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"modernSSL\",\n                            \"description\": \"True if modern SSL\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"obsoleteSslProtocol\",\n                            \"description\": \"True if the connection is using an obsolete SSL protocol.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"obsoleteSslKeyExchange\",\n                            \"description\": \"True if the connection is using an obsolete SSL key exchange.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"obsoleteSslCipher\",\n                            \"description\": \"True if the connection is using an obsolete SSL cipher.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"obsoleteSslSignature\",\n                            \"description\": \"True if the connection is using an obsolete SSL signature.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SafetyTipStatus\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"badReputation\",\n                        \"lookalike\"\n                    ]\n                },\n                {\n                    \"id\": \"SafetyTipInfo\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"safetyTipStatus\",\n                            \"description\": \"Describes whether the page triggers any safety tips or reputation warnings. Default is unknown.\",\n                            \"$ref\": \"SafetyTipStatus\"\n                        },\n                        {\n                            \"name\": \"safeUrl\",\n                            \"description\": \"The URL the safety tip suggested (\\\"Did you mean?\\\"). Only filled in for lookalike matches.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"VisibleSecurityState\",\n                    \"description\": \"Security state information about the page.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"securityState\",\n                            \"description\": \"The security level of the page.\",\n                            \"$ref\": \"SecurityState\"\n                        },\n                        {\n                            \"name\": \"certificateSecurityState\",\n                            \"description\": \"Security state details about the page certificate.\",\n                            \"optional\": true,\n                            \"$ref\": \"CertificateSecurityState\"\n                        },\n                        {\n                            \"name\": \"safetyTipInfo\",\n                            \"description\": \"The type of Safety Tip triggered on the page. Note that this field will be set even if the Safety Tip UI was not actually shown.\",\n                            \"optional\": true,\n                            \"$ref\": \"SafetyTipInfo\"\n                        },\n                        {\n                            \"name\": \"securityStateIssueIds\",\n                            \"description\": \"Array of security state issues ids.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SecurityStateExplanation\",\n                    \"description\": \"An explanation of an factor contributing to the security state.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"securityState\",\n                            \"description\": \"Security state representing the severity of the factor being explained.\",\n                            \"$ref\": \"SecurityState\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Title describing the type of factor.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"summary\",\n                            \"description\": \"Short phrase describing the type of factor.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"description\",\n                            \"description\": \"Full text explanation of the factor.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"mixedContentType\",\n                            \"description\": \"The type of mixed content described by the explanation.\",\n                            \"$ref\": \"MixedContentType\"\n                        },\n                        {\n                            \"name\": \"certificate\",\n                            \"description\": \"Page certificate.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"recommendations\",\n                            \"description\": \"Recommendations to fix any issues.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InsecureContentStatus\",\n                    \"description\": \"Information about insecure content on the page.\",\n                    \"deprecated\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"ranMixedContent\",\n                            \"description\": \"Always false.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"displayedMixedContent\",\n                            \"description\": \"Always false.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"containedMixedForm\",\n                            \"description\": \"Always false.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"ranContentWithCertErrors\",\n                            \"description\": \"Always false.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"displayedContentWithCertErrors\",\n                            \"description\": \"Always false.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"ranInsecureContentStyle\",\n                            \"description\": \"Always set to unknown.\",\n                            \"$ref\": \"SecurityState\"\n                        },\n                        {\n                            \"name\": \"displayedInsecureContentStyle\",\n                            \"description\": \"Always set to unknown.\",\n                            \"$ref\": \"SecurityState\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CertificateErrorAction\",\n                    \"description\": \"The action to take when a certificate error occurs. continue will continue processing the\\nrequest and cancel will cancel the request.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"continue\",\n                        \"cancel\"\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables tracking security state changes.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables tracking security state changes.\"\n                },\n                {\n                    \"name\": \"setIgnoreCertificateErrors\",\n                    \"description\": \"Enable/disable whether all certificate errors should be ignored.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"ignore\",\n                            \"description\": \"If true, all certificate errors will be ignored.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"handleCertificateError\",\n                    \"description\": \"Handles a certificate error that fired a certificateError event.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventId\",\n                            \"description\": \"The ID of the event.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"action\",\n                            \"description\": \"The action to take on the certificate error.\",\n                            \"$ref\": \"CertificateErrorAction\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setOverrideCertificateErrors\",\n                    \"description\": \"Enable/disable overriding certificate errors. If enabled, all certificate error events need to\\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"override\",\n                            \"description\": \"If true, certificate errors will be overridden.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"certificateError\",\n                    \"description\": \"There is a certificate error. If overriding certificate errors is enabled, then it should be\\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\\ncertificate error has been allowed internally. Only one client per target should override\\ncertificate errors at the same time.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventId\",\n                            \"description\": \"The ID of the event.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"errorType\",\n                            \"description\": \"The type of the error.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"requestURL\",\n                            \"description\": \"The url that was requested.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"visibleSecurityStateChanged\",\n                    \"description\": \"The security state of the page changed.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"visibleSecurityState\",\n                            \"description\": \"Security state information about the page.\",\n                            \"$ref\": \"VisibleSecurityState\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"securityStateChanged\",\n                    \"description\": \"The security state of the page changed. No longer being sent.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"securityState\",\n                            \"description\": \"Security state.\",\n                            \"$ref\": \"SecurityState\"\n                        },\n                        {\n                            \"name\": \"schemeIsCryptographic\",\n                            \"description\": \"True if the page was loaded over cryptographic transport such as HTTPS.\",\n                            \"deprecated\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"explanations\",\n                            \"description\": \"Previously a list of explanations for the security state. Now always\\nempty.\",\n                            \"deprecated\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SecurityStateExplanation\"\n                            }\n                        },\n                        {\n                            \"name\": \"insecureContentStatus\",\n                            \"description\": \"Information about insecure content on the page.\",\n                            \"deprecated\": true,\n                            \"$ref\": \"InsecureContentStatus\"\n                        },\n                        {\n                            \"name\": \"summary\",\n                            \"description\": \"Overrides user-visible description of the state. Always omitted.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"ServiceWorker\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Target\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"RegistrationID\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ServiceWorkerRegistration\",\n                    \"description\": \"ServiceWorker registration.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"registrationId\",\n                            \"$ref\": \"RegistrationID\"\n                        },\n                        {\n                            \"name\": \"scopeURL\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isDeleted\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerVersionRunningStatus\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"stopped\",\n                        \"starting\",\n                        \"running\",\n                        \"stopping\"\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerVersionStatus\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"new\",\n                        \"installing\",\n                        \"installed\",\n                        \"activating\",\n                        \"activated\",\n                        \"redundant\"\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerVersion\",\n                    \"description\": \"ServiceWorker version.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"versionId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"registrationId\",\n                            \"$ref\": \"RegistrationID\"\n                        },\n                        {\n                            \"name\": \"scriptURL\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"runningStatus\",\n                            \"$ref\": \"ServiceWorkerVersionRunningStatus\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"$ref\": \"ServiceWorkerVersionStatus\"\n                        },\n                        {\n                            \"name\": \"scriptLastModified\",\n                            \"description\": \"The Last-Modified header value of the main script.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"scriptResponseTime\",\n                            \"description\": \"The time at which the response headers of the main script were received from the server.\\nFor cached script it is the last time the cache entry was validated.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"controlledClients\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Target.TargetID\"\n                            }\n                        },\n                        {\n                            \"name\": \"targetId\",\n                            \"optional\": true,\n                            \"$ref\": \"Target.TargetID\"\n                        },\n                        {\n                            \"name\": \"routerRules\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ServiceWorkerErrorMessage\",\n                    \"description\": \"ServiceWorker error message.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"errorMessage\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"registrationId\",\n                            \"$ref\": \"RegistrationID\"\n                        },\n                        {\n                            \"name\": \"versionId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sourceURL\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"deliverPushMessage\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"registrationId\",\n                            \"$ref\": \"RegistrationID\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\"\n                },\n                {\n                    \"name\": \"dispatchSyncEvent\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"registrationId\",\n                            \"$ref\": \"RegistrationID\"\n                        },\n                        {\n                            \"name\": \"tag\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lastChance\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dispatchPeriodicSyncEvent\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"registrationId\",\n                            \"$ref\": \"RegistrationID\"\n                        },\n                        {\n                            \"name\": \"tag\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"enable\"\n                },\n                {\n                    \"name\": \"setForceUpdateOnPageLoad\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"forceUpdateOnPageLoad\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"skipWaiting\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scopeURL\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startWorker\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scopeURL\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopAllWorkers\"\n                },\n                {\n                    \"name\": \"stopWorker\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"versionId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"unregister\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scopeURL\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"updateRegistration\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scopeURL\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"workerErrorReported\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"errorMessage\",\n                            \"$ref\": \"ServiceWorkerErrorMessage\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"workerRegistrationUpdated\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"registrations\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ServiceWorkerRegistration\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"workerVersionUpdated\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"versions\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ServiceWorkerVersion\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"SmartCardEmulation\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"ResultCode\",\n                    \"description\": \"Indicates the PC/SC error code.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__ErrorCodes.html\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/secauthn/authentication-return-values\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"success\",\n                        \"removed-card\",\n                        \"reset-card\",\n                        \"unpowered-card\",\n                        \"unresponsive-card\",\n                        \"unsupported-card\",\n                        \"reader-unavailable\",\n                        \"sharing-violation\",\n                        \"not-transacted\",\n                        \"no-smartcard\",\n                        \"proto-mismatch\",\n                        \"system-cancelled\",\n                        \"not-ready\",\n                        \"cancelled\",\n                        \"insufficient-buffer\",\n                        \"invalid-handle\",\n                        \"invalid-parameter\",\n                        \"invalid-value\",\n                        \"no-memory\",\n                        \"timeout\",\n                        \"unknown-reader\",\n                        \"unsupported-feature\",\n                        \"no-readers-available\",\n                        \"service-stopped\",\n                        \"no-service\",\n                        \"comm-error\",\n                        \"internal-error\",\n                        \"server-too-busy\",\n                        \"unexpected\",\n                        \"shutdown\",\n                        \"unknown-card\",\n                        \"unknown\"\n                    ]\n                },\n                {\n                    \"id\": \"ShareMode\",\n                    \"description\": \"Maps to the |SCARD_SHARE_*| values.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"shared\",\n                        \"exclusive\",\n                        \"direct\"\n                    ]\n                },\n                {\n                    \"id\": \"Disposition\",\n                    \"description\": \"Indicates what the reader should do with the card.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"leave-card\",\n                        \"reset-card\",\n                        \"unpower-card\",\n                        \"eject-card\"\n                    ]\n                },\n                {\n                    \"id\": \"ConnectionState\",\n                    \"description\": \"Maps to |SCARD_*| connection state values.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"absent\",\n                        \"present\",\n                        \"swallowed\",\n                        \"powered\",\n                        \"negotiable\",\n                        \"specific\"\n                    ]\n                },\n                {\n                    \"id\": \"ReaderStateFlags\",\n                    \"description\": \"Maps to the |SCARD_STATE_*| flags.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"unaware\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"ignore\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"changed\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"unknown\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"unavailable\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"empty\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"present\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"exclusive\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"inuse\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"mute\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"unpowered\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ProtocolSet\",\n                    \"description\": \"Maps to the |SCARD_PROTOCOL_*| flags.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"t0\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"t1\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"raw\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Protocol\",\n                    \"description\": \"Maps to the |SCARD_PROTOCOL_*| values.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"t0\",\n                        \"t1\",\n                        \"raw\"\n                    ]\n                },\n                {\n                    \"id\": \"ReaderStateIn\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"reader\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"currentState\",\n                            \"$ref\": \"ReaderStateFlags\"\n                        },\n                        {\n                            \"name\": \"currentInsertionCount\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ReaderStateOut\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"reader\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"eventState\",\n                            \"$ref\": \"ReaderStateFlags\"\n                        },\n                        {\n                            \"name\": \"eventCount\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"atr\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables the |SmartCardEmulation| domain.\"\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables the |SmartCardEmulation| domain.\"\n                },\n                {\n                    \"name\": \"reportEstablishContextResult\",\n                    \"description\": \"Reports the successful result of a |SCardEstablishContext| call.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaa1b8970169fd4883a6dc4a8f43f19b67\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardestablishcontext\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportReleaseContextResult\",\n                    \"description\": \"Reports the successful result of a |SCardReleaseContext| call.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga6aabcba7744c5c9419fdd6404f73a934\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardreleasecontext\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportListReadersResult\",\n                    \"description\": \"Reports the successful result of a |SCardListReaders| call.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga93b07815789b3cf2629d439ecf20f0d9\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardlistreadersa\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"readers\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportGetStatusChangeResult\",\n                    \"description\": \"Reports the successful result of a |SCardGetStatusChange| call.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga33247d5d1257d59e55647c3bb717db24\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardgetstatuschangea\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"readerStates\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ReaderStateOut\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportBeginTransactionResult\",\n                    \"description\": \"Reports the result of a |SCardBeginTransaction| call.\\nOn success, this creates a new transaction object.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaddb835dce01a0da1d6ca02d33ee7d861\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardbegintransaction\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportPlainResult\",\n                    \"description\": \"Reports the successful result of a call that returns only a result code.\\nUsed for: |SCardCancel|, |SCardDisconnect|, |SCardSetAttrib|, |SCardEndTransaction|.\\n\\nThis maps to:\\n1. SCardCancel\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaacbbc0c6d6c0cbbeb4f4debf6fbeeee6\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardcancel\\n\\n2. SCardDisconnect\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga4be198045c73ec0deb79e66c0ca1738a\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scarddisconnect\\n\\n3. SCardSetAttrib\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga060f0038a4ddfd5dd2b8fadf3c3a2e4f\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardsetattrib\\n\\n4. SCardEndTransaction\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gae8742473b404363e5c587f570d7e2f3b\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardendtransaction\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportConnectResult\",\n                    \"description\": \"Reports the successful result of a |SCardConnect| call.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga4e515829752e0a8dbc4d630696a8d6a5\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardconnecta\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"activeProtocol\",\n                            \"optional\": true,\n                            \"$ref\": \"Protocol\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportDataResult\",\n                    \"description\": \"Reports the successful result of a call that sends back data on success.\\nUsed for |SCardTransmit|, |SCardControl|, and |SCardGetAttrib|.\\n\\nThis maps to:\\n1. SCardTransmit\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga9a2d77242a271310269065e64633ab99\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardtransmit\\n\\n2. SCardControl\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gac3454d4657110fd7f753b2d3d8f4e32f\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardcontrol\\n\\n3. SCardGetAttrib\\n   PC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaacfec51917255b7a25b94c5104961602\\n   Microsoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardgetattrib\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportStatusResult\",\n                    \"description\": \"Reports the successful result of a |SCardStatus| call.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gae49c3c894ad7ac12a5b896bde70d0382\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardstatusa\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"readerName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"state\",\n                            \"$ref\": \"ConnectionState\"\n                        },\n                        {\n                            \"name\": \"atr\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"protocol\",\n                            \"optional\": true,\n                            \"$ref\": \"Protocol\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportError\",\n                    \"description\": \"Reports an error result for the given request.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"resultCode\",\n                            \"$ref\": \"ResultCode\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"establishContextRequested\",\n                    \"description\": \"Fired when |SCardEstablishContext| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaa1b8970169fd4883a6dc4a8f43f19b67\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardestablishcontext\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"releaseContextRequested\",\n                    \"description\": \"Fired when |SCardReleaseContext| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga6aabcba7744c5c9419fdd6404f73a934\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardreleasecontext\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"listReadersRequested\",\n                    \"description\": \"Fired when |SCardListReaders| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga93b07815789b3cf2629d439ecf20f0d9\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardlistreadersa\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getStatusChangeRequested\",\n                    \"description\": \"Fired when |SCardGetStatusChange| is called. Timeout is specified in milliseconds.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga33247d5d1257d59e55647c3bb717db24\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardgetstatuschangea\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"readerStates\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ReaderStateIn\"\n                            }\n                        },\n                        {\n                            \"name\": \"timeout\",\n                            \"description\": \"in milliseconds, if absent, it means \\\"infinite\\\"\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"cancelRequested\",\n                    \"description\": \"Fired when |SCardCancel| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaacbbc0c6d6c0cbbeb4f4debf6fbeeee6\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardcancel\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"connectRequested\",\n                    \"description\": \"Fired when |SCardConnect| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga4e515829752e0a8dbc4d630696a8d6a5\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardconnecta\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"reader\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"shareMode\",\n                            \"$ref\": \"ShareMode\"\n                        },\n                        {\n                            \"name\": \"preferredProtocols\",\n                            \"$ref\": \"ProtocolSet\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disconnectRequested\",\n                    \"description\": \"Fired when |SCardDisconnect| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga4be198045c73ec0deb79e66c0ca1738a\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scarddisconnect\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"disposition\",\n                            \"$ref\": \"Disposition\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"transmitRequested\",\n                    \"description\": \"Fired when |SCardTransmit| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga9a2d77242a271310269065e64633ab99\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardtransmit\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"protocol\",\n                            \"optional\": true,\n                            \"$ref\": \"Protocol\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"controlRequested\",\n                    \"description\": \"Fired when |SCardControl| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gac3454d4657110fd7f753b2d3d8f4e32f\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardcontrol\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"controlCode\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAttribRequested\",\n                    \"description\": \"Fired when |SCardGetAttrib| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaacfec51917255b7a25b94c5104961602\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardgetattrib\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"attribId\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAttribRequested\",\n                    \"description\": \"Fired when |SCardSetAttrib| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#ga060f0038a4ddfd5dd2b8fadf3c3a2e4f\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardsetattrib\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"attribId\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"statusRequested\",\n                    \"description\": \"Fired when |SCardStatus| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gae49c3c894ad7ac12a5b896bde70d0382\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardstatusa\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"beginTransactionRequested\",\n                    \"description\": \"Fired when |SCardBeginTransaction| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gaddb835dce01a0da1d6ca02d33ee7d861\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardbegintransaction\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"endTransactionRequested\",\n                    \"description\": \"Fired when |SCardEndTransaction| is called.\\n\\nThis maps to:\\nPC/SC Lite: https://pcsclite.apdu.fr/api/group__API.html#gae8742473b404363e5c587f570d7e2f3b\\nMicrosoft: https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardendtransaction\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"requestId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"handle\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"disposition\",\n                            \"$ref\": \"Disposition\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Storage\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Browser\",\n                \"Network\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"SerializedStorageKey\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"StorageType\",\n                    \"description\": \"Enum of possible storage types.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"cookies\",\n                        \"file_systems\",\n                        \"indexeddb\",\n                        \"local_storage\",\n                        \"shader_cache\",\n                        \"websql\",\n                        \"service_workers\",\n                        \"cache_storage\",\n                        \"interest_groups\",\n                        \"shared_storage\",\n                        \"storage_buckets\",\n                        \"all\",\n                        \"other\"\n                    ]\n                },\n                {\n                    \"id\": \"UsageForType\",\n                    \"description\": \"Usage for a storage type.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"storageType\",\n                            \"description\": \"Name of storage type.\",\n                            \"$ref\": \"StorageType\"\n                        },\n                        {\n                            \"name\": \"usage\",\n                            \"description\": \"Storage usage (bytes).\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"TrustTokens\",\n                    \"description\": \"Pair of issuer origin and number of available (signed, but not used) Trust\\nTokens from that issuer.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"issuerOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"count\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InterestGroupAuctionId\",\n                    \"description\": \"Protected audience interest group auction identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"InterestGroupAccessType\",\n                    \"description\": \"Enum of interest group access types.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"join\",\n                        \"leave\",\n                        \"update\",\n                        \"loaded\",\n                        \"bid\",\n                        \"win\",\n                        \"additionalBid\",\n                        \"additionalBidWin\",\n                        \"topLevelBid\",\n                        \"topLevelAdditionalBid\",\n                        \"clear\"\n                    ]\n                },\n                {\n                    \"id\": \"InterestGroupAuctionEventType\",\n                    \"description\": \"Enum of auction events.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"started\",\n                        \"configResolved\"\n                    ]\n                },\n                {\n                    \"id\": \"InterestGroupAuctionFetchType\",\n                    \"description\": \"Enum of network fetches auctions can do.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"bidderJs\",\n                        \"bidderWasm\",\n                        \"sellerJs\",\n                        \"bidderTrustedSignals\",\n                        \"sellerTrustedSignals\"\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageAccessScope\",\n                    \"description\": \"Enum of shared storage access scopes.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"window\",\n                        \"sharedStorageWorklet\",\n                        \"protectedAudienceWorklet\",\n                        \"header\"\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageAccessMethod\",\n                    \"description\": \"Enum of shared storage access methods.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"addModule\",\n                        \"createWorklet\",\n                        \"selectURL\",\n                        \"run\",\n                        \"batchUpdate\",\n                        \"set\",\n                        \"append\",\n                        \"delete\",\n                        \"clear\",\n                        \"get\",\n                        \"keys\",\n                        \"values\",\n                        \"entries\",\n                        \"length\",\n                        \"remainingBudget\"\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageEntry\",\n                    \"description\": \"Struct for a single key-value pair in an origin's shared storage.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageMetadata\",\n                    \"description\": \"Details for an origin's shared storage.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"creationTime\",\n                            \"description\": \"Time when the origin's shared storage was last created.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"length\",\n                            \"description\": \"Number of key-value pairs stored in origin's shared storage.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"remainingBudget\",\n                            \"description\": \"Current amount of bits of entropy remaining in the navigation budget.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"bytesUsed\",\n                            \"description\": \"Total number of bytes stored as key-value pairs in origin's shared\\nstorage.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedStoragePrivateAggregationConfig\",\n                    \"description\": \"Represents a dictionary object passed in as privateAggregationConfig to\\nrun or selectURL.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"aggregationCoordinatorOrigin\",\n                            \"description\": \"The chosen aggregation service deployment.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"description\": \"The context ID provided.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"filteringIdMaxBytes\",\n                            \"description\": \"Configures the maximum size allowed for filtering IDs.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxContributions\",\n                            \"description\": \"The limit on the number of contributions in the final report.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageReportingMetadata\",\n                    \"description\": \"Pair of reporting metadata details for a candidate URL for `selectURL()`.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"eventType\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"reportingUrl\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageUrlWithMetadata\",\n                    \"description\": \"Bundles a candidate URL with its reporting metadata.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"Spec of candidate URL.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"reportingMetadata\",\n                            \"description\": \"Any associated reporting metadata.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SharedStorageReportingMetadata\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SharedStorageAccessParams\",\n                    \"description\": \"Bundles the parameters for shared storage access events whose\\npresence/absence can vary according to SharedStorageAccessType.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptSourceUrl\",\n                            \"description\": \"Spec of the module script URL.\\nPresent only for SharedStorageAccessMethods: addModule and\\ncreateWorklet.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"dataOrigin\",\n                            \"description\": \"String denoting \\\"context-origin\\\", \\\"script-origin\\\", or a custom\\norigin to be used as the worklet's data origin.\\nPresent only for SharedStorageAccessMethod: createWorklet.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"operationName\",\n                            \"description\": \"Name of the registered operation to be run.\\nPresent only for SharedStorageAccessMethods: run and selectURL.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"operationId\",\n                            \"description\": \"ID of the operation call.\\nPresent only for SharedStorageAccessMethods: run and selectURL.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"keepAlive\",\n                            \"description\": \"Whether or not to keep the worket alive for future run or selectURL\\ncalls.\\nPresent only for SharedStorageAccessMethods: run and selectURL.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"privateAggregationConfig\",\n                            \"description\": \"Configures the private aggregation options.\\nPresent only for SharedStorageAccessMethods: run and selectURL.\",\n                            \"optional\": true,\n                            \"$ref\": \"SharedStoragePrivateAggregationConfig\"\n                        },\n                        {\n                            \"name\": \"serializedData\",\n                            \"description\": \"The operation's serialized data in bytes (converted to a string).\\nPresent only for SharedStorageAccessMethods: run and selectURL.\\nTODO(crbug.com/401011862): Consider updating this parameter to binary.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"urlsWithMetadata\",\n                            \"description\": \"Array of candidate URLs' specs, along with any associated metadata.\\nPresent only for SharedStorageAccessMethod: selectURL.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SharedStorageUrlWithMetadata\"\n                            }\n                        },\n                        {\n                            \"name\": \"urnUuid\",\n                            \"description\": \"Spec of the URN:UUID generated for a selectURL call.\\nPresent only for SharedStorageAccessMethod: selectURL.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"description\": \"Key for a specific entry in an origin's shared storage.\\nPresent only for SharedStorageAccessMethods: set, append, delete, and\\nget.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Value for a specific entry in an origin's shared storage.\\nPresent only for SharedStorageAccessMethods: set and append.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"ignoreIfPresent\",\n                            \"description\": \"Whether or not to set an entry for a key if that key is already present.\\nPresent only for SharedStorageAccessMethod: set.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"workletOrdinal\",\n                            \"description\": \"A number denoting the (0-based) order of the worklet's\\ncreation relative to all other shared storage worklets created by\\ndocuments using the current storage partition.\\nPresent only for SharedStorageAccessMethods: addModule, createWorklet.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"workletTargetId\",\n                            \"description\": \"Hex representation of the DevTools token used as the TargetID for the\\nassociated shared storage worklet.\\nPresent only for SharedStorageAccessMethods: addModule, createWorklet,\\nrun, selectURL, and any other SharedStorageAccessMethod when the\\nSharedStorageAccessScope is sharedStorageWorklet.\",\n                            \"optional\": true,\n                            \"$ref\": \"Target.TargetID\"\n                        },\n                        {\n                            \"name\": \"withLock\",\n                            \"description\": \"Name of the lock to be acquired, if present.\\nOptionally present only for SharedStorageAccessMethods: batchUpdate,\\nset, append, delete, and clear.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"batchUpdateId\",\n                            \"description\": \"If the method has been called as part of a batchUpdate, then this\\nnumber identifies the batch to which it belongs.\\nOptionally present only for SharedStorageAccessMethods:\\nbatchUpdate (required), set, append, delete, and clear.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"batchSize\",\n                            \"description\": \"Number of modifier methods sent in batch.\\nPresent only for SharedStorageAccessMethod: batchUpdate.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StorageBucketsDurability\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"relaxed\",\n                        \"strict\"\n                    ]\n                },\n                {\n                    \"id\": \"StorageBucket\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"$ref\": \"SerializedStorageKey\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"If not specified, it is the default bucket of the storageKey.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StorageBucketInfo\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"bucket\",\n                            \"$ref\": \"StorageBucket\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"expiration\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"quota\",\n                            \"description\": \"Storage quota (bytes).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"persistent\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"durability\",\n                            \"$ref\": \"StorageBucketsDurability\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingSourceType\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"navigation\",\n                        \"event\"\n                    ]\n                },\n                {\n                    \"id\": \"UnsignedInt64AsBase10\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"UnsignedInt128AsBase16\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"SignedInt64AsBase10\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"AttributionReportingFilterDataEntry\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"values\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingFilterConfig\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"filterValues\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingFilterDataEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"lookbackWindow\",\n                            \"description\": \"duration in seconds\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingFilterPair\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"filters\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingFilterConfig\"\n                            }\n                        },\n                        {\n                            \"name\": \"notFilters\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingFilterConfig\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregationKeysEntry\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"$ref\": \"UnsignedInt128AsBase16\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingEventReportWindows\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"start\",\n                            \"description\": \"duration in seconds\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"ends\",\n                            \"description\": \"duration in seconds\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingTriggerDataMatching\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"exact\",\n                        \"modulus\"\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableDebugReportingData\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"keyPiece\",\n                            \"$ref\": \"UnsignedInt128AsBase16\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"number instead of integer because not all uint32 can be represented by\\nint\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"types\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableDebugReportingConfig\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"budget\",\n                            \"description\": \"number instead of integer because not all uint32 can be represented by\\nint, only present for source registrations\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"keyPiece\",\n                            \"$ref\": \"UnsignedInt128AsBase16\"\n                        },\n                        {\n                            \"name\": \"debugData\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingAggregatableDebugReportingData\"\n                            }\n                        },\n                        {\n                            \"name\": \"aggregationCoordinatorOrigin\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionScopesData\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"values\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"limit\",\n                            \"description\": \"number instead of integer because not all uint32 can be represented by\\nint\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"maxEventStates\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingNamedBudgetDef\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"budget\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingSourceRegistration\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"time\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"expiry\",\n                            \"description\": \"duration in seconds\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"triggerData\",\n                            \"description\": \"number instead of integer because not all uint32 can be represented by\\nint\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"number\"\n                            }\n                        },\n                        {\n                            \"name\": \"eventReportWindows\",\n                            \"$ref\": \"AttributionReportingEventReportWindows\"\n                        },\n                        {\n                            \"name\": \"aggregatableReportWindow\",\n                            \"description\": \"duration in seconds\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"AttributionReportingSourceType\"\n                        },\n                        {\n                            \"name\": \"sourceOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"reportingOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"destinationSites\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"eventId\",\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"priority\",\n                            \"$ref\": \"SignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"filterData\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingFilterDataEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"aggregationKeys\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingAggregationKeysEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"debugKey\",\n                            \"optional\": true,\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"triggerDataMatching\",\n                            \"$ref\": \"AttributionReportingTriggerDataMatching\"\n                        },\n                        {\n                            \"name\": \"destinationLimitPriority\",\n                            \"$ref\": \"SignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"aggregatableDebugReportingConfig\",\n                            \"$ref\": \"AttributionReportingAggregatableDebugReportingConfig\"\n                        },\n                        {\n                            \"name\": \"scopesData\",\n                            \"optional\": true,\n                            \"$ref\": \"AttributionScopesData\"\n                        },\n                        {\n                            \"name\": \"maxEventLevelReports\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"namedBudgets\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingNamedBudgetDef\"\n                            }\n                        },\n                        {\n                            \"name\": \"debugReporting\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"eventLevelEpsilon\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingSourceRegistrationResult\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"success\",\n                        \"internalError\",\n                        \"insufficientSourceCapacity\",\n                        \"insufficientUniqueDestinationCapacity\",\n                        \"excessiveReportingOrigins\",\n                        \"prohibitedByBrowserPolicy\",\n                        \"successNoised\",\n                        \"destinationReportingLimitReached\",\n                        \"destinationGlobalLimitReached\",\n                        \"destinationBothLimitsReached\",\n                        \"reportingOriginsPerSiteLimitReached\",\n                        \"exceedsMaxChannelCapacity\",\n                        \"exceedsMaxScopesChannelCapacity\",\n                        \"exceedsMaxTriggerStateCardinality\",\n                        \"exceedsMaxEventStatesLimit\",\n                        \"destinationPerDayReportingLimitReached\"\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingSourceRegistrationTimeConfig\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"include\",\n                        \"exclude\"\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableValueDictEntry\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"number instead of integer because not all uint32 can be represented by\\nint\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"filteringId\",\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableValueEntry\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"values\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingAggregatableValueDictEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"filters\",\n                            \"$ref\": \"AttributionReportingFilterPair\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingEventTriggerData\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"data\",\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"priority\",\n                            \"$ref\": \"SignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"dedupKey\",\n                            \"optional\": true,\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"filters\",\n                            \"$ref\": \"AttributionReportingFilterPair\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableTriggerData\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"keyPiece\",\n                            \"$ref\": \"UnsignedInt128AsBase16\"\n                        },\n                        {\n                            \"name\": \"sourceKeys\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"filters\",\n                            \"$ref\": \"AttributionReportingFilterPair\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableDedupKey\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"dedupKey\",\n                            \"optional\": true,\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"filters\",\n                            \"$ref\": \"AttributionReportingFilterPair\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingNamedBudgetCandidate\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"filters\",\n                            \"$ref\": \"AttributionReportingFilterPair\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingTriggerRegistration\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"filters\",\n                            \"$ref\": \"AttributionReportingFilterPair\"\n                        },\n                        {\n                            \"name\": \"debugKey\",\n                            \"optional\": true,\n                            \"$ref\": \"UnsignedInt64AsBase10\"\n                        },\n                        {\n                            \"name\": \"aggregatableDedupKeys\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingAggregatableDedupKey\"\n                            }\n                        },\n                        {\n                            \"name\": \"eventTriggerData\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingEventTriggerData\"\n                            }\n                        },\n                        {\n                            \"name\": \"aggregatableTriggerData\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingAggregatableTriggerData\"\n                            }\n                        },\n                        {\n                            \"name\": \"aggregatableValues\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingAggregatableValueEntry\"\n                            }\n                        },\n                        {\n                            \"name\": \"aggregatableFilteringIdMaxBytes\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"debugReporting\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"aggregationCoordinatorOrigin\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sourceRegistrationTimeConfig\",\n                            \"$ref\": \"AttributionReportingSourceRegistrationTimeConfig\"\n                        },\n                        {\n                            \"name\": \"triggerContextId\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"aggregatableDebugReportingConfig\",\n                            \"$ref\": \"AttributionReportingAggregatableDebugReportingConfig\"\n                        },\n                        {\n                            \"name\": \"scopes\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"namedBudgets\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"AttributionReportingNamedBudgetCandidate\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingEventLevelResult\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"success\",\n                        \"successDroppedLowerPriority\",\n                        \"internalError\",\n                        \"noCapacityForAttributionDestination\",\n                        \"noMatchingSources\",\n                        \"deduplicated\",\n                        \"excessiveAttributions\",\n                        \"priorityTooLow\",\n                        \"neverAttributedSource\",\n                        \"excessiveReportingOrigins\",\n                        \"noMatchingSourceFilterData\",\n                        \"prohibitedByBrowserPolicy\",\n                        \"noMatchingConfigurations\",\n                        \"excessiveReports\",\n                        \"falselyAttributedSource\",\n                        \"reportWindowPassed\",\n                        \"notRegistered\",\n                        \"reportWindowNotStarted\",\n                        \"noMatchingTriggerData\"\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingAggregatableResult\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"success\",\n                        \"internalError\",\n                        \"noCapacityForAttributionDestination\",\n                        \"noMatchingSources\",\n                        \"excessiveAttributions\",\n                        \"excessiveReportingOrigins\",\n                        \"noHistograms\",\n                        \"insufficientBudget\",\n                        \"insufficientNamedBudget\",\n                        \"noMatchingSourceFilterData\",\n                        \"notRegistered\",\n                        \"prohibitedByBrowserPolicy\",\n                        \"deduplicated\",\n                        \"reportWindowPassed\",\n                        \"excessiveReports\"\n                    ]\n                },\n                {\n                    \"id\": \"AttributionReportingReportResult\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"sent\",\n                        \"prohibited\",\n                        \"failedToAssemble\",\n                        \"expired\"\n                    ]\n                },\n                {\n                    \"id\": \"RelatedWebsiteSet\",\n                    \"description\": \"A single Related Website Set object.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"primarySites\",\n                            \"description\": \"The primary site of this set, along with the ccTLDs if there is any.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"associatedSites\",\n                            \"description\": \"The associated sites of this set, along with the ccTLDs if there is any.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"serviceSites\",\n                            \"description\": \"The service sites of this set, along with the ccTLDs if there is any.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getStorageKeyForFrame\",\n                    \"description\": \"Returns a storage key given a frame id.\\nDeprecated. Please use Storage.getStorageKey instead.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"$ref\": \"SerializedStorageKey\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getStorageKey\",\n                    \"description\": \"Returns storage key for the given frame. If no frame ID is provided,\\nthe storage key of the target executing this command is returned.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"frameId\",\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"$ref\": \"SerializedStorageKey\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearDataForOrigin\",\n                    \"description\": \"Clears storage for origin.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageTypes\",\n                            \"description\": \"Comma separated list of StorageType to clear.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearDataForStorageKey\",\n                    \"description\": \"Clears storage for storage key.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageTypes\",\n                            \"description\": \"Comma separated list of StorageType to clear.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getCookies\",\n                    \"description\": \"Returns all browser cookies.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"Browser context to use when called on the browser endpoint.\",\n                            \"optional\": true,\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"cookies\",\n                            \"description\": \"Array of cookie objects.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Network.Cookie\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCookies\",\n                    \"description\": \"Sets given cookies.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"cookies\",\n                            \"description\": \"Cookies to be set.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Network.CookieParam\"\n                            }\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"Browser context to use when called on the browser endpoint.\",\n                            \"optional\": true,\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearCookies\",\n                    \"description\": \"Clears cookies.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"Browser context to use when called on the browser endpoint.\",\n                            \"optional\": true,\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getUsageAndQuota\",\n                    \"description\": \"Returns usage and quota in bytes.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"usage\",\n                            \"description\": \"Storage usage (bytes).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"quota\",\n                            \"description\": \"Storage quota (bytes).\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"overrideActive\",\n                            \"description\": \"Whether or not the origin has an active storage quota override\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"usageBreakdown\",\n                            \"description\": \"Storage usage per type (bytes).\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"UsageForType\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"overrideQuotaForOrigin\",\n                    \"description\": \"Override quota for the specified origin\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"quotaSize\",\n                            \"description\": \"The quota size (in bytes) to override the original quota with.\\nIf this is called multiple times, the overridden quota will be equal to\\nthe quotaSize provided in the final call. If this is called without\\nspecifying a quotaSize, the quota will be reset to the default value for\\nthe specified origin. If this is called multiple times with different\\norigins, the override will be maintained for each origin until it is\\ndisabled (called without a quotaSize).\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trackCacheStorageForOrigin\",\n                    \"description\": \"Registers origin to be notified when an update occurs to its cache storage list.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trackCacheStorageForStorageKey\",\n                    \"description\": \"Registers storage key to be notified when an update occurs to its cache storage list.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trackIndexedDBForOrigin\",\n                    \"description\": \"Registers origin to be notified when an update occurs to its IndexedDB.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"trackIndexedDBForStorageKey\",\n                    \"description\": \"Registers storage key to be notified when an update occurs to its IndexedDB.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"untrackCacheStorageForOrigin\",\n                    \"description\": \"Unregisters origin from receiving notifications for cache storage.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"untrackCacheStorageForStorageKey\",\n                    \"description\": \"Unregisters storage key from receiving notifications for cache storage.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"untrackIndexedDBForOrigin\",\n                    \"description\": \"Unregisters origin from receiving notifications for IndexedDB.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Security origin.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"untrackIndexedDBForStorageKey\",\n                    \"description\": \"Unregisters storage key from receiving notifications for IndexedDB.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getTrustTokens\",\n                    \"description\": \"Returns the number of stored Trust Tokens per issuer for the\\ncurrent browsing context.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"tokens\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"TrustTokens\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearTrustTokens\",\n                    \"description\": \"Removes all Trust Tokens issued by the provided issuerOrigin.\\nLeaves other stored data, including the issuer's Redemption Records, intact.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"issuerOrigin\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"didDeleteTokens\",\n                            \"description\": \"True if any tokens were deleted, false otherwise.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getInterestGroupDetails\",\n                    \"description\": \"Gets details for a named interest group.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"details\",\n                            \"description\": \"This largely corresponds to:\\nhttps://wicg.github.io/turtledove/#dictdef-generatebidinterestgroup\\nbut has absolute expirationTime instead of relative lifetimeMs and\\nalso adds joiningOrigin.\",\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInterestGroupTracking\",\n                    \"description\": \"Enables/Disables issuing of interestGroupAccessed events.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInterestGroupAuctionTracking\",\n                    \"description\": \"Enables/Disables issuing of interestGroupAuctionEventOccurred and\\ninterestGroupAuctionNetworkRequestCreated.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSharedStorageMetadata\",\n                    \"description\": \"Gets metadata for an origin's shared storage.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"metadata\",\n                            \"$ref\": \"SharedStorageMetadata\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSharedStorageEntries\",\n                    \"description\": \"Gets the entries in an given origin's shared storage.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"entries\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SharedStorageEntry\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSharedStorageEntry\",\n                    \"description\": \"Sets entry with `key` and `value` for a given origin's shared storage.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"ignoreIfPresent\",\n                            \"description\": \"If `ignoreIfPresent` is included and true, then only sets the entry if\\n`key` doesn't already exist.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteSharedStorageEntry\",\n                    \"description\": \"Deletes entry for `key` (if it exists) for a given origin's shared storage.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"key\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearSharedStorageEntries\",\n                    \"description\": \"Clears all entries for a given origin's shared storage.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resetSharedStorageBudget\",\n                    \"description\": \"Resets the budget for `ownerOrigin` by clearing all budget withdrawals.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSharedStorageTracking\",\n                    \"description\": \"Enables/disables issuing of sharedStorageAccessed events.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setStorageBucketTracking\",\n                    \"description\": \"Set tracking for a storage key's buckets.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"storageKey\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"enable\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"deleteStorageBucket\",\n                    \"description\": \"Deletes the Storage Bucket with the given storage key and bucket name.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"bucket\",\n                            \"$ref\": \"StorageBucket\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"runBounceTrackingMitigations\",\n                    \"description\": \"Deletes state for sites identified as potential bounce trackers, immediately.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"deletedSites\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAttributionReportingLocalTestingMode\",\n                    \"description\": \"https://wicg.github.io/attribution-reporting-api/\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"description\": \"If enabled, noise is suppressed and reports are sent immediately.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAttributionReportingTracking\",\n                    \"description\": \"Enables/disables issuing of Attribution Reporting events.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enable\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"sendPendingAttributionReports\",\n                    \"description\": \"Sends all pending Attribution Reports immediately, regardless of their\\nscheduled report time.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"numSent\",\n                            \"description\": \"The number of reports that were sent.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getRelatedWebsiteSets\",\n                    \"description\": \"Returns the effective Related Website Sets in use by this profile for the browser\\nsession. The effective Related Website Sets will not change during a browser session.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"sets\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RelatedWebsiteSet\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getAffectedUrlsForThirdPartyCookieMetadata\",\n                    \"description\": \"Returns the list of URLs from a page and its embedded resources that match\\nexisting grace period URL pattern rules.\\nhttps://developers.google.com/privacy-sandbox/cookies/temporary-exceptions/grace-period\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"firstPartyUrl\",\n                            \"description\": \"The URL of the page currently being visited.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"thirdPartyUrls\",\n                            \"description\": \"The list of embedded resource URLs from the page.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"matchedUrls\",\n                            \"description\": \"Array of matching URLs. If there is a primary pattern match for the first-\\nparty URL, only the first-party URL is returned in the array.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setProtectedAudienceKAnonymity\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"owner\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"hashes\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"cacheStorageContentUpdated\",\n                    \"description\": \"A cache's contents have been modified.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bucketId\",\n                            \"description\": \"Storage bucket to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"cacheName\",\n                            \"description\": \"Name of cache in origin.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"cacheStorageListUpdated\",\n                    \"description\": \"A cache has been added/deleted.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bucketId\",\n                            \"description\": \"Storage bucket to update.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"indexedDBContentUpdated\",\n                    \"description\": \"The origin's IndexedDB object store has been modified.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bucketId\",\n                            \"description\": \"Storage bucket to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"databaseName\",\n                            \"description\": \"Database to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectStoreName\",\n                            \"description\": \"ObjectStore to update.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"indexedDBListUpdated\",\n                    \"description\": \"The origin's IndexedDB database list has been modified.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Origin to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"storageKey\",\n                            \"description\": \"Storage key to update.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bucketId\",\n                            \"description\": \"Storage bucket to update.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"interestGroupAccessed\",\n                    \"description\": \"One of the interest groups was accessed. Note that these events are global\\nto all targets sharing an interest group store.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"accessTime\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"InterestGroupAccessType\"\n                        },\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"componentSellerOrigin\",\n                            \"description\": \"For topLevelBid/topLevelAdditionalBid, and when appropriate,\\nwin and additionalBidWin\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bid\",\n                            \"description\": \"For bid or somethingBid event, if done locally and not on a server.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"bidCurrency\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"uniqueAuctionId\",\n                            \"description\": \"For non-global events --- links to interestGroupAuctionEvent\",\n                            \"optional\": true,\n                            \"$ref\": \"InterestGroupAuctionId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"interestGroupAuctionEventOccurred\",\n                    \"description\": \"An auction involving interest groups is taking place. These events are\\ntarget-specific.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"eventTime\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"InterestGroupAuctionEventType\"\n                        },\n                        {\n                            \"name\": \"uniqueAuctionId\",\n                            \"$ref\": \"InterestGroupAuctionId\"\n                        },\n                        {\n                            \"name\": \"parentAuctionId\",\n                            \"description\": \"Set for child auctions.\",\n                            \"optional\": true,\n                            \"$ref\": \"InterestGroupAuctionId\"\n                        },\n                        {\n                            \"name\": \"auctionConfig\",\n                            \"description\": \"Set for started and configResolved\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"interestGroupAuctionNetworkRequestCreated\",\n                    \"description\": \"Specifies which auctions a particular network fetch may be related to, and\\nin what role. Note that it is not ordered with respect to\\nNetwork.requestWillBeSent (but will happen before loadingFinished\\nloadingFailed).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"$ref\": \"InterestGroupAuctionFetchType\"\n                        },\n                        {\n                            \"name\": \"requestId\",\n                            \"$ref\": \"Network.RequestId\"\n                        },\n                        {\n                            \"name\": \"auctions\",\n                            \"description\": \"This is the set of the auctions using the worklet that issued this\\nrequest.  In the case of trusted signals, it's possible that only some of\\nthem actually care about the keys being queried.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InterestGroupAuctionId\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"sharedStorageAccessed\",\n                    \"description\": \"Shared storage was accessed by the associated page.\\nThe following parameters are included in all events.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"accessTime\",\n                            \"description\": \"Time of the access.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"scope\",\n                            \"description\": \"Enum value indicating the access scope.\",\n                            \"$ref\": \"SharedStorageAccessScope\"\n                        },\n                        {\n                            \"name\": \"method\",\n                            \"description\": \"Enum value indicating the Shared Storage API method invoked.\",\n                            \"$ref\": \"SharedStorageAccessMethod\"\n                        },\n                        {\n                            \"name\": \"mainFrameId\",\n                            \"description\": \"DevTools Frame Token for the primary frame tree's root.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"description\": \"Serialization of the origin owning the Shared Storage data.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"ownerSite\",\n                            \"description\": \"Serialization of the site owning the Shared Storage data.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"params\",\n                            \"description\": \"The sub-parameters wrapped by `params` are all optional and their\\npresence/absence depends on `type`.\",\n                            \"$ref\": \"SharedStorageAccessParams\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"sharedStorageWorkletOperationExecutionFinished\",\n                    \"description\": \"A shared storage run or selectURL operation finished its execution.\\nThe following parameters are included in all events.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"finishedTime\",\n                            \"description\": \"Time that the operation finished.\",\n                            \"$ref\": \"Network.TimeSinceEpoch\"\n                        },\n                        {\n                            \"name\": \"executionTime\",\n                            \"description\": \"Time, in microseconds, from start of shared storage JS API call until\\nend of operation execution in the worklet.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"method\",\n                            \"description\": \"Enum value indicating the Shared Storage API method invoked.\",\n                            \"$ref\": \"SharedStorageAccessMethod\"\n                        },\n                        {\n                            \"name\": \"operationId\",\n                            \"description\": \"ID of the operation call.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"workletTargetId\",\n                            \"description\": \"Hex representation of the DevTools token used as the TargetID for the\\nassociated shared storage worklet.\",\n                            \"$ref\": \"Target.TargetID\"\n                        },\n                        {\n                            \"name\": \"mainFrameId\",\n                            \"description\": \"DevTools Frame Token for the primary frame tree's root.\",\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"ownerOrigin\",\n                            \"description\": \"Serialization of the origin owning the Shared Storage data.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"storageBucketCreatedOrUpdated\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"bucketInfo\",\n                            \"$ref\": \"StorageBucketInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"storageBucketDeleted\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"bucketId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attributionReportingSourceRegistered\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"registration\",\n                            \"$ref\": \"AttributionReportingSourceRegistration\"\n                        },\n                        {\n                            \"name\": \"result\",\n                            \"$ref\": \"AttributionReportingSourceRegistrationResult\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attributionReportingTriggerRegistered\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"registration\",\n                            \"$ref\": \"AttributionReportingTriggerRegistration\"\n                        },\n                        {\n                            \"name\": \"eventLevel\",\n                            \"$ref\": \"AttributionReportingEventLevelResult\"\n                        },\n                        {\n                            \"name\": \"aggregatable\",\n                            \"$ref\": \"AttributionReportingAggregatableResult\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attributionReportingReportSent\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"body\",\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"result\",\n                            \"$ref\": \"AttributionReportingReportResult\"\n                        },\n                        {\n                            \"name\": \"netError\",\n                            \"description\": \"If result is `sent`, populated with net/HTTP status.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"netErrorName\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"httpStatusCode\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attributionReportingVerboseDebugReportSent\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"body\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"object\"\n                            }\n                        },\n                        {\n                            \"name\": \"netError\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"netErrorName\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"httpStatusCode\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"SystemInfo\",\n            \"description\": \"The SystemInfo domain defines methods and events for querying low-level system information.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"GPUDevice\",\n                    \"description\": \"Describes a single graphics processor (GPU).\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"vendorId\",\n                            \"description\": \"PCI ID of the GPU vendor, if available; 0 otherwise.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"deviceId\",\n                            \"description\": \"PCI ID of the GPU device, if available; 0 otherwise.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"subSysId\",\n                            \"description\": \"Sub sys ID of the GPU, only available on Windows.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"revision\",\n                            \"description\": \"Revision of the GPU, only available on Windows.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"vendorString\",\n                            \"description\": \"String description of the GPU vendor, if the PCI ID is not available.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"deviceString\",\n                            \"description\": \"String description of the GPU device, if the PCI ID is not available.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"driverVendor\",\n                            \"description\": \"String description of the GPU driver vendor.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"driverVersion\",\n                            \"description\": \"String description of the GPU driver version.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Size\",\n                    \"description\": \"Describes the width and height dimensions of an entity.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Width in pixels.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Height in pixels.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"VideoDecodeAcceleratorCapability\",\n                    \"description\": \"Describes a supported video decoding profile with its associated minimum and\\nmaximum resolutions.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"profile\",\n                            \"description\": \"Video codec profile that is supported, e.g. VP9 Profile 2.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"maxResolution\",\n                            \"description\": \"Maximum video dimensions in pixels supported for this |profile|.\",\n                            \"$ref\": \"Size\"\n                        },\n                        {\n                            \"name\": \"minResolution\",\n                            \"description\": \"Minimum video dimensions in pixels supported for this |profile|.\",\n                            \"$ref\": \"Size\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"VideoEncodeAcceleratorCapability\",\n                    \"description\": \"Describes a supported video encoding profile with its associated maximum\\nresolution and maximum framerate.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"profile\",\n                            \"description\": \"Video codec profile that is supported, e.g H264 Main.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"maxResolution\",\n                            \"description\": \"Maximum video dimensions in pixels supported for this |profile|.\",\n                            \"$ref\": \"Size\"\n                        },\n                        {\n                            \"name\": \"maxFramerateNumerator\",\n                            \"description\": \"Maximum encoding framerate in frames per second supported for this\\n|profile|, as fraction's numerator and denominator, e.g. 24/1 fps,\\n24000/1001 fps, etc.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"maxFramerateDenominator\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SubsamplingFormat\",\n                    \"description\": \"YUV subsampling type of the pixels of a given image.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"yuv420\",\n                        \"yuv422\",\n                        \"yuv444\"\n                    ]\n                },\n                {\n                    \"id\": \"ImageType\",\n                    \"description\": \"Image format of a given image.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"jpeg\",\n                        \"webp\",\n                        \"unknown\"\n                    ]\n                },\n                {\n                    \"id\": \"GPUInfo\",\n                    \"description\": \"Provides information about the GPU(s) on the system.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"devices\",\n                            \"description\": \"The graphics devices on the system. Element 0 is the primary GPU.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"GPUDevice\"\n                            }\n                        },\n                        {\n                            \"name\": \"auxAttributes\",\n                            \"description\": \"An optional dictionary of additional GPU related attributes.\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"featureStatus\",\n                            \"description\": \"An optional dictionary of graphics features and their status.\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"driverBugWorkarounds\",\n                            \"description\": \"An optional array of GPU driver bug workarounds.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"videoDecoding\",\n                            \"description\": \"Supported accelerated video decoding capabilities.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"VideoDecodeAcceleratorCapability\"\n                            }\n                        },\n                        {\n                            \"name\": \"videoEncoding\",\n                            \"description\": \"Supported accelerated video encoding capabilities.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"VideoEncodeAcceleratorCapability\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ProcessInfo\",\n                    \"description\": \"Represents process info.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Specifies process type.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Specifies process id.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"cpuTime\",\n                            \"description\": \"Specifies cumulative CPU usage in seconds across all threads of the\\nprocess since the process start.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getInfo\",\n                    \"description\": \"Returns information about the system.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"gpu\",\n                            \"description\": \"Information about the GPUs on the system.\",\n                            \"$ref\": \"GPUInfo\"\n                        },\n                        {\n                            \"name\": \"modelName\",\n                            \"description\": \"A platform-dependent description of the model of the machine. On Mac OS, this is, for\\nexample, 'MacBookPro'. Will be the empty string if not supported.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"modelVersion\",\n                            \"description\": \"A platform-dependent description of the version of the machine. On Mac OS, this is, for\\nexample, '10.1'. Will be the empty string if not supported.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"commandLine\",\n                            \"description\": \"The command line string used to launch the browser. Will be the empty string if not\\nsupported.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getFeatureState\",\n                    \"description\": \"Returns information about the feature state.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"featureState\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"featureEnabled\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getProcessInfo\",\n                    \"description\": \"Returns information about all running processes.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"processInfo\",\n                            \"description\": \"An array of process info blocks.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ProcessInfo\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Target\",\n            \"description\": \"Supports additional targets discovery and allows to attach to them.\",\n            \"types\": [\n                {\n                    \"id\": \"TargetID\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"SessionID\",\n                    \"description\": \"Unique identifier of attached debugging session.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"TargetInfo\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"List of types: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypeTab%5B%5D%22\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"attached\",\n                            \"description\": \"Whether the target has an attached client.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"openerId\",\n                            \"description\": \"Opener target Id\",\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"canAccessOpener\",\n                            \"description\": \"Whether the target has access to the originating window.\",\n                            \"experimental\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"openerFrameId\",\n                            \"description\": \"Frame id of originating window (is only set if target has an opener).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"parentFrameId\",\n                            \"description\": \"Id of the parent frame, only present for the \\\"iframe\\\" targets.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Page.FrameId\"\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        },\n                        {\n                            \"name\": \"subtype\",\n                            \"description\": \"Provides additional details for specific target types. For example, for\\nthe type of \\\"page\\\", this may be set to \\\"prerender\\\".\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FilterEntry\",\n                    \"description\": \"A filter used by target query/discovery/auto-attach operations.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"exclude\",\n                            \"description\": \"If set, causes exclusion of matching targets from the list.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"If not present, matches any type.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"TargetFilter\",\n                    \"description\": \"The entries in TargetFilter are matched sequentially against targets and\\nthe first entry that matches determines if the target is included or not,\\ndepending on the value of `exclude` field in the entry.\\nIf filter is not specified, the one assumed is\\n[{type: \\\"browser\\\", exclude: true}, {type: \\\"tab\\\", exclude: true}, {}]\\n(i.e. include everything but `browser` and `tab`).\",\n                    \"experimental\": true,\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"FilterEntry\"\n                    }\n                },\n                {\n                    \"id\": \"RemoteLocation\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"host\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"port\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WindowState\",\n                    \"description\": \"The state of the target window.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"normal\",\n                        \"minimized\",\n                        \"maximized\",\n                        \"fullscreen\"\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"activateTarget\",\n                    \"description\": \"Activates (focuses) the target.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attachToTarget\",\n                    \"description\": \"Attaches to the target with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"flatten\",\n                            \"description\": \"Enables \\\"flat\\\" access to the session via specifying sessionId attribute in the commands.\\nWe plan to make this the default, deprecate non-flattened mode,\\nand eventually retire it. See crbug.com/991325.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Id assigned to the session.\",\n                            \"$ref\": \"SessionID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"attachToBrowserTarget\",\n                    \"description\": \"Attaches to the browser target, only uses flat sessionId mode.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Id assigned to the session.\",\n                            \"$ref\": \"SessionID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"closeTarget\",\n                    \"description\": \"Closes the target. If the target is a page that gets closed too.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"success\",\n                            \"description\": \"Always set to true. If an error occurs, the response indicates protocol error.\",\n                            \"deprecated\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"exposeDevToolsProtocol\",\n                    \"description\": \"Inject object to the target's main frame that provides a communication\\nchannel with browser target.\\n\\nInjected object will be available as `window[bindingName]`.\\n\\nThe object has the following API:\\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"bindingName\",\n                            \"description\": \"Binding name, 'cdp' if not specified.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"inheritPermissions\",\n                            \"description\": \"If true, inherits the current root session's permissions (default: false).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"createBrowserContext\",\n                    \"description\": \"Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\\none.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"disposeOnDetach\",\n                            \"description\": \"If specified, disposes this context when debugging session disconnects.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"proxyServer\",\n                            \"description\": \"Proxy server, similar to the one passed to --proxy-server\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"proxyBypassList\",\n                            \"description\": \"Proxy bypass list, similar to the one passed to --proxy-bypass-list\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"originsWithUniversalNetworkAccess\",\n                            \"description\": \"An optional list of origins to grant unlimited cross-origin access to.\\nParts of the URL other than those constituting origin are ignored.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"The id of the context created.\",\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getBrowserContexts\",\n                    \"description\": \"Returns all browser contexts created with `Target.createBrowserContext` method.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"browserContextIds\",\n                            \"description\": \"An array of browser context ids.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Browser.BrowserContextID\"\n                            }\n                        },\n                        {\n                            \"name\": \"defaultBrowserContextId\",\n                            \"description\": \"The id of the default browser context if available.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"createTarget\",\n                    \"description\": \"Creates a new page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"The initial URL the page will be navigated to. An empty string indicates about:blank.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"left\",\n                            \"description\": \"Frame left origin in DIP (requires newWindow to be true or headless shell).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"top\",\n                            \"description\": \"Frame top origin in DIP (requires newWindow to be true or headless shell).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"width\",\n                            \"description\": \"Frame width in DIP (requires newWindow to be true or headless shell).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"height\",\n                            \"description\": \"Frame height in DIP (requires newWindow to be true or headless shell).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"windowState\",\n                            \"description\": \"Frame window state (requires newWindow to be true or headless shell).\\nDefault is normal.\",\n                            \"optional\": true,\n                            \"$ref\": \"WindowState\"\n                        },\n                        {\n                            \"name\": \"browserContextId\",\n                            \"description\": \"The browser context to create the page in.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        },\n                        {\n                            \"name\": \"enableBeginFrameControl\",\n                            \"description\": \"Whether BeginFrames for this target will be controlled via DevTools (headless shell only,\\nnot supported on MacOS yet, false by default).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"newWindow\",\n                            \"description\": \"Whether to create a new Window or Tab (false by default, not supported by headless shell).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"background\",\n                            \"description\": \"Whether to create the target in background or foreground (false by default, not supported\\nby headless shell).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"forTab\",\n                            \"description\": \"Whether to create the target of type \\\"tab\\\".\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hidden\",\n                            \"description\": \"Whether to create a hidden target. The hidden target is observable via protocol, but not\\npresent in the tab UI strip. Cannot be created with `forTab: true`, `newWindow: true` or\\n`background: false`. The life-time of the tab is limited to the life-time of the session.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"focus\",\n                            \"description\": \"If specified, the option is used to determine if the new target should\\nbe focused or not. By default, the focus behavior depends on the\\nvalue of the background field. For example, background=false and focus=false\\nwill result in the target tab being opened but the browser window remain\\nunchanged (if it was in the background, it will remain in the background)\\nand background=false with focus=undefined will result in the window being focused.\\nUsing background: true and focus: true is not supported and will result in an error.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"The id of the page opened.\",\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"detachFromTarget\",\n                    \"description\": \"Detaches session with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Session to detach.\",\n                            \"optional\": true,\n                            \"$ref\": \"SessionID\"\n                        },\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"Deprecated.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disposeBrowserContext\",\n                    \"description\": \"Deletes a BrowserContext. All the belonging pages will be closed without calling their\\nbeforeunload hooks.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"browserContextId\",\n                            \"$ref\": \"Browser.BrowserContextID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getTargetInfo\",\n                    \"description\": \"Returns information about a target.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetInfo\",\n                            \"$ref\": \"TargetInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getTargets\",\n                    \"description\": \"Retrieves a list of available targets.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"filter\",\n                            \"description\": \"Only targets matching filter will be reported. If filter is not specified\\nand target discovery is currently enabled, a filter used for target discovery\\nis used for consistency.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetFilter\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetInfos\",\n                            \"description\": \"The list of targets.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"TargetInfo\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"sendMessageToTarget\",\n                    \"description\": \"Sends protocol message over session with given id.\\nConsider using flat mode instead; see commands attachToTarget, setAutoAttach,\\nand crbug.com/991325.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"message\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Identifier of the session.\",\n                            \"optional\": true,\n                            \"$ref\": \"SessionID\"\n                        },\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"Deprecated.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAutoAttach\",\n                    \"description\": \"Controls whether to automatically attach to new targets which are considered\\nto be directly related to this one (for example, iframes or workers).\\nWhen turned on, attaches to all existing related targets as well. When turned off,\\nautomatically detaches from all currently attached targets.\\nThis also clears all targets added by `autoAttachRelated` from the list of targets to watch\\nfor creation of related targets.\\nYou might want to call this recursively for auto-attached targets to attach\\nto all available targets.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"autoAttach\",\n                            \"description\": \"Whether to auto-attach to related targets.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"waitForDebuggerOnStart\",\n                            \"description\": \"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"flatten\",\n                            \"description\": \"Enables \\\"flat\\\" access to the session via specifying sessionId attribute in the commands.\\nWe plan to make this the default, deprecate non-flattened mode,\\nand eventually retire it. See crbug.com/991325.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"filter\",\n                            \"description\": \"Only targets matching filter will be attached.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetFilter\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"autoAttachRelated\",\n                    \"description\": \"Adds the specified target to the list of targets that will be monitored for any related target\\ncreation (such as child frames, child workers and new versions of service worker) and reported\\nthrough `attachedToTarget`. The specified target is also auto-attached.\\nThis cancels the effect of any previous `setAutoAttach` and is also cancelled by subsequent\\n`setAutoAttach`. Only available at the Browser target.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"waitForDebuggerOnStart\",\n                            \"description\": \"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"filter\",\n                            \"description\": \"Only targets matching filter will be attached.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetFilter\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setDiscoverTargets\",\n                    \"description\": \"Controls whether to discover available targets and notify via\\n`targetCreated/targetInfoChanged/targetDestroyed` events.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"discover\",\n                            \"description\": \"Whether to discover available targets.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"filter\",\n                            \"description\": \"Only targets matching filter will be attached. If `discover` is false,\\n`filter` must be omitted or empty.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetFilter\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setRemoteLocations\",\n                    \"description\": \"Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\\n`true`.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"locations\",\n                            \"description\": \"List of remote locations.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RemoteLocation\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getDevToolsTarget\",\n                    \"description\": \"Gets the targetId of the DevTools page target opened for the given target\\n(if any).\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"Page or tab target ID.\",\n                            \"$ref\": \"TargetID\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"The targetId of DevTools page target if exists.\",\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"openDevTools\",\n                    \"description\": \"Opens a DevTools window for the target.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"This can be the page or tab target ID.\",\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"panelId\",\n                            \"description\": \"The id of the panel we want DevTools to open initially. Currently\\nsupported panels are elements, console, network, sources, resources\\nand performance.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"The targetId of DevTools page target.\",\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"attachedToTarget\",\n                    \"description\": \"Issued when attached to target because of auto-attach or `attachToTarget` command.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Identifier assigned to the session used to send/receive messages.\",\n                            \"$ref\": \"SessionID\"\n                        },\n                        {\n                            \"name\": \"targetInfo\",\n                            \"$ref\": \"TargetInfo\"\n                        },\n                        {\n                            \"name\": \"waitingForDebugger\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"detachedFromTarget\",\n                    \"description\": \"Issued when detached from target for any reason (including `detachFromTarget` command). Can be\\nissued multiple times per target if multiple sessions have been attached to it.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Detached session identifier.\",\n                            \"$ref\": \"SessionID\"\n                        },\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"Deprecated.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"receivedMessageFromTarget\",\n                    \"description\": \"Notifies about a new protocol message received from the session (as reported in\\n`attachedToTarget` event).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"sessionId\",\n                            \"description\": \"Identifier of a session which sends a message.\",\n                            \"$ref\": \"SessionID\"\n                        },\n                        {\n                            \"name\": \"message\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"targetId\",\n                            \"description\": \"Deprecated.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"targetCreated\",\n                    \"description\": \"Issued when a possible inspection target is created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetInfo\",\n                            \"$ref\": \"TargetInfo\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"targetDestroyed\",\n                    \"description\": \"Issued when a target is destroyed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"targetCrashed\",\n                    \"description\": \"Issued when a target has crashed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetId\",\n                            \"$ref\": \"TargetID\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"description\": \"Termination status type.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"errorCode\",\n                            \"description\": \"Termination error code.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"targetInfoChanged\",\n                    \"description\": \"Issued when some information about a target has changed. This only happens between\\n`targetCreated` and `targetDestroyed`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"targetInfo\",\n                            \"$ref\": \"TargetInfo\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Tethering\",\n            \"description\": \"The Tethering domain defines methods and events for browser port binding.\",\n            \"experimental\": true,\n            \"commands\": [\n                {\n                    \"name\": \"bind\",\n                    \"description\": \"Request browser port binding.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"port\",\n                            \"description\": \"Port number to bind.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"unbind\",\n                    \"description\": \"Request browser port unbinding.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"port\",\n                            \"description\": \"Port number to unbind.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"accepted\",\n                    \"description\": \"Informs that port was successfully bound and got a specified connection id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"port\",\n                            \"description\": \"Port number that was successfully bound.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"connectionId\",\n                            \"description\": \"Connection id to be used.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Tracing\",\n            \"dependencies\": [\n                \"IO\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"MemoryDumpConfig\",\n                    \"description\": \"Configuration for memory dump. Used only when \\\"memory-infra\\\" category is enabled.\",\n                    \"experimental\": true,\n                    \"type\": \"object\"\n                },\n                {\n                    \"id\": \"TraceConfig\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"recordMode\",\n                            \"description\": \"Controls how the trace buffer stores data. The default is `recordUntilFull`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"recordUntilFull\",\n                                \"recordContinuously\",\n                                \"recordAsMuchAsPossible\",\n                                \"echoToConsole\"\n                            ]\n                        },\n                        {\n                            \"name\": \"traceBufferSizeInKb\",\n                            \"description\": \"Size of the trace buffer in kilobytes. If not specified or zero is passed, a default value\\nof 200 MB would be used.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"enableSampling\",\n                            \"description\": \"Turns on JavaScript stack sampling.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"enableSystrace\",\n                            \"description\": \"Turns on system tracing.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"enableArgumentFilter\",\n                            \"description\": \"Turns on argument filter.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includedCategories\",\n                            \"description\": \"Included category filters.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"excludedCategories\",\n                            \"description\": \"Excluded category filters.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"syntheticDelays\",\n                            \"description\": \"Configuration to synthesize the delays in tracing.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"memoryDumpConfig\",\n                            \"description\": \"Configuration for memory dump triggers. Used only when \\\"memory-infra\\\" category is enabled.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"MemoryDumpConfig\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StreamFormat\",\n                    \"description\": \"Data format of a trace. Can be either the legacy JSON format or the\\nprotocol buffer format. Note that the JSON format will be deprecated soon.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"json\",\n                        \"proto\"\n                    ]\n                },\n                {\n                    \"id\": \"StreamCompression\",\n                    \"description\": \"Compression type to use for traces returned via streams.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"none\",\n                        \"gzip\"\n                    ]\n                },\n                {\n                    \"id\": \"MemoryDumpLevelOfDetail\",\n                    \"description\": \"Details exposed when memory request explicitly declared.\\nKeep consistent with memory_dump_request_args.h and\\nmemory_instrumentation.mojom\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"background\",\n                        \"light\",\n                        \"detailed\"\n                    ]\n                },\n                {\n                    \"id\": \"TracingBackend\",\n                    \"description\": \"Backend type to use for tracing. `chrome` uses the Chrome-integrated\\ntracing service and is supported on all platforms. `system` is only\\nsupported on Chrome OS and uses the Perfetto system tracing service.\\n`auto` chooses `system` when the perfettoConfig provided to Tracing.start\\nspecifies at least one non-Chrome data source; otherwise uses `chrome`.\",\n                    \"experimental\": true,\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"auto\",\n                        \"chrome\",\n                        \"system\"\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"end\",\n                    \"description\": \"Stop trace events collection.\"\n                },\n                {\n                    \"name\": \"getCategories\",\n                    \"description\": \"Gets supported tracing categories.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"categories\",\n                            \"description\": \"A list of supported tracing categories.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getTrackEventDescriptor\",\n                    \"description\": \"Return a descriptor for all available tracing categories.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"descriptor\",\n                            \"description\": \"Base64-encoded serialized perfetto.protos.TrackEventDescriptor protobuf message. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"recordClockSyncMarker\",\n                    \"description\": \"Record a clock sync marker in the trace.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"syncId\",\n                            \"description\": \"The ID of this clock sync marker\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"requestMemoryDump\",\n                    \"description\": \"Request a global memory dump.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"deterministic\",\n                            \"description\": \"Enables more deterministic results by forcing garbage collection\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"levelOfDetail\",\n                            \"description\": \"Specifies level of details in memory dump. Defaults to \\\"detailed\\\".\",\n                            \"optional\": true,\n                            \"$ref\": \"MemoryDumpLevelOfDetail\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"dumpGuid\",\n                            \"description\": \"GUID of the resulting global memory dump.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"success\",\n                            \"description\": \"True iff the global memory dump succeeded.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"start\",\n                    \"description\": \"Start trace events collection.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"categories\",\n                            \"description\": \"Category/tag filter\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"options\",\n                            \"description\": \"Tracing options\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bufferUsageReportingInterval\",\n                            \"description\": \"If set, the agent will issue bufferUsage events at this interval, specified in milliseconds\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"transferMode\",\n                            \"description\": \"Whether to report trace events as series of dataCollected events or to save trace to a\\nstream (defaults to `ReportEvents`).\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"ReportEvents\",\n                                \"ReturnAsStream\"\n                            ]\n                        },\n                        {\n                            \"name\": \"streamFormat\",\n                            \"description\": \"Trace data format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `json`).\",\n                            \"optional\": true,\n                            \"$ref\": \"StreamFormat\"\n                        },\n                        {\n                            \"name\": \"streamCompression\",\n                            \"description\": \"Compression format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `none`)\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"StreamCompression\"\n                        },\n                        {\n                            \"name\": \"traceConfig\",\n                            \"optional\": true,\n                            \"$ref\": \"TraceConfig\"\n                        },\n                        {\n                            \"name\": \"perfettoConfig\",\n                            \"description\": \"Base64-encoded serialized perfetto.protos.TraceConfig protobuf message\\nWhen specified, the parameters `categories`, `options`, `traceConfig`\\nare ignored. (Encoded as a base64 string when passed over JSON)\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"tracingBackend\",\n                            \"description\": \"Backend type (defaults to `auto`)\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TracingBackend\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"bufferUsage\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"percentFull\",\n                            \"description\": \"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"eventCount\",\n                            \"description\": \"An approximate number of events in the trace log.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"dataCollected\",\n                    \"description\": \"Contains a bucket of collected trace events. When tracing is stopped collected events will be\\nsent as a sequence of dataCollected events followed by tracingComplete event.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"value\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"object\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"tracingComplete\",\n                    \"description\": \"Signals that tracing is stopped and there is no trace buffers pending flush, all data were\\ndelivered via dataCollected events.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"dataLossOccurred\",\n                            \"description\": \"Indicates whether some trace data is known to have been lost, e.g. because the trace ring\\nbuffer wrapped around.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"stream\",\n                            \"description\": \"A handle of the stream that holds resulting trace data.\",\n                            \"optional\": true,\n                            \"$ref\": \"IO.StreamHandle\"\n                        },\n                        {\n                            \"name\": \"traceFormat\",\n                            \"description\": \"Trace data format of returned stream.\",\n                            \"optional\": true,\n                            \"$ref\": \"StreamFormat\"\n                        },\n                        {\n                            \"name\": \"streamCompression\",\n                            \"description\": \"Compression format of returned stream.\",\n                            \"optional\": true,\n                            \"$ref\": \"StreamCompression\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"WebAudio\",\n            \"description\": \"This domain allows inspection of Web Audio API.\\nhttps://webaudio.github.io/web-audio-api/\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"GraphObjectId\",\n                    \"description\": \"An unique ID for a graph object (AudioContext, AudioNode, AudioParam) in Web Audio API\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ContextType\",\n                    \"description\": \"Enum of BaseAudioContext types\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"realtime\",\n                        \"offline\"\n                    ]\n                },\n                {\n                    \"id\": \"ContextState\",\n                    \"description\": \"Enum of AudioContextState from the spec\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"suspended\",\n                        \"running\",\n                        \"closed\",\n                        \"interrupted\"\n                    ]\n                },\n                {\n                    \"id\": \"NodeType\",\n                    \"description\": \"Enum of AudioNode types\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"ChannelCountMode\",\n                    \"description\": \"Enum of AudioNode::ChannelCountMode from the spec\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"clamped-max\",\n                        \"explicit\",\n                        \"max\"\n                    ]\n                },\n                {\n                    \"id\": \"ChannelInterpretation\",\n                    \"description\": \"Enum of AudioNode::ChannelInterpretation from the spec\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"discrete\",\n                        \"speakers\"\n                    ]\n                },\n                {\n                    \"id\": \"ParamType\",\n                    \"description\": \"Enum of AudioParam types\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"AutomationRate\",\n                    \"description\": \"Enum of AudioParam::AutomationRate from the spec\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"a-rate\",\n                        \"k-rate\"\n                    ]\n                },\n                {\n                    \"id\": \"ContextRealtimeData\",\n                    \"description\": \"Fields in AudioContext that change in real-time.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"currentTime\",\n                            \"description\": \"The current context time in second in BaseAudioContext.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"renderCapacity\",\n                            \"description\": \"The time spent on rendering graph divided by render quantum duration,\\nand multiplied by 100. 100 means the audio renderer reached the full\\ncapacity and glitch may occur.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"callbackIntervalMean\",\n                            \"description\": \"A running mean of callback interval.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"callbackIntervalVariance\",\n                            \"description\": \"A running variance of callback interval.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BaseAudioContext\",\n                    \"description\": \"Protocol object for BaseAudioContext\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"contextType\",\n                            \"$ref\": \"ContextType\"\n                        },\n                        {\n                            \"name\": \"contextState\",\n                            \"$ref\": \"ContextState\"\n                        },\n                        {\n                            \"name\": \"realtimeData\",\n                            \"optional\": true,\n                            \"$ref\": \"ContextRealtimeData\"\n                        },\n                        {\n                            \"name\": \"callbackBufferSize\",\n                            \"description\": \"Platform-dependent callback buffer size.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"maxOutputChannelCount\",\n                            \"description\": \"Number of output channels supported by audio hardware in use.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"sampleRate\",\n                            \"description\": \"Context sample rate.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AudioListener\",\n                    \"description\": \"Protocol object for AudioListener\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"listenerId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AudioNode\",\n                    \"description\": \"Protocol object for AudioNode\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"nodeType\",\n                            \"$ref\": \"NodeType\"\n                        },\n                        {\n                            \"name\": \"numberOfInputs\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"numberOfOutputs\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"channelCount\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"channelCountMode\",\n                            \"$ref\": \"ChannelCountMode\"\n                        },\n                        {\n                            \"name\": \"channelInterpretation\",\n                            \"$ref\": \"ChannelInterpretation\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"AudioParam\",\n                    \"description\": \"Protocol object for AudioParam\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"paramId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"paramType\",\n                            \"$ref\": \"ParamType\"\n                        },\n                        {\n                            \"name\": \"rate\",\n                            \"$ref\": \"AutomationRate\"\n                        },\n                        {\n                            \"name\": \"defaultValue\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"minValue\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"maxValue\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables the WebAudio domain and starts sending context lifetime events.\"\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables the WebAudio domain.\"\n                },\n                {\n                    \"name\": \"getRealtimeData\",\n                    \"description\": \"Fetch the realtime data from the registered contexts.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"realtimeData\",\n                            \"$ref\": \"ContextRealtimeData\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"contextCreated\",\n                    \"description\": \"Notifies that a new BaseAudioContext has been created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"context\",\n                            \"$ref\": \"BaseAudioContext\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"contextWillBeDestroyed\",\n                    \"description\": \"Notifies that an existing BaseAudioContext will be destroyed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"contextChanged\",\n                    \"description\": \"Notifies that existing BaseAudioContext has changed some properties (id stays the same)..\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"context\",\n                            \"$ref\": \"BaseAudioContext\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"audioListenerCreated\",\n                    \"description\": \"Notifies that the construction of an AudioListener has finished.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"listener\",\n                            \"$ref\": \"AudioListener\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"audioListenerWillBeDestroyed\",\n                    \"description\": \"Notifies that a new AudioListener has been created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"listenerId\",\n                            \"$ref\": \"GraphObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"audioNodeCreated\",\n                    \"description\": \"Notifies that a new AudioNode has been created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"node\",\n                            \"$ref\": \"AudioNode\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"audioNodeWillBeDestroyed\",\n                    \"description\": \"Notifies that an existing AudioNode has been destroyed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"GraphObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"audioParamCreated\",\n                    \"description\": \"Notifies that a new AudioParam has been created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"param\",\n                            \"$ref\": \"AudioParam\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"audioParamWillBeDestroyed\",\n                    \"description\": \"Notifies that an existing AudioParam has been destroyed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"paramId\",\n                            \"$ref\": \"GraphObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nodesConnected\",\n                    \"description\": \"Notifies that two AudioNodes are connected.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"destinationId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceOutputIndex\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"destinationInputIndex\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nodesDisconnected\",\n                    \"description\": \"Notifies that AudioNodes are disconnected. The destination can be null, and it means all the outgoing connections from the source are disconnected.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"destinationId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceOutputIndex\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"destinationInputIndex\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nodeParamConnected\",\n                    \"description\": \"Notifies that an AudioNode is connected to an AudioParam.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"destinationId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceOutputIndex\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nodeParamDisconnected\",\n                    \"description\": \"Notifies that an AudioNode is disconnected to an AudioParam.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"contextId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"destinationId\",\n                            \"$ref\": \"GraphObjectId\"\n                        },\n                        {\n                            \"name\": \"sourceOutputIndex\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"WebAuthn\",\n            \"description\": \"This domain allows configuring virtual authenticators to test the WebAuthn\\nAPI.\",\n            \"experimental\": true,\n            \"types\": [\n                {\n                    \"id\": \"AuthenticatorId\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"AuthenticatorProtocol\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"u2f\",\n                        \"ctap2\"\n                    ]\n                },\n                {\n                    \"id\": \"Ctap2Version\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"ctap2_0\",\n                        \"ctap2_1\"\n                    ]\n                },\n                {\n                    \"id\": \"AuthenticatorTransport\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"usb\",\n                        \"nfc\",\n                        \"ble\",\n                        \"cable\",\n                        \"internal\"\n                    ]\n                },\n                {\n                    \"id\": \"VirtualAuthenticatorOptions\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"protocol\",\n                            \"$ref\": \"AuthenticatorProtocol\"\n                        },\n                        {\n                            \"name\": \"ctap2Version\",\n                            \"description\": \"Defaults to ctap2_0. Ignored if |protocol| == u2f.\",\n                            \"optional\": true,\n                            \"$ref\": \"Ctap2Version\"\n                        },\n                        {\n                            \"name\": \"transport\",\n                            \"$ref\": \"AuthenticatorTransport\"\n                        },\n                        {\n                            \"name\": \"hasResidentKey\",\n                            \"description\": \"Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hasUserVerification\",\n                            \"description\": \"Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hasLargeBlob\",\n                            \"description\": \"If set to true, the authenticator will support the largeBlob extension.\\nhttps://w3c.github.io/webauthn#largeBlob\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hasCredBlob\",\n                            \"description\": \"If set to true, the authenticator will support the credBlob extension.\\nhttps://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hasMinPinLength\",\n                            \"description\": \"If set to true, the authenticator will support the minPinLength extension.\\nhttps://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-minpinlength-extension\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"hasPrf\",\n                            \"description\": \"If set to true, the authenticator will support the prf extension.\\nhttps://w3c.github.io/webauthn/#prf-extension\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"automaticPresenceSimulation\",\n                            \"description\": \"If set to true, tests of user presence will succeed immediately.\\nOtherwise, they will not be resolved. Defaults to true.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isUserVerified\",\n                            \"description\": \"Sets whether User Verification succeeds or fails for an authenticator.\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"defaultBackupEligibility\",\n                            \"description\": \"Credentials created by this authenticator will have the backup\\neligibility (BE) flag set to this value. Defaults to false.\\nhttps://w3c.github.io/webauthn/#sctn-credential-backup\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"defaultBackupState\",\n                            \"description\": \"Credentials created by this authenticator will have the backup state\\n(BS) flag set to this value. Defaults to false.\\nhttps://w3c.github.io/webauthn/#sctn-credential-backup\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Credential\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"credentialId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"isResidentCredential\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"rpId\",\n                            \"description\": \"Relying Party ID the credential is scoped to. Must be set when adding a\\ncredential.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"privateKey\",\n                            \"description\": \"The ECDSA P-256 private key in PKCS#8 format. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"userHandle\",\n                            \"description\": \"An opaque byte sequence with a maximum size of 64 bytes mapping the\\ncredential to a specific user. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"signCount\",\n                            \"description\": \"Signature counter. This is incremented by one for each successful\\nassertion.\\nSee https://w3c.github.io/webauthn/#signature-counter\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"largeBlob\",\n                            \"description\": \"The large blob associated with the credential.\\nSee https://w3c.github.io/webauthn/#sctn-large-blob-extension (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"backupEligibility\",\n                            \"description\": \"Assertions returned by this credential will have the backup eligibility\\n(BE) flag set to this value. Defaults to the authenticator's\\ndefaultBackupEligibility value.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"backupState\",\n                            \"description\": \"Assertions returned by this credential will have the backup state (BS)\\nflag set to this value. Defaults to the authenticator's\\ndefaultBackupState value.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"userName\",\n                            \"description\": \"The credential's user.name property. Equivalent to empty if not set.\\nhttps://w3c.github.io/webauthn/#dom-publickeycredentialentity-name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"userDisplayName\",\n                            \"description\": \"The credential's user.displayName property. Equivalent to empty if\\nnot set.\\nhttps://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enable the WebAuthn domain and start intercepting credential storage and\\nretrieval with a virtual authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"enableUI\",\n                            \"description\": \"Whether to enable the WebAuthn user interface. Enabling the UI is\\nrecommended for debugging and demo purposes, as it is closer to the real\\nexperience. Disabling the UI is recommended for automated testing.\\nSupported at the embedder's discretion if UI is available.\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disable the WebAuthn domain.\"\n                },\n                {\n                    \"name\": \"addVirtualAuthenticator\",\n                    \"description\": \"Creates and adds a virtual authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"options\",\n                            \"$ref\": \"VirtualAuthenticatorOptions\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setResponseOverrideBits\",\n                    \"description\": \"Resets parameters isBogusSignature, isBadUV, isBadUP to false if they are not present.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"isBogusSignature\",\n                            \"description\": \"If isBogusSignature is set, overrides the signature in the authenticator response to be zero.\\nDefaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isBadUV\",\n                            \"description\": \"If isBadUV is set, overrides the UV bit in the flags in the authenticator response to\\nbe zero. Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isBadUP\",\n                            \"description\": \"If isBadUP is set, overrides the UP bit in the flags in the authenticator response to\\nbe zero. Defaults to false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeVirtualAuthenticator\",\n                    \"description\": \"Removes the given authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"addCredential\",\n                    \"description\": \"Adds the credential to the specified authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credential\",\n                            \"$ref\": \"Credential\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getCredential\",\n                    \"description\": \"Returns a single credential stored in the given virtual authenticator that\\nmatches the credential ID.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credentialId\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"credential\",\n                            \"$ref\": \"Credential\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getCredentials\",\n                    \"description\": \"Returns all the credentials stored in the given virtual authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"credentials\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Credential\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeCredential\",\n                    \"description\": \"Removes a credential from the authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credentialId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"clearCredentials\",\n                    \"description\": \"Clears all the credentials from the specified device.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setUserVerified\",\n                    \"description\": \"Sets whether User Verification succeeds or fails for an authenticator.\\nThe default is true.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"isUserVerified\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAutomaticPresenceSimulation\",\n                    \"description\": \"Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator.\\nThe default is true.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"enabled\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCredentialProperties\",\n                    \"description\": \"Allows setting credential properties.\\nhttps://w3c.github.io/webauthn/#sctn-automation-set-credential-properties\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credentialId\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"backupEligibility\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"backupState\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"credentialAdded\",\n                    \"description\": \"Triggered when a credential is added to an authenticator.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credential\",\n                            \"$ref\": \"Credential\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"credentialDeleted\",\n                    \"description\": \"Triggered when a credential is deleted, e.g. through\\nPublicKeyCredential.signalUnknownCredential().\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credentialId\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"credentialUpdated\",\n                    \"description\": \"Triggered when a credential is updated, e.g. through\\nPublicKeyCredential.signalCurrentUserDetails().\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credential\",\n                            \"$ref\": \"Credential\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"credentialAsserted\",\n                    \"description\": \"Triggered when a credential is used in a webauthn assertion.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"authenticatorId\",\n                            \"$ref\": \"AuthenticatorId\"\n                        },\n                        {\n                            \"name\": \"credential\",\n                            \"$ref\": \"Credential\"\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}"
  },
  {
    "path": "cli/cdp-protocol/js_protocol.json",
    "content": "{\n    \"version\": {\n        \"major\": \"1\",\n        \"minor\": \"3\"\n    },\n    \"domains\": [\n        {\n            \"domain\": \"Console\",\n            \"description\": \"This domain is deprecated - use Runtime or Log instead.\",\n            \"deprecated\": true,\n            \"dependencies\": [\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"ConsoleMessage\",\n                    \"description\": \"Console message.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"source\",\n                            \"description\": \"Message source.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"xml\",\n                                \"javascript\",\n                                \"network\",\n                                \"console-api\",\n                                \"storage\",\n                                \"appcache\",\n                                \"rendering\",\n                                \"security\",\n                                \"other\",\n                                \"deprecation\",\n                                \"worker\"\n                            ]\n                        },\n                        {\n                            \"name\": \"level\",\n                            \"description\": \"Message severity.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"log\",\n                                \"warning\",\n                                \"error\",\n                                \"debug\",\n                                \"info\"\n                            ]\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Message text.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the message origin.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"line\",\n                            \"description\": \"Line number in the resource that generated this message (1-based).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"column\",\n                            \"description\": \"Column number in the resource that generated this message (1-based).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"clearMessages\",\n                    \"description\": \"Does nothing.\"\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables console domain, prevents further console messages from being reported to the client.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables console domain, sends the messages collected so far to the client by means of the\\n`messageAdded` notification.\"\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"messageAdded\",\n                    \"description\": \"Issued when new console message is added.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"message\",\n                            \"description\": \"Console message that has been added.\",\n                            \"$ref\": \"ConsoleMessage\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Debugger\",\n            \"description\": \"Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\\nbreakpoints, stepping through execution, exploring stack traces, etc.\",\n            \"dependencies\": [\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"BreakpointId\",\n                    \"description\": \"Breakpoint identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"CallFrameId\",\n                    \"description\": \"Call frame identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"Location\",\n                    \"description\": \"Location in the source code.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Script identifier as reported in the `Debugger.scriptParsed`.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number in the script (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Column number in the script (0-based).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScriptPosition\",\n                    \"description\": \"Location in the source code.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"lineNumber\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"LocationRange\",\n                    \"description\": \"Location range within one script.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"start\",\n                            \"$ref\": \"ScriptPosition\"\n                        },\n                        {\n                            \"name\": \"end\",\n                            \"$ref\": \"ScriptPosition\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CallFrame\",\n                    \"description\": \"JavaScript call frame. Array of call frames form the call stack.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"callFrameId\",\n                            \"description\": \"Call frame identifier. This identifier is only valid while the virtual machine is paused.\",\n                            \"$ref\": \"CallFrameId\"\n                        },\n                        {\n                            \"name\": \"functionName\",\n                            \"description\": \"Name of the JavaScript function called on this call frame.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"functionLocation\",\n                            \"description\": \"Location in the source code.\",\n                            \"optional\": true,\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Location in the source code.\",\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"JavaScript script name or url.\\nDeprecated in favor of using the `location.scriptId` to resolve the URL via a previously\\nsent `Debugger.scriptParsed` event.\",\n                            \"deprecated\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scopeChain\",\n                            \"description\": \"Scope chain for this call frame.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Scope\"\n                            }\n                        },\n                        {\n                            \"name\": \"this\",\n                            \"description\": \"`this` object for this call frame.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"returnValue\",\n                            \"description\": \"The value being returned, if the function is at return point.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"canBeRestarted\",\n                            \"description\": \"Valid only while the VM is paused and indicates whether this frame\\ncan be restarted or not. Note that a `true` value here does not\\nguarantee that Debugger#restartFrame with this CallFrameId will be\\nsuccessful, but it is very likely.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Scope\",\n                    \"description\": \"Scope description.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Scope type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"global\",\n                                \"local\",\n                                \"with\",\n                                \"closure\",\n                                \"catch\",\n                                \"block\",\n                                \"script\",\n                                \"eval\",\n                                \"module\",\n                                \"wasm-expression-stack\"\n                            ]\n                        },\n                        {\n                            \"name\": \"object\",\n                            \"description\": \"Object representing the scope. For `global` and `with` scopes it represents the actual\\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\\nvariables as its properties.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"startLocation\",\n                            \"description\": \"Location in the source code where scope starts\",\n                            \"optional\": true,\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"endLocation\",\n                            \"description\": \"Location in the source code where scope ends\",\n                            \"optional\": true,\n                            \"$ref\": \"Location\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SearchMatch\",\n                    \"description\": \"Search match for resource.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number in resource content.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"lineContent\",\n                            \"description\": \"Line with match content.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"BreakLocation\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Script identifier as reported in the `Debugger.scriptParsed`.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number in the script (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Column number in the script (0-based).\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"debuggerStatement\",\n                                \"call\",\n                                \"return\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"WasmDisassemblyChunk\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"lines\",\n                            \"description\": \"The next chunk of disassembled lines.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"bytecodeOffsets\",\n                            \"description\": \"The bytecode offsets describing the start of each line.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScriptLanguage\",\n                    \"description\": \"Enum of possible script languages.\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"JavaScript\",\n                        \"WebAssembly\"\n                    ]\n                },\n                {\n                    \"id\": \"DebugSymbols\",\n                    \"description\": \"Debug symbols available for a wasm script.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the debug symbols.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"SourceMap\",\n                                \"EmbeddedDWARF\",\n                                \"ExternalDWARF\"\n                            ]\n                        },\n                        {\n                            \"name\": \"externalURL\",\n                            \"description\": \"URL of the external symbol source.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ResolvedBreakpoint\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"description\": \"Breakpoint unique identifier.\",\n                            \"$ref\": \"BreakpointId\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Actual breakpoint location.\",\n                            \"$ref\": \"Location\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"continueToLocation\",\n                    \"description\": \"Continues execution until specific location is reached.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Location to continue to.\",\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"targetCallFrames\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"any\",\n                                \"current\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables debugger for given page.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables debugger for the given page. Clients should not assume that the debugging has been\\nenabled until the result for this command is received.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"maxScriptsCacheSize\",\n                            \"description\": \"The maximum size in bytes of collected scripts (not referenced by other heap objects)\\nthe debugger can hold. Puts no limit if parameter is omitted.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"debuggerId\",\n                            \"description\": \"Unique identifier of the debugger.\",\n                            \"experimental\": true,\n                            \"$ref\": \"Runtime.UniqueDebuggerId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"evaluateOnCallFrame\",\n                    \"description\": \"Evaluates expression on a given call frame.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"callFrameId\",\n                            \"description\": \"Call frame identifier to evaluate on.\",\n                            \"$ref\": \"CallFrameId\"\n                        },\n                        {\n                            \"name\": \"expression\",\n                            \"description\": \"Expression to evaluate.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"String object group name to put result into (allows rapid releasing resulting object handles\\nusing `releaseObjectGroup`).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"includeCommandLineAPI\",\n                            \"description\": \"Specifies whether command line API should be available to the evaluated expression, defaults\\nto false.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"silent\",\n                            \"description\": \"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"returnByValue\",\n                            \"description\": \"Whether the result is expected to be a JSON object that should be sent by value.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generatePreview\",\n                            \"description\": \"Whether preview should be generated for the result.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"throwOnSideEffect\",\n                            \"description\": \"Whether to throw an exception if side effect cannot be ruled out during evaluation.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"timeout\",\n                            \"description\": \"Terminate execution after timing out (number of milliseconds).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.TimeDelta\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Object wrapper for the evaluation result.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getPossibleBreakpoints\",\n                    \"description\": \"Returns possible locations for breakpoint. scriptId in start and end range locations should be\\nthe same.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"start\",\n                            \"description\": \"Start of range to search possible breakpoint locations in.\",\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"end\",\n                            \"description\": \"End of range to search possible breakpoint locations in (excluding). When not specified, end\\nof scripts is used as end of range.\",\n                            \"optional\": true,\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"restrictToFunction\",\n                            \"description\": \"Only consider locations which are in the same (non-nested) function as start.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"locations\",\n                            \"description\": \"List of the possible breakpoint locations.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"BreakLocation\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getScriptSource\",\n                    \"description\": \"Returns source for the script with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script to get source for.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"scriptSource\",\n                            \"description\": \"Script source (empty in case of Wasm bytecode).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bytecode\",\n                            \"description\": \"Wasm bytecode. (Encoded as a base64 string when passed over JSON)\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disassembleWasmModule\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script to disassemble\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"streamId\",\n                            \"description\": \"For large modules, return a stream from which additional chunks of\\ndisassembly can be read successively.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"totalNumberOfLines\",\n                            \"description\": \"The total number of lines in the disassembly text.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"functionBodyOffsets\",\n                            \"description\": \"The offsets of all function bodies, in the format [start1, end1,\\nstart2, end2, ...] where all ends are exclusive.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"chunk\",\n                            \"description\": \"The first chunk of disassembly.\",\n                            \"$ref\": \"WasmDisassemblyChunk\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"nextWasmDisassemblyChunk\",\n                    \"description\": \"Disassemble the next chunk of lines for the module corresponding to the\\nstream. If disassembly is complete, this API will invalidate the streamId\\nand return an empty chunk. Any subsequent calls for the now invalid stream\\nwill return errors.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"streamId\",\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"chunk\",\n                            \"description\": \"The next chunk of disassembly.\",\n                            \"$ref\": \"WasmDisassemblyChunk\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getWasmBytecode\",\n                    \"description\": \"This command is deprecated. Use getScriptSource instead.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the Wasm script to get source for.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"bytecode\",\n                            \"description\": \"Script source. (Encoded as a base64 string when passed over JSON)\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getStackTrace\",\n                    \"description\": \"Returns stack trace with given `stackTraceId`.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"stackTraceId\",\n                            \"$ref\": \"Runtime.StackTraceId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"stackTrace\",\n                            \"$ref\": \"Runtime.StackTrace\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"pause\",\n                    \"description\": \"Stops on the next JavaScript statement.\"\n                },\n                {\n                    \"name\": \"pauseOnAsyncCall\",\n                    \"experimental\": true,\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"parentStackTraceId\",\n                            \"description\": \"Debugger will pause when async call with given stack trace is started.\",\n                            \"$ref\": \"Runtime.StackTraceId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeBreakpoint\",\n                    \"description\": \"Removes JavaScript breakpoint.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"$ref\": \"BreakpointId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"restartFrame\",\n                    \"description\": \"Restarts particular call frame from the beginning. The old, deprecated\\nbehavior of `restartFrame` is to stay paused and allow further CDP commands\\nafter a restart was scheduled. This can cause problems with restarting, so\\nwe now continue execution immediatly after it has been scheduled until we\\nreach the beginning of the restarted frame.\\n\\nTo stay back-wards compatible, `restartFrame` now expects a `mode`\\nparameter to be present. If the `mode` parameter is missing, `restartFrame`\\nerrors out.\\n\\nThe various return values are deprecated and `callFrames` is always empty.\\nUse the call frames from the `Debugger#paused` events instead, that fires\\nonce V8 pauses at the beginning of the restarted function.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"callFrameId\",\n                            \"description\": \"Call frame identifier to evaluate on.\",\n                            \"$ref\": \"CallFrameId\"\n                        },\n                        {\n                            \"name\": \"mode\",\n                            \"description\": \"The `mode` parameter must be present and set to 'StepInto', otherwise\\n`restartFrame` will error out.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"StepInto\"\n                            ]\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"callFrames\",\n                            \"description\": \"New stack trace.\",\n                            \"deprecated\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CallFrame\"\n                            }\n                        },\n                        {\n                            \"name\": \"asyncStackTrace\",\n                            \"description\": \"Async stack trace, if any.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"asyncStackTraceId\",\n                            \"description\": \"Async stack trace, if any.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTraceId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resume\",\n                    \"description\": \"Resumes JavaScript execution.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"terminateOnResume\",\n                            \"description\": \"Set to true to terminate execution upon resuming execution. In contrast\\nto Runtime.terminateExecution, this will allows to execute further\\nJavaScript (i.e. via evaluation) until execution of the paused code\\nis actually resumed, at which point termination is triggered.\\nIf execution is currently not paused, this parameter has no effect.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"searchInContent\",\n                    \"description\": \"Searches for given string in script content.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script to search in.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"query\",\n                            \"description\": \"String to search for.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"caseSensitive\",\n                            \"description\": \"If true, search is case sensitive.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isRegex\",\n                            \"description\": \"If true, treats string parameter as regex.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"List of search matches.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SearchMatch\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAsyncCallStackDepth\",\n                    \"description\": \"Enables or disables async call stacks tracking.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"maxDepth\",\n                            \"description\": \"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBlackboxExecutionContexts\",\n                    \"description\": \"Replace previous blackbox execution contexts with passed ones. Forces backend to skip\\nstepping/pausing in scripts in these execution contexts. VM will try to leave blackboxed script by\\nperforming 'step in' several times, finally resorting to 'step out' if unsuccessful.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"uniqueIds\",\n                            \"description\": \"Array of execution context unique ids for the debugger to ignore.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBlackboxPatterns\",\n                    \"description\": \"Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\\nperforming 'step in' several times, finally resorting to 'step out' if unsuccessful.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"patterns\",\n                            \"description\": \"Array of regexps that will be used to check script url for blackbox state.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"skipAnonymous\",\n                            \"description\": \"If true, also ignore scripts with no source url.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBlackboxedRanges\",\n                    \"description\": \"Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\\nscripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful.\\nPositions array contains positions where blackbox state is changed. First interval isn't\\nblackboxed. Array should be sorted.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"positions\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScriptPosition\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBreakpoint\",\n                    \"description\": \"Sets JavaScript breakpoint at a given location.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Location to set breakpoint in.\",\n                            \"$ref\": \"Location\"\n                        },\n                        {\n                            \"name\": \"condition\",\n                            \"description\": \"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"description\": \"Id of the created breakpoint for further reference.\",\n                            \"$ref\": \"BreakpointId\"\n                        },\n                        {\n                            \"name\": \"actualLocation\",\n                            \"description\": \"Location this breakpoint resolved into.\",\n                            \"$ref\": \"Location\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setInstrumentationBreakpoint\",\n                    \"description\": \"Sets instrumentation breakpoint.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"instrumentation\",\n                            \"description\": \"Instrumentation name.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"beforeScriptExecution\",\n                                \"beforeScriptWithSourceMapExecution\"\n                            ]\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"description\": \"Id of the created breakpoint for further reference.\",\n                            \"$ref\": \"BreakpointId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBreakpointByUrl\",\n                    \"description\": \"Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\\n`locations` property. Further matching script parsing will result in subsequent\\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number to set breakpoint at.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the resources to set breakpoint on.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"urlRegex\",\n                            \"description\": \"Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\\n`urlRegex` must be specified.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scriptHash\",\n                            \"description\": \"Script hash of the resources to set breakpoint on.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Offset in the line to set breakpoint at.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"condition\",\n                            \"description\": \"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"description\": \"Id of the created breakpoint for further reference.\",\n                            \"$ref\": \"BreakpointId\"\n                        },\n                        {\n                            \"name\": \"locations\",\n                            \"description\": \"List of the locations this breakpoint resolved into upon addition.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Location\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBreakpointOnFunctionCall\",\n                    \"description\": \"Sets JavaScript breakpoint before each call to the given function.\\nIf another function was created from the same source as a given one,\\ncalling it will also trigger the breakpoint.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Function object id.\",\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"condition\",\n                            \"description\": \"Expression to use as a breakpoint condition. When specified, debugger will\\nstop on the breakpoint if this expression evaluates to true.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"description\": \"Id of the created breakpoint for further reference.\",\n                            \"$ref\": \"BreakpointId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setBreakpointsActive\",\n                    \"description\": \"Activates / deactivates all breakpoints on the page.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"active\",\n                            \"description\": \"New value for breakpoints active state.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setPauseOnExceptions\",\n                    \"description\": \"Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions,\\nor caught exceptions, no exceptions. Initial pause on exceptions state is `none`.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"state\",\n                            \"description\": \"Pause on exceptions mode.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"none\",\n                                \"caught\",\n                                \"uncaught\",\n                                \"all\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setReturnValue\",\n                    \"description\": \"Changes return value in top frame. Available only at return break position.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"newValue\",\n                            \"description\": \"New return value.\",\n                            \"$ref\": \"Runtime.CallArgument\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setScriptSource\",\n                    \"description\": \"Edits JavaScript source live.\\n\\nIn general, functions that are currently on the stack can not be edited with\\na single exception: If the edited function is the top-most stack frame and\\nthat is the only activation of that function on the stack. In this case\\nthe live edit will be successful and a `Debugger.restartFrame` for the\\ntop-most function is automatically triggered.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script to edit.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"scriptSource\",\n                            \"description\": \"New content of the script.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"dryRun\",\n                            \"description\": \"If true the change will not actually be applied. Dry run may be used to get result\\ndescription without actually modifying the code.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"allowTopFrameEditing\",\n                            \"description\": \"If true, then `scriptSource` is allowed to change the function on top of the stack\\nas long as the top-most stack frame is the only activation of that function.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"callFrames\",\n                            \"description\": \"New stack trace in case editing has happened while VM was stopped.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CallFrame\"\n                            }\n                        },\n                        {\n                            \"name\": \"stackChanged\",\n                            \"description\": \"Whether current call stack  was modified after applying the changes.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"asyncStackTrace\",\n                            \"description\": \"Async stack trace, if any.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"asyncStackTraceId\",\n                            \"description\": \"Async stack trace, if any.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTraceId\"\n                        },\n                        {\n                            \"name\": \"status\",\n                            \"description\": \"Whether the operation was successful or not. Only `Ok` denotes a\\nsuccessful live edit while the other enum variants denote why\\nthe live edit failed.\",\n                            \"experimental\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"Ok\",\n                                \"CompileError\",\n                                \"BlockedByActiveGenerator\",\n                                \"BlockedByActiveFunction\",\n                                \"BlockedByTopLevelEsModuleChange\"\n                            ]\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details if any. Only present when `status` is `CompileError`.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSkipAllPauses\",\n                    \"description\": \"Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"skip\",\n                            \"description\": \"New value for skip pauses state.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setVariableValue\",\n                    \"description\": \"Changes value of variable in a callframe. Object-based scopes are not supported and must be\\nmutated manually.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scopeNumber\",\n                            \"description\": \"0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch'\\nscope types are allowed. Other scopes could be manipulated manually.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"variableName\",\n                            \"description\": \"Variable name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"newValue\",\n                            \"description\": \"New variable value.\",\n                            \"$ref\": \"Runtime.CallArgument\"\n                        },\n                        {\n                            \"name\": \"callFrameId\",\n                            \"description\": \"Id of callframe that holds variable.\",\n                            \"$ref\": \"CallFrameId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stepInto\",\n                    \"description\": \"Steps into the function call.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"breakOnAsyncCall\",\n                            \"description\": \"Debugger will pause on the execution of the first async task which was scheduled\\nbefore next pause.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"skipList\",\n                            \"description\": \"The skipList specifies location ranges that should be skipped on step into.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"LocationRange\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stepOut\",\n                    \"description\": \"Steps out of the function call.\"\n                },\n                {\n                    \"name\": \"stepOver\",\n                    \"description\": \"Steps over the statement.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"skipList\",\n                            \"description\": \"The skipList specifies location ranges that should be skipped on step over.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"LocationRange\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"breakpointResolved\",\n                    \"description\": \"Fired when breakpoint is resolved to an actual script and location.\\nDeprecated in favor of `resolvedBreakpoints` in the `scriptParsed` event.\",\n                    \"deprecated\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"breakpointId\",\n                            \"description\": \"Breakpoint unique identifier.\",\n                            \"$ref\": \"BreakpointId\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Actual breakpoint location.\",\n                            \"$ref\": \"Location\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"paused\",\n                    \"description\": \"Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"callFrames\",\n                            \"description\": \"Call stack the virtual machine stopped on.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CallFrame\"\n                            }\n                        },\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"Pause reason.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"ambiguous\",\n                                \"assert\",\n                                \"CSPViolation\",\n                                \"debugCommand\",\n                                \"DOM\",\n                                \"EventListener\",\n                                \"exception\",\n                                \"instrumentation\",\n                                \"OOM\",\n                                \"other\",\n                                \"promiseRejection\",\n                                \"XHR\",\n                                \"step\"\n                            ]\n                        },\n                        {\n                            \"name\": \"data\",\n                            \"description\": \"Object containing break-specific auxiliary properties.\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"hitBreakpoints\",\n                            \"description\": \"Hit breakpoints IDs\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        },\n                        {\n                            \"name\": \"asyncStackTrace\",\n                            \"description\": \"Async stack trace, if any.\",\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"asyncStackTraceId\",\n                            \"description\": \"Async stack trace, if any.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTraceId\"\n                        },\n                        {\n                            \"name\": \"asyncCallStackTraceId\",\n                            \"description\": \"Never present, will be removed.\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTraceId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resumed\",\n                    \"description\": \"Fired when the virtual machine resumed execution.\"\n                },\n                {\n                    \"name\": \"scriptFailedToParse\",\n                    \"description\": \"Fired when virtual machine fails to parse the script.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Identifier of the script parsed.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL or name of the script parsed (if any).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"startLine\",\n                            \"description\": \"Line offset of the script within the resource with given URL (for script tags).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"startColumn\",\n                            \"description\": \"Column offset of the script within the resource with given URL.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endLine\",\n                            \"description\": \"Last line of the script.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endColumn\",\n                            \"description\": \"Length of the last line of the script.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Specifies script creation context.\",\n                            \"$ref\": \"Runtime.ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"hash\",\n                            \"description\": \"Content hash of the script, SHA-256.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"buildId\",\n                            \"description\": \"For Wasm modules, the content of the `build_id` custom section. For JavaScript the `debugId` magic comment.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"executionContextAuxData\",\n                            \"description\": \"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"sourceMapURL\",\n                            \"description\": \"URL of source map associated with script (if any).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"hasSourceURL\",\n                            \"description\": \"True, if this script has sourceURL.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isModule\",\n                            \"description\": \"True, if this script is ES6 module.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"length\",\n                            \"description\": \"This script length.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"stackTrace\",\n                            \"description\": \"JavaScript top stack frame of where the script parsed event was triggered if available.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"codeOffset\",\n                            \"description\": \"If the scriptLanguage is WebAssembly, the code section offset in the module.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"scriptLanguage\",\n                            \"description\": \"The language of the script.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Debugger.ScriptLanguage\"\n                        },\n                        {\n                            \"name\": \"embedderName\",\n                            \"description\": \"The name the embedder supplied for this script.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"scriptParsed\",\n                    \"description\": \"Fired when virtual machine parses script. This event is also fired for all known and uncollected\\nscripts upon enabling debugger.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Identifier of the script parsed.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL or name of the script parsed (if any).\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"startLine\",\n                            \"description\": \"Line offset of the script within the resource with given URL (for script tags).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"startColumn\",\n                            \"description\": \"Column offset of the script within the resource with given URL.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endLine\",\n                            \"description\": \"Last line of the script.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endColumn\",\n                            \"description\": \"Length of the last line of the script.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Specifies script creation context.\",\n                            \"$ref\": \"Runtime.ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"hash\",\n                            \"description\": \"Content hash of the script, SHA-256.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"buildId\",\n                            \"description\": \"For Wasm modules, the content of the `build_id` custom section. For JavaScript the `debugId` magic comment.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"executionContextAuxData\",\n                            \"description\": \"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"isLiveEdit\",\n                            \"description\": \"True, if this script is generated as a result of the live edit operation.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"sourceMapURL\",\n                            \"description\": \"URL of source map associated with script (if any).\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"hasSourceURL\",\n                            \"description\": \"True, if this script has sourceURL.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isModule\",\n                            \"description\": \"True, if this script is ES6 module.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"length\",\n                            \"description\": \"This script length.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"stackTrace\",\n                            \"description\": \"JavaScript top stack frame of where the script parsed event was triggered if available.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Runtime.StackTrace\"\n                        },\n                        {\n                            \"name\": \"codeOffset\",\n                            \"description\": \"If the scriptLanguage is WebAssembly, the code section offset in the module.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"scriptLanguage\",\n                            \"description\": \"The language of the script.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"Debugger.ScriptLanguage\"\n                        },\n                        {\n                            \"name\": \"debugSymbols\",\n                            \"description\": \"If the scriptLanguage is WebAssembly, the source of debug symbols for the module.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Debugger.DebugSymbols\"\n                            }\n                        },\n                        {\n                            \"name\": \"embedderName\",\n                            \"description\": \"The name the embedder supplied for this script.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"resolvedBreakpoints\",\n                            \"description\": \"The list of set breakpoints in this script if calls to `setBreakpointByUrl`\\nmatches this script's URL or hash. Clients that use this list can ignore the\\n`breakpointResolved` event. They are equivalent.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ResolvedBreakpoint\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"HeapProfiler\",\n            \"experimental\": true,\n            \"dependencies\": [\n                \"Runtime\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"HeapSnapshotObjectId\",\n                    \"description\": \"Heap snapshot object id.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"SamplingHeapProfileNode\",\n                    \"description\": \"Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"callFrame\",\n                            \"description\": \"Function location.\",\n                            \"$ref\": \"Runtime.CallFrame\"\n                        },\n                        {\n                            \"name\": \"selfSize\",\n                            \"description\": \"Allocations size in bytes for the node excluding children.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Node id. Ids are unique across all profiles collected between startSampling and stopSampling.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"children\",\n                            \"description\": \"Child nodes.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SamplingHeapProfileNode\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SamplingHeapProfileSample\",\n                    \"description\": \"A single sample from a sampling profile.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"size\",\n                            \"description\": \"Allocation size in bytes attributed to the sample.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"nodeId\",\n                            \"description\": \"Id of the corresponding profile tree node.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"ordinal\",\n                            \"description\": \"Time-ordered sample ordinal number. It is unique across all profiles retrieved\\nbetween startSampling and stopSampling.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"SamplingHeapProfile\",\n                    \"description\": \"Sampling profile.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"head\",\n                            \"$ref\": \"SamplingHeapProfileNode\"\n                        },\n                        {\n                            \"name\": \"samples\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"SamplingHeapProfileSample\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"addInspectedHeapObject\",\n                    \"description\": \"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"heapObjectId\",\n                            \"description\": \"Heap snapshot object id to be accessible by means of $x command line API.\",\n                            \"$ref\": \"HeapSnapshotObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"collectGarbage\"\n                },\n                {\n                    \"name\": \"disable\"\n                },\n                {\n                    \"name\": \"enable\"\n                },\n                {\n                    \"name\": \"getHeapObjectId\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Identifier of the object to get heap object id for.\",\n                            \"$ref\": \"Runtime.RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"heapSnapshotObjectId\",\n                            \"description\": \"Id of the heap snapshot object corresponding to the passed remote object id.\",\n                            \"$ref\": \"HeapSnapshotObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getObjectByHeapObjectId\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"$ref\": \"HeapSnapshotObjectId\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic group name that can be used to release multiple objects.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Evaluation result.\",\n                            \"$ref\": \"Runtime.RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getSamplingProfile\",\n                    \"returns\": [\n                        {\n                            \"name\": \"profile\",\n                            \"description\": \"Return the sampling profile being collected.\",\n                            \"$ref\": \"SamplingHeapProfile\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startSampling\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"samplingInterval\",\n                            \"description\": \"Average sample interval in bytes. Poisson distribution is used for the intervals. The\\ndefault value is 32768 bytes.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"stackDepth\",\n                            \"description\": \"Maximum stack depth. The default value is 128.\",\n                            \"optional\": true,\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"includeObjectsCollectedByMajorGC\",\n                            \"description\": \"By default, the sampling heap profiler reports only objects which are\\nstill alive when the profile is returned via getSamplingProfile or\\nstopSampling, which is useful for determining what functions contribute\\nthe most to steady-state memory usage. This flag instructs the sampling\\nheap profiler to also include information about objects discarded by\\nmajor GC, which will show which functions cause large temporary memory\\nusage or long GC pauses.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeObjectsCollectedByMinorGC\",\n                            \"description\": \"By default, the sampling heap profiler reports only objects which are\\nstill alive when the profile is returned via getSamplingProfile or\\nstopSampling, which is useful for determining what functions contribute\\nthe most to steady-state memory usage. This flag instructs the sampling\\nheap profiler to also include information about objects discarded by\\nminor GC, which is useful when tuning a latency-sensitive application\\nfor minimal GC activity.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"startTrackingHeapObjects\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"trackAllocations\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopSampling\",\n                    \"returns\": [\n                        {\n                            \"name\": \"profile\",\n                            \"description\": \"Recorded sampling heap profile.\",\n                            \"$ref\": \"SamplingHeapProfile\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopTrackingHeapObjects\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"reportProgress\",\n                            \"description\": \"If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken\\nwhen the tracking is stopped.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"treatGlobalObjectsAsRoots\",\n                            \"description\": \"Deprecated in favor of `exposeInternals`.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"captureNumericValue\",\n                            \"description\": \"If true, numerical values are included in the snapshot\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"exposeInternals\",\n                            \"description\": \"If true, exposes internals of the snapshot.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"takeHeapSnapshot\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"reportProgress\",\n                            \"description\": \"If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"treatGlobalObjectsAsRoots\",\n                            \"description\": \"If true, a raw snapshot without artificial roots will be generated.\\nDeprecated in favor of `exposeInternals`.\",\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"captureNumericValue\",\n                            \"description\": \"If true, numerical values are included in the snapshot\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"exposeInternals\",\n                            \"description\": \"If true, exposes internals of the snapshot.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"addHeapSnapshotChunk\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"chunk\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"heapStatsUpdate\",\n                    \"description\": \"If heap objects tracking has been started then backend may send update for one or more fragments\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"statsUpdate\",\n                            \"description\": \"An array of triplets. Each triplet describes a fragment. The first integer is the fragment\\nindex, the second integer is a total count of objects for the fragment, the third integer is\\na total size of the objects for the fragment.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"lastSeenObjectId\",\n                    \"description\": \"If heap objects tracking has been started then backend regularly sends a current value for last\\nseen object id and corresponding timestamp. If the were changes in the heap since last event\\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"lastSeenObjectId\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"reportHeapSnapshotProgress\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"done\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"total\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"finished\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"resetProfiles\"\n                }\n            ]\n        },\n        {\n            \"domain\": \"Profiler\",\n            \"dependencies\": [\n                \"Runtime\",\n                \"Debugger\"\n            ],\n            \"types\": [\n                {\n                    \"id\": \"ProfileNode\",\n                    \"description\": \"Profile node. Holds callsite information, execution statistics and child nodes.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Unique id of the node.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"callFrame\",\n                            \"description\": \"Function location.\",\n                            \"$ref\": \"Runtime.CallFrame\"\n                        },\n                        {\n                            \"name\": \"hitCount\",\n                            \"description\": \"Number of samples where this node was on top of the call stack.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"children\",\n                            \"description\": \"Child node ids.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"deoptReason\",\n                            \"description\": \"The reason of being not optimized. The function may be deoptimized or marked as don't\\noptimize.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"positionTicks\",\n                            \"description\": \"An array of source position ticks.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PositionTickInfo\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Profile\",\n                    \"description\": \"Profile.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"nodes\",\n                            \"description\": \"The list of profile nodes. First item is the root node.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ProfileNode\"\n                            }\n                        },\n                        {\n                            \"name\": \"startTime\",\n                            \"description\": \"Profiling start timestamp in microseconds.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"endTime\",\n                            \"description\": \"Profiling end timestamp in microseconds.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"samples\",\n                            \"description\": \"Ids of samples top nodes.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        },\n                        {\n                            \"name\": \"timeDeltas\",\n                            \"description\": \"Time intervals between adjacent samples in microseconds. The first delta is relative to the\\nprofile startTime.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PositionTickInfo\",\n                    \"description\": \"Specifies a number of samples attributed to a certain source position.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"line\",\n                            \"description\": \"Source line number (1-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"ticks\",\n                            \"description\": \"Number of samples attributed to the source line.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CoverageRange\",\n                    \"description\": \"Coverage data for a source range.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"startOffset\",\n                            \"description\": \"JavaScript script source offset for the range start.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"endOffset\",\n                            \"description\": \"JavaScript script source offset for the range end.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"count\",\n                            \"description\": \"Collected execution count of the source range.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"FunctionCoverage\",\n                    \"description\": \"Coverage data for a JavaScript function.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"functionName\",\n                            \"description\": \"JavaScript function name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"ranges\",\n                            \"description\": \"Source ranges inside the function with coverage data.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CoverageRange\"\n                            }\n                        },\n                        {\n                            \"name\": \"isBlockCoverage\",\n                            \"description\": \"Whether coverage data for this function has block granularity.\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ScriptCoverage\",\n                    \"description\": \"Coverage data for a JavaScript script.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"JavaScript script id.\",\n                            \"$ref\": \"Runtime.ScriptId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"JavaScript script name or url.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"functions\",\n                            \"description\": \"Functions contained in the script that has coverage data.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"FunctionCoverage\"\n                            }\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"disable\"\n                },\n                {\n                    \"name\": \"enable\"\n                },\n                {\n                    \"name\": \"getBestEffortCoverage\",\n                    \"description\": \"Collect coverage data for the current isolate. The coverage data may be incomplete due to\\ngarbage collection.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Coverage data for the current isolate.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScriptCoverage\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setSamplingInterval\",\n                    \"description\": \"Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"interval\",\n                            \"description\": \"New sampling interval in microseconds.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"start\"\n                },\n                {\n                    \"name\": \"startPreciseCoverage\",\n                    \"description\": \"Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\\ncounters.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"callCount\",\n                            \"description\": \"Collect accurate call counts beyond simple 'covered' or 'not covered'.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"detailed\",\n                            \"description\": \"Collect block-based coverage.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"allowTriggeredUpdates\",\n                            \"description\": \"Allow the backend to send updates on its own initiative\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stop\",\n                    \"returns\": [\n                        {\n                            \"name\": \"profile\",\n                            \"description\": \"Recorded profile.\",\n                            \"$ref\": \"Profile\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"stopPreciseCoverage\",\n                    \"description\": \"Disable precise code coverage. Disabling releases unnecessary execution count records and allows\\nexecuting optimized code.\"\n                },\n                {\n                    \"name\": \"takePreciseCoverage\",\n                    \"description\": \"Collect coverage data for the current isolate, and resets execution counters. Precise code\\ncoverage needs to have started.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Coverage data for the current isolate.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScriptCoverage\"\n                            }\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"consoleProfileFinished\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Location of console.profileEnd().\",\n                            \"$ref\": \"Debugger.Location\"\n                        },\n                        {\n                            \"name\": \"profile\",\n                            \"$ref\": \"Profile\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Profile title passed as an argument to console.profile().\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"consoleProfileStarted\",\n                    \"description\": \"Sent when new profile recording is started using console.profile() call.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"location\",\n                            \"description\": \"Location of console.profile().\",\n                            \"$ref\": \"Debugger.Location\"\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"description\": \"Profile title passed as an argument to console.profile().\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"preciseCoverageDeltaUpdate\",\n                    \"description\": \"Reports coverage delta since the last poll (either from an event like this, or from\\n`takePreciseCoverage` for the current isolate. May only be sent if precise code\\ncoverage has been started. This event can be trigged by the embedder to, for example,\\ntrigger collection of coverage data immediately at a certain point in time.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"occasion\",\n                            \"description\": \"Identifier for distinguishing coverage events.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Coverage data for the current isolate.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"ScriptCoverage\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Runtime\",\n            \"description\": \"Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\\nEvaluation results are returned as mirror object that expose object type, string representation\\nand unique identifier that can be used for further object reference. Original objects are\\nmaintained in memory unless they are either explicitly released or are released along with the\\nother objects in their object group.\",\n            \"types\": [\n                {\n                    \"id\": \"ScriptId\",\n                    \"description\": \"Unique script identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"SerializationOptions\",\n                    \"description\": \"Represents options for serialization. Overrides `generatePreview` and `returnByValue`.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"serialization\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"deep\",\n                                \"json\",\n                                \"idOnly\"\n                            ]\n                        },\n                        {\n                            \"name\": \"maxDepth\",\n                            \"description\": \"Deep serialization depth. Default is full depth. Respected only in `deep` serialization mode.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"additionalParameters\",\n                            \"description\": \"Embedder-specific parameters. For example if connected to V8 in Chrome these control DOM\\nserialization via `maxNodeDepth: integer` and `includeShadowTree: \\\"none\\\" | \\\"open\\\" | \\\"all\\\"`.\\nValues can be only of type string or integer.\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"DeepSerializedValue\",\n                    \"description\": \"Represents deep serialized value.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"undefined\",\n                                \"null\",\n                                \"string\",\n                                \"number\",\n                                \"boolean\",\n                                \"bigint\",\n                                \"regexp\",\n                                \"date\",\n                                \"symbol\",\n                                \"array\",\n                                \"object\",\n                                \"function\",\n                                \"map\",\n                                \"set\",\n                                \"weakmap\",\n                                \"weakset\",\n                                \"error\",\n                                \"proxy\",\n                                \"promise\",\n                                \"typedarray\",\n                                \"arraybuffer\",\n                                \"node\",\n                                \"window\",\n                                \"generator\"\n                            ]\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"optional\": true,\n                            \"type\": \"any\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"weakLocalObjectReference\",\n                            \"description\": \"Set if value reference met more then once during serialization. In such\\ncase, value is provided only to one of the serialized values. Unique\\nper value in the scope of one CDP call.\",\n                            \"optional\": true,\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"RemoteObjectId\",\n                    \"description\": \"Unique object identifier.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"UnserializableValue\",\n                    \"description\": \"Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\\n`-Infinity`, and bigint literals.\",\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"RemoteObject\",\n                    \"description\": \"Mirror object referencing original JavaScript object.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Object type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"object\",\n                                \"function\",\n                                \"undefined\",\n                                \"string\",\n                                \"number\",\n                                \"boolean\",\n                                \"symbol\",\n                                \"bigint\"\n                            ]\n                        },\n                        {\n                            \"name\": \"subtype\",\n                            \"description\": \"Object subtype hint. Specified for `object` type values only.\\nNOTE: If you change anything here, make sure to also update\\n`subtype` in `ObjectPreview` and `PropertyPreview` below.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"array\",\n                                \"null\",\n                                \"node\",\n                                \"regexp\",\n                                \"date\",\n                                \"map\",\n                                \"set\",\n                                \"weakmap\",\n                                \"weakset\",\n                                \"iterator\",\n                                \"generator\",\n                                \"error\",\n                                \"proxy\",\n                                \"promise\",\n                                \"typedarray\",\n                                \"arraybuffer\",\n                                \"dataview\",\n                                \"webassemblymemory\",\n                                \"wasmvalue\",\n                                \"trustedtype\"\n                            ]\n                        },\n                        {\n                            \"name\": \"className\",\n                            \"description\": \"Object class (constructor) name. Specified for `object` type values only.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Remote object value in case of primitive values or JSON values (if it was requested).\",\n                            \"optional\": true,\n                            \"type\": \"any\"\n                        },\n                        {\n                            \"name\": \"unserializableValue\",\n                            \"description\": \"Primitive value which can not be JSON-stringified does not have `value`, but gets this\\nproperty.\",\n                            \"optional\": true,\n                            \"$ref\": \"UnserializableValue\"\n                        },\n                        {\n                            \"name\": \"description\",\n                            \"description\": \"String representation of the object.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"deepSerializedValue\",\n                            \"description\": \"Deep serialized value.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"DeepSerializedValue\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Unique object identifier (for non-primitive values).\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"preview\",\n                            \"description\": \"Preview containing abbreviated property values. Specified for `object` type values only.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"ObjectPreview\"\n                        },\n                        {\n                            \"name\": \"customPreview\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"CustomPreview\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CustomPreview\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"header\",\n                            \"description\": \"The JSON-stringified result of formatter.header(object, config) call.\\nIt contains json ML array that represents RemoteObject.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"bodyGetterId\",\n                            \"description\": \"If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\\nThe result value is json ML array.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ObjectPreview\",\n                    \"description\": \"Object containing abbreviated remote object value.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Object type.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"object\",\n                                \"function\",\n                                \"undefined\",\n                                \"string\",\n                                \"number\",\n                                \"boolean\",\n                                \"symbol\",\n                                \"bigint\"\n                            ]\n                        },\n                        {\n                            \"name\": \"subtype\",\n                            \"description\": \"Object subtype hint. Specified for `object` type values only.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"array\",\n                                \"null\",\n                                \"node\",\n                                \"regexp\",\n                                \"date\",\n                                \"map\",\n                                \"set\",\n                                \"weakmap\",\n                                \"weakset\",\n                                \"iterator\",\n                                \"generator\",\n                                \"error\",\n                                \"proxy\",\n                                \"promise\",\n                                \"typedarray\",\n                                \"arraybuffer\",\n                                \"dataview\",\n                                \"webassemblymemory\",\n                                \"wasmvalue\",\n                                \"trustedtype\"\n                            ]\n                        },\n                        {\n                            \"name\": \"description\",\n                            \"description\": \"String representation of the object.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"overflow\",\n                            \"description\": \"True iff some of the properties or entries of the original object did not fit.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"properties\",\n                            \"description\": \"List of the properties.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PropertyPreview\"\n                            }\n                        },\n                        {\n                            \"name\": \"entries\",\n                            \"description\": \"List of the entries. Specified for `map` and `set` subtype values only.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"EntryPreview\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PropertyPreview\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Object type. Accessor means that the property itself is an accessor property.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"object\",\n                                \"function\",\n                                \"undefined\",\n                                \"string\",\n                                \"number\",\n                                \"boolean\",\n                                \"symbol\",\n                                \"accessor\",\n                                \"bigint\"\n                            ]\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"User-friendly property value string.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"valuePreview\",\n                            \"description\": \"Nested value preview.\",\n                            \"optional\": true,\n                            \"$ref\": \"ObjectPreview\"\n                        },\n                        {\n                            \"name\": \"subtype\",\n                            \"description\": \"Object subtype hint. Specified for `object` type values only.\",\n                            \"optional\": true,\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"array\",\n                                \"null\",\n                                \"node\",\n                                \"regexp\",\n                                \"date\",\n                                \"map\",\n                                \"set\",\n                                \"weakmap\",\n                                \"weakset\",\n                                \"iterator\",\n                                \"generator\",\n                                \"error\",\n                                \"proxy\",\n                                \"promise\",\n                                \"typedarray\",\n                                \"arraybuffer\",\n                                \"dataview\",\n                                \"webassemblymemory\",\n                                \"wasmvalue\",\n                                \"trustedtype\"\n                            ]\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"EntryPreview\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"key\",\n                            \"description\": \"Preview of the key. Specified for map-like collection entries.\",\n                            \"optional\": true,\n                            \"$ref\": \"ObjectPreview\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Preview of the value.\",\n                            \"$ref\": \"ObjectPreview\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PropertyDescriptor\",\n                    \"description\": \"Object property descriptor.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Property name or symbol description.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The value associated with the property.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"writable\",\n                            \"description\": \"True if the value associated with the property may be changed (data descriptors only).\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"get\",\n                            \"description\": \"A function which serves as a getter for the property, or `undefined` if there is no getter\\n(accessor descriptors only).\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"set\",\n                            \"description\": \"A function which serves as a setter for the property, or `undefined` if there is no setter\\n(accessor descriptors only).\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"configurable\",\n                            \"description\": \"True if the type of this property descriptor may be changed and if the property may be\\ndeleted from the corresponding object.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"enumerable\",\n                            \"description\": \"True if this property shows up during enumeration of the properties on the corresponding\\nobject.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"wasThrown\",\n                            \"description\": \"True if the result was thrown during the evaluation.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"isOwn\",\n                            \"description\": \"True if the property is owned for the object.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"symbol\",\n                            \"description\": \"Property symbol object, if the property is of the `symbol` type.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"InternalPropertyDescriptor\",\n                    \"description\": \"Object internal property descriptor. This property isn't normally visible in JavaScript code.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Conventional property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The value associated with the property.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"PrivatePropertyDescriptor\",\n                    \"description\": \"Object private field descriptor.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Private property name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"The value associated with the private property.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"get\",\n                            \"description\": \"A function which serves as a getter for the private property,\\nor `undefined` if there is no getter (accessor descriptors only).\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"set\",\n                            \"description\": \"A function which serves as a setter for the private property,\\nor `undefined` if there is no setter (accessor descriptors only).\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"CallArgument\",\n                    \"description\": \"Represents function call argument. Either remote object id `objectId`, primitive `value`,\\nunserializable primitive value or neither of (for undefined) them should be specified.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"value\",\n                            \"description\": \"Primitive value or serializable javascript object.\",\n                            \"optional\": true,\n                            \"type\": \"any\"\n                        },\n                        {\n                            \"name\": \"unserializableValue\",\n                            \"description\": \"Primitive value which can not be JSON-stringified.\",\n                            \"optional\": true,\n                            \"$ref\": \"UnserializableValue\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Remote object handle.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ExecutionContextId\",\n                    \"description\": \"Id of an execution context.\",\n                    \"type\": \"integer\"\n                },\n                {\n                    \"id\": \"ExecutionContextDescription\",\n                    \"description\": \"Description of an isolated world.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"Unique id of the execution context. It can be used to specify in which execution context\\nscript evaluation should be performed.\",\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"origin\",\n                            \"description\": \"Execution context origin.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Human readable name describing given context.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"uniqueId\",\n                            \"description\": \"A system-unique execution context identifier. Unlike the id, this is unique across\\nmultiple processes, so can be reliably used to identify specific context while backend\\nperforms a cross-process navigation.\",\n                            \"experimental\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"auxData\",\n                            \"description\": \"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}\",\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"ExceptionDetails\",\n                    \"description\": \"Detailed information about exception (or error) that was thrown during script compilation or\\nexecution.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"exceptionId\",\n                            \"description\": \"Exception id.\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"text\",\n                            \"description\": \"Exception text, which should be used together with exception object when available.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"Line number of the exception location (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"Column number of the exception location (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Script ID of the exception location.\",\n                            \"optional\": true,\n                            \"$ref\": \"ScriptId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"URL of the exception location, to be used when the script was not reported.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"stackTrace\",\n                            \"description\": \"JavaScript stack trace if available.\",\n                            \"optional\": true,\n                            \"$ref\": \"StackTrace\"\n                        },\n                        {\n                            \"name\": \"exception\",\n                            \"description\": \"Exception object if available.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Identifier of the context where exception happened.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"exceptionMetaData\",\n                            \"description\": \"Dictionary with entries of meta data that the client associated\\nwith this exception, such as information about associated network\\nrequests, etc.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"object\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"Timestamp\",\n                    \"description\": \"Number of milliseconds since epoch.\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"id\": \"TimeDelta\",\n                    \"description\": \"Number of milliseconds.\",\n                    \"type\": \"number\"\n                },\n                {\n                    \"id\": \"CallFrame\",\n                    \"description\": \"Stack entry for runtime errors and assertions.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"functionName\",\n                            \"description\": \"JavaScript function name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"JavaScript script id.\",\n                            \"$ref\": \"ScriptId\"\n                        },\n                        {\n                            \"name\": \"url\",\n                            \"description\": \"JavaScript script name or url.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"lineNumber\",\n                            \"description\": \"JavaScript script line number (0-based).\",\n                            \"type\": \"integer\"\n                        },\n                        {\n                            \"name\": \"columnNumber\",\n                            \"description\": \"JavaScript script column number (0-based).\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"StackTrace\",\n                    \"description\": \"Call frames for assertions or error messages.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"description\",\n                            \"description\": \"String label of this stack trace. For async traces this may be a name of the function that\\ninitiated the async call.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"callFrames\",\n                            \"description\": \"JavaScript function name.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CallFrame\"\n                            }\n                        },\n                        {\n                            \"name\": \"parent\",\n                            \"description\": \"Asynchronous JavaScript stack trace that preceded this stack, if available.\",\n                            \"optional\": true,\n                            \"$ref\": \"StackTrace\"\n                        },\n                        {\n                            \"name\": \"parentId\",\n                            \"description\": \"Asynchronous JavaScript stack trace that preceded this stack, if available.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"StackTraceId\"\n                        }\n                    ]\n                },\n                {\n                    \"id\": \"UniqueDebuggerId\",\n                    \"description\": \"Unique identifier of current debugger.\",\n                    \"experimental\": true,\n                    \"type\": \"string\"\n                },\n                {\n                    \"id\": \"StackTraceId\",\n                    \"description\": \"If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.\",\n                    \"experimental\": true,\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"id\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"debuggerId\",\n                            \"optional\": true,\n                            \"$ref\": \"UniqueDebuggerId\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"awaitPromise\",\n                    \"description\": \"Add handler to promise with given promise object id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"promiseObjectId\",\n                            \"description\": \"Identifier of the promise.\",\n                            \"$ref\": \"RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"returnByValue\",\n                            \"description\": \"Whether the result is expected to be a JSON object that should be sent by value.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generatePreview\",\n                            \"description\": \"Whether preview should be generated for the result.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Promise result. Will contain rejected value if promise was rejected.\",\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details if stack strace is available.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"callFunctionOn\",\n                    \"description\": \"Calls function with given declaration on the given object. Object group of the result is\\ninherited from the target object.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"functionDeclaration\",\n                            \"description\": \"Declaration of the function to call.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Identifier of the object to call function on. Either objectId or executionContextId should\\nbe specified.\",\n                            \"optional\": true,\n                            \"$ref\": \"RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"arguments\",\n                            \"description\": \"Call arguments. All call arguments must belong to the same JavaScript world as the target\\nobject.\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"CallArgument\"\n                            }\n                        },\n                        {\n                            \"name\": \"silent\",\n                            \"description\": \"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"returnByValue\",\n                            \"description\": \"Whether the result is expected to be a JSON object which should be sent by value.\\nCan be overriden by `serializationOptions`.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generatePreview\",\n                            \"description\": \"Whether preview should be generated for the result.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"userGesture\",\n                            \"description\": \"Whether execution should be treated as initiated by user in the UI.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"awaitPromise\",\n                            \"description\": \"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Specifies execution context which global object will be used to call function on. Either\\nexecutionContextId or objectId should be specified.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic group name that can be used to release multiple objects. If objectGroup is not\\nspecified and objectId is, objectGroup will be inherited from object.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"throwOnSideEffect\",\n                            \"description\": \"Whether to throw an exception if side effect cannot be ruled out during evaluation.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"uniqueContextId\",\n                            \"description\": \"An alternative way to specify the execution context to call function on.\\nCompared to contextId that may be reused across processes, this is guaranteed to be\\nsystem-unique, so it can be used to prevent accidental function call\\nin context different than intended (e.g. as a result of navigation across process\\nboundaries).\\nThis is mutually exclusive with `executionContextId`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"serializationOptions\",\n                            \"description\": \"Specifies the result serialization. If provided, overrides\\n`generatePreview` and `returnByValue`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"SerializationOptions\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Call result.\",\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"compileScript\",\n                    \"description\": \"Compiles expression.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"expression\",\n                            \"description\": \"Expression to compile.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"sourceURL\",\n                            \"description\": \"Source url to be set for the script.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"persistScript\",\n                            \"description\": \"Specifies whether the compiled script should be persisted.\",\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script.\",\n                            \"optional\": true,\n                            \"$ref\": \"ScriptId\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"disable\",\n                    \"description\": \"Disables reporting of execution contexts creation.\"\n                },\n                {\n                    \"name\": \"discardConsoleEntries\",\n                    \"description\": \"Discards collected exceptions and console API calls.\"\n                },\n                {\n                    \"name\": \"enable\",\n                    \"description\": \"Enables reporting of execution contexts creation by means of `executionContextCreated` event.\\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\\ncontext.\"\n                },\n                {\n                    \"name\": \"evaluate\",\n                    \"description\": \"Evaluates expression on global object.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"expression\",\n                            \"description\": \"Expression to evaluate.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic group name that can be used to release multiple objects.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"includeCommandLineAPI\",\n                            \"description\": \"Determines whether Command Line API should be available during the evaluation.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"silent\",\n                            \"description\": \"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"contextId\",\n                            \"description\": \"Specifies in which execution context to perform evaluation. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.\\nThis is mutually exclusive with `uniqueContextId`, which offers an\\nalternative way to identify the execution context that is more reliable\\nin a multi-process environment.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"returnByValue\",\n                            \"description\": \"Whether the result is expected to be a JSON object that should be sent by value.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generatePreview\",\n                            \"description\": \"Whether preview should be generated for the result.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"userGesture\",\n                            \"description\": \"Whether execution should be treated as initiated by user in the UI.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"awaitPromise\",\n                            \"description\": \"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"throwOnSideEffect\",\n                            \"description\": \"Whether to throw an exception if side effect cannot be ruled out during evaluation.\\nThis implies `disableBreaks` below.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"timeout\",\n                            \"description\": \"Terminate execution after timing out (number of milliseconds).\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"TimeDelta\"\n                        },\n                        {\n                            \"name\": \"disableBreaks\",\n                            \"description\": \"Disable breakpoints during execution.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"replMode\",\n                            \"description\": \"Setting this flag to true enables `let` re-declaration and top-level `await`.\\nNote that `let` variables can only be re-declared if they originate from\\n`replMode` themselves.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"allowUnsafeEvalBlockedByCSP\",\n                            \"description\": \"The Content Security Policy (CSP) for the target might block 'unsafe-eval'\\nwhich includes eval(), Function(), setTimeout() and setInterval()\\nwhen called with non-callable arguments. This flag bypasses CSP for this\\nevaluation and allows unsafe-eval. Defaults to true.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"uniqueContextId\",\n                            \"description\": \"An alternative way to specify the execution context to evaluate in.\\nCompared to contextId that may be reused across processes, this is guaranteed to be\\nsystem-unique, so it can be used to prevent accidental evaluation of the expression\\nin context different than intended (e.g. as a result of navigation across process\\nboundaries).\\nThis is mutually exclusive with `contextId`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"serializationOptions\",\n                            \"description\": \"Specifies the result serialization. If provided, overrides\\n`generatePreview` and `returnByValue`.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"SerializationOptions\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Evaluation result.\",\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getIsolateId\",\n                    \"description\": \"Returns the isolate id.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"id\",\n                            \"description\": \"The isolate id.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getHeapUsage\",\n                    \"description\": \"Returns the JavaScript heap usage.\\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.\",\n                    \"experimental\": true,\n                    \"returns\": [\n                        {\n                            \"name\": \"usedSize\",\n                            \"description\": \"Used JavaScript heap size in bytes.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"totalSize\",\n                            \"description\": \"Allocated JavaScript heap size in bytes.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"embedderHeapUsedSize\",\n                            \"description\": \"Used size in bytes in the embedder's garbage-collected heap.\",\n                            \"type\": \"number\"\n                        },\n                        {\n                            \"name\": \"backingStorageSize\",\n                            \"description\": \"Size in bytes of backing storage for array buffers and external strings.\",\n                            \"type\": \"number\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getProperties\",\n                    \"description\": \"Returns properties of a given object. Object group of the result is inherited from the target\\nobject.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Identifier of the object to return properties for.\",\n                            \"$ref\": \"RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"ownProperties\",\n                            \"description\": \"If true, returns properties belonging only to the element itself, not to its prototype\\nchain.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"accessorPropertiesOnly\",\n                            \"description\": \"If true, returns accessor properties (with getter/setter) only; internal properties are not\\nreturned either.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generatePreview\",\n                            \"description\": \"Whether preview should be generated for the results.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"nonIndexedPropertiesOnly\",\n                            \"description\": \"If true, returns non-indexed properties only.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Object properties.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PropertyDescriptor\"\n                            }\n                        },\n                        {\n                            \"name\": \"internalProperties\",\n                            \"description\": \"Internal object properties (only of the element itself).\",\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"InternalPropertyDescriptor\"\n                            }\n                        },\n                        {\n                            \"name\": \"privateProperties\",\n                            \"description\": \"Object private properties.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"PrivatePropertyDescriptor\"\n                            }\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"globalLexicalScopeNames\",\n                    \"description\": \"Returns all let, const and class variables from global scope.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Specifies in which execution context to lookup global scope variables.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"names\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"queryObjects\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"prototypeObjectId\",\n                            \"description\": \"Identifier of the prototype to return objects for.\",\n                            \"$ref\": \"RemoteObjectId\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic group name that can be used to release the results.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"objects\",\n                            \"description\": \"Array with objects.\",\n                            \"$ref\": \"RemoteObject\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"releaseObject\",\n                    \"description\": \"Releases remote object with given id.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectId\",\n                            \"description\": \"Identifier of the object to release.\",\n                            \"$ref\": \"RemoteObjectId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"releaseObjectGroup\",\n                    \"description\": \"Releases all remote objects that belong to a given group.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic object group name.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"runIfWaitingForDebugger\",\n                    \"description\": \"Tells inspected instance to run if it was waiting for debugger to attach.\"\n                },\n                {\n                    \"name\": \"runScript\",\n                    \"description\": \"Runs script with given id in a given context.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"scriptId\",\n                            \"description\": \"Id of the script to run.\",\n                            \"$ref\": \"ScriptId\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"objectGroup\",\n                            \"description\": \"Symbolic group name that can be used to release multiple objects.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"silent\",\n                            \"description\": \"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"includeCommandLineAPI\",\n                            \"description\": \"Determines whether Command Line API should be available during the evaluation.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"returnByValue\",\n                            \"description\": \"Whether the result is expected to be a JSON object which should be sent by value.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"generatePreview\",\n                            \"description\": \"Whether preview should be generated for the result.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        },\n                        {\n                            \"name\": \"awaitPromise\",\n                            \"description\": \"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.\",\n                            \"optional\": true,\n                            \"type\": \"boolean\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"result\",\n                            \"description\": \"Run result.\",\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"description\": \"Exception details.\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setAsyncCallStackDepth\",\n                    \"description\": \"Enables or disables async call stacks tracking.\",\n                    \"redirect\": \"Debugger\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"maxDepth\",\n                            \"description\": \"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setCustomObjectFormatterEnabled\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"enabled\",\n                            \"type\": \"boolean\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"setMaxCallStackSizeToCapture\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"size\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"terminateExecution\",\n                    \"description\": \"Terminate current or next JavaScript execution.\\nWill cancel the termination when the outer-most script execution ends.\",\n                    \"experimental\": true\n                },\n                {\n                    \"name\": \"addBinding\",\n                    \"description\": \"If executionContextId is empty, adds binding with the given name on the\\nglobal objects of all inspected contexts, including those created later,\\nbindings survive reloads.\\nBinding function takes exactly one argument, this argument should be string,\\nin case of any other input, function throws an exception.\\nEach binding function call produces Runtime.bindingCalled notification.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"If specified, the binding would only be exposed to the specified\\nexecution context. If omitted and `executionContextName` is not set,\\nthe binding is exposed to all execution contexts of the target.\\nThis parameter is mutually exclusive with `executionContextName`.\\nDeprecated in favor of `executionContextName` due to an unclear use case\\nand bugs in implementation (crbug.com/1169639). `executionContextId` will be\\nremoved in the future.\",\n                            \"experimental\": true,\n                            \"deprecated\": true,\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"executionContextName\",\n                            \"description\": \"If specified, the binding is exposed to the executionContext with\\nmatching name, even for contexts created after the binding is added.\\nSee also `ExecutionContext.name` and `worldName` parameter to\\n`Page.addScriptToEvaluateOnNewDocument`.\\nThis parameter is mutually exclusive with `executionContextId`.\",\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"removeBinding\",\n                    \"description\": \"This method does not remove binding function from global object but\\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"getExceptionDetails\",\n                    \"description\": \"This method tries to lookup and populate exception details for a\\nJavaScript Error object.\\nNote that the stackTrace portion of the resulting exceptionDetails will\\nonly be populated if the Runtime domain was enabled at the time when the\\nError was thrown.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"errorObjectId\",\n                            \"description\": \"The error object for which to resolve the exception details.\",\n                            \"$ref\": \"RemoteObjectId\"\n                        }\n                    ],\n                    \"returns\": [\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"optional\": true,\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                }\n            ],\n            \"events\": [\n                {\n                    \"name\": \"bindingCalled\",\n                    \"description\": \"Notification is issued every time when binding is called.\",\n                    \"experimental\": true,\n                    \"parameters\": [\n                        {\n                            \"name\": \"name\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"payload\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Identifier of the context where the call was made.\",\n                            \"$ref\": \"ExecutionContextId\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"consoleAPICalled\",\n                    \"description\": \"Issued when console API was called.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"type\",\n                            \"description\": \"Type of the call.\",\n                            \"type\": \"string\",\n                            \"enum\": [\n                                \"log\",\n                                \"debug\",\n                                \"info\",\n                                \"error\",\n                                \"warning\",\n                                \"dir\",\n                                \"dirxml\",\n                                \"table\",\n                                \"trace\",\n                                \"clear\",\n                                \"startGroup\",\n                                \"startGroupCollapsed\",\n                                \"endGroup\",\n                                \"assert\",\n                                \"profile\",\n                                \"profileEnd\",\n                                \"count\",\n                                \"timeEnd\"\n                            ]\n                        },\n                        {\n                            \"name\": \"args\",\n                            \"description\": \"Call arguments.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"RemoteObject\"\n                            }\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Identifier of the context where the call was made.\",\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Call timestamp.\",\n                            \"$ref\": \"Timestamp\"\n                        },\n                        {\n                            \"name\": \"stackTrace\",\n                            \"description\": \"Stack trace captured when the call was made. The async stack chain is automatically reported for\\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.\",\n                            \"optional\": true,\n                            \"$ref\": \"StackTrace\"\n                        },\n                        {\n                            \"name\": \"context\",\n                            \"description\": \"Console context descriptor for calls on non-default console context (not console.*):\\n'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call\\non named context.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"exceptionRevoked\",\n                    \"description\": \"Issued when unhandled exception was revoked.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"reason\",\n                            \"description\": \"Reason describing why exception was revoked.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"exceptionId\",\n                            \"description\": \"The id of revoked exception, as reported in `exceptionThrown`.\",\n                            \"type\": \"integer\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"exceptionThrown\",\n                    \"description\": \"Issued when exception was thrown and unhandled.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"timestamp\",\n                            \"description\": \"Timestamp of the exception.\",\n                            \"$ref\": \"Timestamp\"\n                        },\n                        {\n                            \"name\": \"exceptionDetails\",\n                            \"$ref\": \"ExceptionDetails\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"executionContextCreated\",\n                    \"description\": \"Issued when new execution context is created.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"context\",\n                            \"description\": \"A newly created execution context.\",\n                            \"$ref\": \"ExecutionContextDescription\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"executionContextDestroyed\",\n                    \"description\": \"Issued when execution context is destroyed.\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Id of the destroyed context\",\n                            \"deprecated\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        },\n                        {\n                            \"name\": \"executionContextUniqueId\",\n                            \"description\": \"Unique Id of the destroyed context\",\n                            \"experimental\": true,\n                            \"type\": \"string\"\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"executionContextsCleared\",\n                    \"description\": \"Issued when all executionContexts were cleared in browser\"\n                },\n                {\n                    \"name\": \"inspectRequested\",\n                    \"description\": \"Issued when object should be inspected (for example, as a result of inspect() command line API\\ncall).\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"object\",\n                            \"$ref\": \"RemoteObject\"\n                        },\n                        {\n                            \"name\": \"hints\",\n                            \"type\": \"object\"\n                        },\n                        {\n                            \"name\": \"executionContextId\",\n                            \"description\": \"Identifier of the context where the call was made.\",\n                            \"experimental\": true,\n                            \"optional\": true,\n                            \"$ref\": \"ExecutionContextId\"\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"domain\": \"Schema\",\n            \"description\": \"This domain is deprecated.\",\n            \"deprecated\": true,\n            \"types\": [\n                {\n                    \"id\": \"Domain\",\n                    \"description\": \"Description of the protocol domain.\",\n                    \"type\": \"object\",\n                    \"properties\": [\n                        {\n                            \"name\": \"name\",\n                            \"description\": \"Domain name.\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"version\",\n                            \"description\": \"Domain version.\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            ],\n            \"commands\": [\n                {\n                    \"name\": \"getDomains\",\n                    \"description\": \"Returns supported domains.\",\n                    \"returns\": [\n                        {\n                            \"name\": \"domains\",\n                            \"description\": \"List of supported domains.\",\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"Domain\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}"
  },
  {
    "path": "cli/src/color.rs",
    "content": "//! Color output utilities respecting NO_COLOR environment variable.\n//!\n//! When the NO_COLOR environment variable is present (regardless of value),\n//! all color formatting is disabled per https://no-color.org/\n\nuse std::env;\nuse std::sync::OnceLock;\n\n/// Returns true if color output is enabled (NO_COLOR is NOT set)\npub fn is_enabled() -> bool {\n    static COLORS_ENABLED: OnceLock<bool> = OnceLock::new();\n    *COLORS_ENABLED.get_or_init(|| env::var(\"NO_COLOR\").is_err())\n}\n\n/// Format text in red (errors)\npub fn red(text: &str) -> String {\n    if is_enabled() {\n        format!(\"\\x1b[31m{}\\x1b[0m\", text)\n    } else {\n        text.to_string()\n    }\n}\n\n/// Format text in green (success)\npub fn green(text: &str) -> String {\n    if is_enabled() {\n        format!(\"\\x1b[32m{}\\x1b[0m\", text)\n    } else {\n        text.to_string()\n    }\n}\n\n/// Format text in yellow (warnings)\npub fn yellow(text: &str) -> String {\n    if is_enabled() {\n        format!(\"\\x1b[33m{}\\x1b[0m\", text)\n    } else {\n        text.to_string()\n    }\n}\n\n/// Format text in cyan (info/progress)\npub fn cyan(text: &str) -> String {\n    if is_enabled() {\n        format!(\"\\x1b[36m{}\\x1b[0m\", text)\n    } else {\n        text.to_string()\n    }\n}\n\n/// Format text in bold\npub fn bold(text: &str) -> String {\n    if is_enabled() {\n        format!(\"\\x1b[1m{}\\x1b[0m\", text)\n    } else {\n        text.to_string()\n    }\n}\n\n/// Format text in dim\npub fn dim(text: &str) -> String {\n    if is_enabled() {\n        format!(\"\\x1b[2m{}\\x1b[0m\", text)\n    } else {\n        text.to_string()\n    }\n}\n\n/// Red X error indicator\npub fn error_indicator() -> &'static str {\n    static INDICATOR: OnceLock<String> = OnceLock::new();\n    INDICATOR.get_or_init(|| {\n        if is_enabled() {\n            \"\\x1b[31m✗\\x1b[0m\".to_string()\n        } else {\n            \"✗\".to_string()\n        }\n    })\n}\n\n/// Green checkmark success indicator\npub fn success_indicator() -> &'static str {\n    static INDICATOR: OnceLock<String> = OnceLock::new();\n    INDICATOR.get_or_init(|| {\n        if is_enabled() {\n            \"\\x1b[32m✓\\x1b[0m\".to_string()\n        } else {\n            \"✓\".to_string()\n        }\n    })\n}\n\n/// Yellow warning indicator\npub fn warning_indicator() -> &'static str {\n    static INDICATOR: OnceLock<String> = OnceLock::new();\n    INDICATOR.get_or_init(|| {\n        if is_enabled() {\n            \"\\x1b[33m⚠\\x1b[0m\".to_string()\n        } else {\n            \"⚠\".to_string()\n        }\n    })\n}\n\n/// Get console log color prefix by level\npub fn console_level_prefix(level: &str) -> String {\n    if !is_enabled() {\n        return format!(\"[{}]\", level);\n    }\n\n    let color = match level {\n        \"error\" => \"\\x1b[31m\",\n        \"warning\" => \"\\x1b[33m\",\n        \"info\" => \"\\x1b[36m\",\n        _ => \"\",\n    };\n    if color.is_empty() {\n        format!(\"[{}]\", level)\n    } else {\n        format!(\"{}[{}]\\x1b[0m\", color, level)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_red_contains_ansi_codes() {\n        // Test the format structure (actual color depends on NO_COLOR env)\n        let formatted = format!(\"\\x1b[31m{}\\x1b[0m\", \"error\");\n        assert!(formatted.contains(\"\\x1b[31m\"));\n        assert!(formatted.contains(\"\\x1b[0m\"));\n    }\n\n    #[test]\n    fn test_green_contains_ansi_codes() {\n        let formatted = format!(\"\\x1b[32m{}\\x1b[0m\", \"success\");\n        assert!(formatted.contains(\"\\x1b[32m\"));\n    }\n\n    #[test]\n    fn test_console_level_prefix_contains_level() {\n        // Regardless of color state, the level text should be present\n        assert!(console_level_prefix(\"error\").contains(\"error\"));\n        assert!(console_level_prefix(\"warning\").contains(\"warning\"));\n        assert!(console_level_prefix(\"info\").contains(\"info\"));\n        assert!(console_level_prefix(\"log\").contains(\"log\"));\n    }\n\n    #[test]\n    fn test_indicators_contain_symbols() {\n        // Regardless of color state, symbols should be present\n        assert!(error_indicator().contains('✗'));\n        assert!(success_indicator().contains('✓'));\n        assert!(warning_indicator().contains('⚠'));\n    }\n}\n"
  },
  {
    "path": "cli/src/commands.rs",
    "content": "use base64::{engine::general_purpose::STANDARD, Engine};\nuse serde_json::{json, Value};\nuse std::io::{self, BufRead};\n\nuse crate::color;\nuse crate::flags::Flags;\nuse crate::validation::{is_valid_session_name, session_name_error};\n\n/// Error type for command parsing with contextual information\n#[derive(Debug)]\npub enum ParseError {\n    /// Command does not exist\n    UnknownCommand { command: String },\n    /// Command exists but subcommand is invalid\n    UnknownSubcommand {\n        subcommand: String,\n        valid_options: &'static [&'static str],\n    },\n    /// Command/subcommand exists but required arguments are missing\n    MissingArguments {\n        context: String,\n        usage: &'static str,\n    },\n    /// Argument exists but has an invalid value\n    InvalidValue {\n        message: String,\n        usage: &'static str,\n    },\n    /// Invalid session name (path traversal or invalid characters)\n    InvalidSessionName { name: String },\n}\n\nimpl ParseError {\n    pub fn format(&self) -> String {\n        match self {\n            ParseError::UnknownCommand { command } => {\n                format!(\"Unknown command: {}\", command)\n            }\n            ParseError::UnknownSubcommand {\n                subcommand,\n                valid_options,\n            } => {\n                format!(\n                    \"Unknown subcommand: {}\\nValid options: {}\",\n                    subcommand,\n                    valid_options.join(\", \")\n                )\n            }\n            ParseError::MissingArguments { context, usage } => {\n                format!(\n                    \"Missing arguments for: {}\\nUsage: agent-browser {}\",\n                    context, usage\n                )\n            }\n            ParseError::InvalidValue { message, usage } => {\n                format!(\"{}\\nUsage: agent-browser {}\", message, usage)\n            }\n            ParseError::InvalidSessionName { name } => session_name_error(name),\n        }\n    }\n}\n\npub fn gen_id() -> String {\n    format!(\n        \"r{}\",\n        std::time::SystemTime::now()\n            .duration_since(std::time::UNIX_EPOCH)\n            .unwrap()\n            .as_micros()\n            % 1000000\n    )\n}\n\npub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError> {\n    if args.is_empty() {\n        return Err(ParseError::MissingArguments {\n            context: \"\".to_string(),\n            usage: \"<command> [args...]\",\n        });\n    }\n\n    let cmd = args[0].as_str();\n    let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();\n    let id = gen_id();\n\n    if flags.cli_annotate && cmd != \"screenshot\" {\n        eprintln!(\n            \"{} --annotate only applies to the screenshot command\",\n            color::warning_indicator()\n        );\n    }\n\n    match cmd {\n        // === Navigation ===\n        // Maps to \"navigate\" action in protocol; reflected in ACTION_CATEGORIES in action-policy.ts\n        \"open\" | \"goto\" | \"navigate\" => {\n            let url = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: cmd.to_string(),\n                usage: \"open <url>\",\n            })?;\n            let url_lower = url.to_lowercase();\n            let url = if url_lower.starts_with(\"http://\")\n                || url_lower.starts_with(\"https://\")\n                || url_lower.starts_with(\"about:\")\n                || url_lower.starts_with(\"data:\")\n                || url_lower.starts_with(\"file:\")\n                || url_lower.starts_with(\"chrome-extension://\")\n                || url_lower.starts_with(\"chrome://\")\n            {\n                url.to_string()\n            } else {\n                format!(\"https://{}\", url)\n            };\n            let mut nav_cmd = json!({ \"id\": id, \"action\": \"navigate\", \"url\": url });\n            // If --headers flag is set, include headers (scoped to this origin)\n            if let Some(ref headers_json) = flags.headers {\n                let headers =\n                    serde_json::from_str::<serde_json::Value>(headers_json).map_err(|_| {\n                        ParseError::InvalidValue {\n                            message: format!(\"Invalid JSON for --headers: {}\", headers_json),\n                            usage: \"open <url> --headers '{\\\"Key\\\": \\\"Value\\\"}'\",\n                        }\n                    })?;\n                nav_cmd[\"headers\"] = headers;\n            }\n            // Include iOS device info if specified (needed for auto-launch with existing daemon)\n            if flags.provider.as_deref() == Some(\"ios\") {\n                if let Some(ref device) = flags.device {\n                    nav_cmd[\"iosDevice\"] = json!(device);\n                }\n            }\n            Ok(nav_cmd)\n        }\n        \"back\" => Ok(json!({ \"id\": id, \"action\": \"back\" })),\n        \"forward\" => Ok(json!({ \"id\": id, \"action\": \"forward\" })),\n        \"reload\" => Ok(json!({ \"id\": id, \"action\": \"reload\" })),\n\n        // === Core Actions ===\n        \"click\" => {\n            let new_tab = rest.contains(&\"--new-tab\");\n            let sel = rest\n                .iter()\n                .find(|arg| **arg != \"--new-tab\")\n                .ok_or_else(|| ParseError::MissingArguments {\n                    context: \"click\".to_string(),\n                    usage: \"click <selector> [--new-tab]\",\n                })?;\n            if new_tab {\n                Ok(json!({ \"id\": id, \"action\": \"click\", \"selector\": sel, \"newTab\": true }))\n            } else {\n                Ok(json!({ \"id\": id, \"action\": \"click\", \"selector\": sel }))\n            }\n        }\n        \"dblclick\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"dblclick\".to_string(),\n                usage: \"dblclick <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"dblclick\", \"selector\": sel }))\n        }\n        \"fill\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"fill\".to_string(),\n                usage: \"fill <selector> <text>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"fill\", \"selector\": sel, \"value\": rest[1..].join(\" \") }))\n        }\n        \"type\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"type\".to_string(),\n                usage: \"type <selector> <text>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"type\", \"selector\": sel, \"text\": rest[1..].join(\" \") }))\n        }\n        \"hover\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"hover\".to_string(),\n                usage: \"hover <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"hover\", \"selector\": sel }))\n        }\n        \"focus\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"focus\".to_string(),\n                usage: \"focus <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"focus\", \"selector\": sel }))\n        }\n        \"check\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"check\".to_string(),\n                usage: \"check <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"check\", \"selector\": sel }))\n        }\n        \"uncheck\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"uncheck\".to_string(),\n                usage: \"uncheck <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"uncheck\", \"selector\": sel }))\n        }\n        \"select\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"select\".to_string(),\n                usage: \"select <selector> <value...>\",\n            })?;\n            let _val = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"select\".to_string(),\n                usage: \"select <selector> <value...>\",\n            })?;\n            let values = &rest[1..];\n            if values.len() == 1 {\n                Ok(json!({ \"id\": id, \"action\": \"select\", \"selector\": sel, \"values\": values[0] }))\n            } else {\n                Ok(json!({ \"id\": id, \"action\": \"select\", \"selector\": sel, \"values\": values }))\n            }\n        }\n        \"drag\" => {\n            let src = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"drag\".to_string(),\n                usage: \"drag <source> <target>\",\n            })?;\n            let tgt = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"drag\".to_string(),\n                usage: \"drag <source> <target>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"drag\", \"source\": src, \"target\": tgt }))\n        }\n        \"upload\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"upload\".to_string(),\n                usage: \"upload <selector> <files...>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"upload\", \"selector\": sel, \"files\": &rest[1..] }))\n        }\n        \"download\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"download\".to_string(),\n                usage: \"download <selector> <path>\",\n            })?;\n            let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"download\".to_string(),\n                usage: \"download <selector> <path>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"download\", \"selector\": sel, \"path\": path }))\n        }\n\n        // === Keyboard ===\n        \"press\" | \"key\" => {\n            let key = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"press\".to_string(),\n                usage: \"press <key>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"press\", \"key\": key }))\n        }\n        \"keydown\" => {\n            let key = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"keydown\".to_string(),\n                usage: \"keydown <key>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"keydown\", \"key\": key }))\n        }\n        \"keyup\" => {\n            let key = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"keyup\".to_string(),\n                usage: \"keyup <key>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"keyup\", \"key\": key }))\n        }\n        \"keyboard\" => {\n            let sub = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"keyboard\".to_string(),\n                usage: \"keyboard <type|inserttext> <text>\",\n            })?;\n            match *sub {\n                \"type\" => {\n                    let text: String = rest[1..].join(\" \");\n                    if text.is_empty() {\n                        return Err(ParseError::MissingArguments {\n                            context: \"keyboard type\".to_string(),\n                            usage: \"keyboard type <text>\",\n                        });\n                    }\n                    Ok(json!({ \"id\": id, \"action\": \"keyboard\", \"subaction\": \"type\", \"text\": text }))\n                }\n                \"inserttext\" | \"insertText\" => {\n                    let text: String = rest[1..].join(\" \");\n                    if text.is_empty() {\n                        return Err(ParseError::MissingArguments {\n                            context: \"keyboard inserttext\".to_string(),\n                            usage: \"keyboard inserttext <text>\",\n                        });\n                    }\n                    Ok(\n                        json!({ \"id\": id, \"action\": \"keyboard\", \"subaction\": \"insertText\", \"text\": text }),\n                    )\n                }\n                _ => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: &[\"type\", \"inserttext\"],\n                }),\n            }\n        }\n\n        // === Scroll ===\n        \"scroll\" => {\n            let mut cmd = json!({ \"id\": id, \"action\": \"scroll\" });\n            let obj = cmd.as_object_mut().unwrap();\n            let mut positional_index = 0;\n            let mut i = 0;\n            while i < rest.len() {\n                match rest[i] {\n                    \"-s\" | \"--selector\" => {\n                        if let Some(s) = rest.get(i + 1) {\n                            obj.insert(\"selector\".to_string(), json!(s));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"scroll --selector\".to_string(),\n                                usage: \"scroll [direction] [amount] [--selector <sel>]\",\n                            });\n                        }\n                    }\n                    arg if arg.starts_with('-') => {}\n                    _ => {\n                        match positional_index {\n                            0 => {\n                                obj.insert(\"direction\".to_string(), json!(rest[i]));\n                            }\n                            1 => {\n                                if let Ok(n) = rest[i].parse::<i32>() {\n                                    obj.insert(\"amount\".to_string(), json!(n));\n                                }\n                            }\n                            _ => {}\n                        }\n                        positional_index += 1;\n                    }\n                }\n                i += 1;\n            }\n            if !obj.contains_key(\"direction\") {\n                obj.insert(\"direction\".to_string(), json!(\"down\"));\n            }\n            if !obj.contains_key(\"amount\") {\n                obj.insert(\"amount\".to_string(), json!(300));\n            }\n            Ok(cmd)\n        }\n        \"scrollintoview\" | \"scrollinto\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"scrollintoview\".to_string(),\n                usage: \"scrollintoview <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"scrollintoview\", \"selector\": sel }))\n        }\n\n        // === Wait ===\n        \"wait\" => {\n            // Check for --url flag: wait --url \"**/dashboard\"\n            if let Some(idx) = rest.iter().position(|&s| s == \"--url\" || s == \"-u\") {\n                let url = rest\n                    .get(idx + 1)\n                    .ok_or_else(|| ParseError::MissingArguments {\n                        context: \"wait --url\".to_string(),\n                        usage: \"wait --url <pattern>\",\n                    })?;\n                return Ok(json!({ \"id\": id, \"action\": \"waitforurl\", \"url\": url }));\n            }\n\n            // Check for --load flag: wait --load networkidle\n            if let Some(idx) = rest.iter().position(|&s| s == \"--load\" || s == \"-l\") {\n                let state = rest\n                    .get(idx + 1)\n                    .ok_or_else(|| ParseError::MissingArguments {\n                        context: \"wait --load\".to_string(),\n                        usage: \"wait --load <state>\",\n                    })?;\n                return Ok(json!({ \"id\": id, \"action\": \"waitforloadstate\", \"state\": state }));\n            }\n\n            // Check for --fn flag: wait --fn \"window.ready === true\"\n            if let Some(idx) = rest.iter().position(|&s| s == \"--fn\" || s == \"-f\") {\n                let expr = rest\n                    .get(idx + 1)\n                    .ok_or_else(|| ParseError::MissingArguments {\n                        context: \"wait --fn\".to_string(),\n                        usage: \"wait --fn <expression>\",\n                    })?;\n                return Ok(json!({ \"id\": id, \"action\": \"waitforfunction\", \"expression\": expr }));\n            }\n\n            // Check for --text flag: wait --text \"Welcome\" [--timeout ms]\n            if let Some(idx) = rest.iter().position(|&s| s == \"--text\" || s == \"-t\") {\n                let text = rest\n                    .get(idx + 1)\n                    .ok_or_else(|| ParseError::MissingArguments {\n                        context: \"wait --text\".to_string(),\n                        usage: \"wait --text <text>\",\n                    })?;\n                let mut cmd = json!({ \"id\": id, \"action\": \"wait\", \"text\": text });\n                if let Some(t_idx) = rest.iter().position(|&s| s == \"--timeout\") {\n                    if let Some(Ok(ms)) = rest.get(t_idx + 1).map(|s| s.parse::<u64>()) {\n                        cmd[\"timeout\"] = json!(ms);\n                    }\n                }\n                return Ok(cmd);\n            }\n\n            // Check for --download flag: wait --download [path] [--timeout ms]\n            if rest.iter().any(|&s| s == \"--download\" || s == \"-d\") {\n                let mut cmd = json!({ \"id\": id, \"action\": \"waitfordownload\" });\n                // Check for optional path (first non-flag argument after --download)\n                let download_idx = rest\n                    .iter()\n                    .position(|&s| s == \"--download\" || s == \"-d\")\n                    .unwrap();\n                if let Some(path) = rest.get(download_idx + 1) {\n                    if !path.starts_with(\"--\") {\n                        cmd[\"path\"] = json!(path);\n                    }\n                }\n                // Check for optional timeout\n                if let Some(idx) = rest.iter().position(|&s| s == \"--timeout\") {\n                    if let Some(timeout_str) = rest.get(idx + 1) {\n                        if let Ok(timeout) = timeout_str.parse::<u64>() {\n                            cmd[\"timeout\"] = json!(timeout);\n                        }\n                    }\n                }\n                return Ok(cmd);\n            }\n\n            // Default: selector or timeout\n            if let Some(arg) = rest.first() {\n                if let Ok(timeout) = arg.parse::<u64>() {\n                    Ok(json!({ \"id\": id, \"action\": \"wait\", \"timeout\": timeout }))\n                } else {\n                    Ok(json!({ \"id\": id, \"action\": \"wait\", \"selector\": arg }))\n                }\n            } else {\n                Err(ParseError::MissingArguments {\n                    context: \"wait\".to_string(),\n                    usage: \"wait <selector|ms|--url|--load|--fn|--text>\",\n                })\n            }\n        }\n\n        // === Screenshot/PDF ===\n        \"screenshot\" => {\n            // screenshot [selector] [path] [--full/-f]\n            // selector: @ref or CSS selector\n            // path: file path (contains / or . or ends with known extension)\n            let mut full_page = false;\n            let positional: Vec<&str> = rest\n                .iter()\n                .filter(|arg| match **arg {\n                    \"--full\" | \"-f\" => {\n                        full_page = true;\n                        false\n                    }\n                    _ => true,\n                })\n                .copied()\n                .collect();\n            let (selector, path) = match (positional.first(), positional.get(1)) {\n                (Some(first), Some(second)) => {\n                    // Two args: first is selector, second is path\n                    (Some(*first), Some(*second))\n                }\n                (Some(first), None) => {\n                    // One arg: determine if it's a selector or a path\n                    let is_relative_path = first.starts_with(\"./\") || first.starts_with(\"../\");\n                    let is_selector = !is_relative_path\n                        && (first.starts_with('.')\n                            || first.starts_with('#')\n                            || first.starts_with('@'));\n                    let has_path_extension = first.ends_with(\".png\")\n                        || first.ends_with(\".jpg\")\n                        || first.ends_with(\".jpeg\")\n                        || first.ends_with(\".webp\");\n                    let is_path = is_relative_path || first.contains('/') || has_path_extension;\n                    if is_selector || !is_path {\n                        (Some(*first), None)\n                    } else {\n                        (None, Some(*first))\n                    }\n                }\n                _ => (None, None),\n            };\n            let mut cmd = json!({\n                \"id\": id, \"action\": \"screenshot\",\n                \"path\": path, \"selector\": selector,\n                \"fullPage\": full_page, \"annotate\": flags.annotate\n            });\n            if let Some(ref fmt) = flags.screenshot_format {\n                cmd[\"format\"] = json!(fmt);\n            }\n            if let Some(q) = flags.screenshot_quality {\n                cmd[\"quality\"] = json!(q);\n                if flags.screenshot_format.as_deref() != Some(\"jpeg\") {\n                    eprintln!(\n                        \"{} --screenshot-quality is ignored for PNG; use --screenshot-format jpeg\",\n                        color::warning_indicator()\n                    );\n                }\n            }\n            if let Some(ref dir) = flags.screenshot_dir {\n                cmd[\"screenshotDir\"] = json!(dir);\n            }\n            Ok(cmd)\n        }\n        \"pdf\" => {\n            let path = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"pdf\".to_string(),\n                usage: \"pdf <path>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"pdf\", \"path\": path }))\n        }\n\n        // === Snapshot ===\n        \"snapshot\" => {\n            let mut cmd = json!({ \"id\": id, \"action\": \"snapshot\" });\n            let obj = cmd.as_object_mut().unwrap();\n            let mut i = 0;\n            while i < rest.len() {\n                match rest[i] {\n                    \"-i\" | \"--interactive\" => {\n                        obj.insert(\"interactive\".to_string(), json!(true));\n                    }\n                    \"-c\" | \"--compact\" => {\n                        obj.insert(\"compact\".to_string(), json!(true));\n                    }\n                    \"-C\" | \"--cursor\" => {\n                        obj.insert(\"cursor\".to_string(), json!(true));\n                    }\n                    \"-d\" | \"--depth\" => {\n                        if let Some(d) = rest.get(i + 1) {\n                            if let Ok(n) = d.parse::<i32>() {\n                                obj.insert(\"maxDepth\".to_string(), json!(n));\n                                i += 1;\n                            }\n                        }\n                    }\n                    \"-s\" | \"--selector\" => {\n                        if let Some(s) = rest.get(i + 1) {\n                            obj.insert(\"selector\".to_string(), json!(s));\n                            i += 1;\n                        }\n                    }\n                    _ => {}\n                }\n                i += 1;\n            }\n            Ok(cmd)\n        }\n\n        // === Eval ===\n        \"eval\" => {\n            // Check for flags: -b/--base64 or --stdin\n            let (is_base64, is_stdin, script_parts): (bool, bool, &[&str]) =\n                if rest.first() == Some(&\"-b\") || rest.first() == Some(&\"--base64\") {\n                    (true, false, &rest[1..])\n                } else if rest.first() == Some(&\"--stdin\") {\n                    (false, true, &rest[1..])\n                } else {\n                    (false, false, rest.as_slice())\n                };\n\n            let script = if is_stdin {\n                // Read script from stdin\n                let stdin = io::stdin();\n                let lines: Vec<String> = stdin\n                    .lock()\n                    .lines()\n                    .map(|l| l.unwrap_or_default())\n                    .collect();\n                lines.join(\"\\n\")\n            } else {\n                let raw_script = script_parts.join(\" \");\n                if is_base64 {\n                    let decoded =\n                        STANDARD\n                            .decode(&raw_script)\n                            .map_err(|_| ParseError::InvalidValue {\n                                message: \"Invalid base64 encoding\".to_string(),\n                                usage: \"eval -b <base64-encoded-script>\",\n                            })?;\n                    String::from_utf8(decoded).map_err(|_| ParseError::InvalidValue {\n                        message: \"Base64 decoded to invalid UTF-8\".to_string(),\n                        usage: \"eval -b <base64-encoded-script>\",\n                    })?\n                } else {\n                    raw_script\n                }\n            };\n            Ok(json!({ \"id\": id, \"action\": \"evaluate\", \"script\": script }))\n        }\n\n        // === Close ===\n        \"close\" | \"quit\" | \"exit\" => Ok(json!({ \"id\": id, \"action\": \"close\" })),\n\n        // === Inspect ===\n        \"inspect\" => Ok(json!({ \"id\": id, \"action\": \"inspect\" })),\n\n        // === Authentication Vault ===\n        \"auth\" => {\n            let sub = rest.first().map(|s| s.as_ref());\n            match sub {\n                Some(\"save\") => {\n                    let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"auth save\".to_string(),\n                        usage: \"agent-browser auth save <name> --url <url> --username <user> --password <pass>\",\n                    })?;\n\n                    let mut url = None;\n                    let mut username = None;\n                    let mut password = None;\n                    let mut password_stdin = false;\n                    let mut username_selector = None;\n                    let mut password_selector = None;\n                    let mut submit_selector = None;\n\n                    let mut j = 2;\n                    while j < rest.len() {\n                        match rest[j] {\n                            \"--url\" => {\n                                url = rest.get(j + 1).cloned();\n                                j += 1;\n                            }\n                            \"--username\" => {\n                                username = rest.get(j + 1).cloned();\n                                j += 1;\n                            }\n                            \"--password\" => {\n                                password = rest.get(j + 1).cloned();\n                                j += 1;\n                            }\n                            \"--password-stdin\" => {\n                                password_stdin = true;\n                            }\n                            \"--username-selector\" => {\n                                username_selector = rest.get(j + 1).cloned();\n                                j += 1;\n                            }\n                            \"--password-selector\" => {\n                                password_selector = rest.get(j + 1).cloned();\n                                j += 1;\n                            }\n                            \"--submit-selector\" => {\n                                submit_selector = rest.get(j + 1).cloned();\n                                j += 1;\n                            }\n                            other => {\n                                if other.starts_with(\"--\") {\n                                    return Err(ParseError::InvalidValue {\n                                        message: format!(\"unknown flag '{}' for auth save\", other),\n                                        usage: \"agent-browser auth save <name> --url <url> --username <user> --password <pass>\",\n                                    });\n                                }\n                            }\n                        }\n                        j += 1;\n                    }\n\n                    let url_val = url.ok_or_else(|| ParseError::MissingArguments {\n                        context: \"auth save\".to_string(),\n                        usage: \"agent-browser auth save <name> --url <url> --username <user> --password <pass> [--password-stdin]\",\n                    })?;\n                    let user_val = username.ok_or_else(|| ParseError::MissingArguments {\n                        context: \"auth save\".to_string(),\n                        usage: \"agent-browser auth save <name> --url <url> --username <user> --password <pass> [--password-stdin]\",\n                    })?;\n\n                    if !password_stdin && password.is_none() {\n                        return Err(ParseError::MissingArguments {\n                            context: \"auth save\".to_string(),\n                            usage: \"agent-browser auth save <name> --url <url> --username <user> --password <pass> [--password-stdin]\",\n                        });\n                    }\n\n                    let mut cmd = json!({\n                        \"id\": id,\n                        \"action\": \"auth_save\",\n                        \"name\": name,\n                        \"url\": url_val,\n                        \"username\": user_val,\n                    });\n                    if password_stdin {\n                        cmd[\"passwordStdin\"] = json!(true);\n                    }\n                    if let Some(pass_val) = password {\n                        cmd[\"password\"] = json!(pass_val);\n                    }\n                    if let Some(us) = username_selector {\n                        cmd[\"usernameSelector\"] = json!(us);\n                    }\n                    if let Some(ps) = password_selector {\n                        cmd[\"passwordSelector\"] = json!(ps);\n                    }\n                    if let Some(ss) = submit_selector {\n                        cmd[\"submitSelector\"] = json!(ss);\n                    }\n                    Ok(cmd)\n                }\n                Some(\"login\") => {\n                    let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"auth login\".to_string(),\n                        usage: \"agent-browser auth login <name>\",\n                    })?;\n                    Ok(json!({ \"id\": id, \"action\": \"auth_login\", \"name\": name }))\n                }\n                Some(\"list\") => Ok(json!({ \"id\": id, \"action\": \"auth_list\" })),\n                Some(\"delete\") | Some(\"remove\") => {\n                    let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"auth delete\".to_string(),\n                        usage: \"agent-browser auth delete <name>\",\n                    })?;\n                    Ok(json!({ \"id\": id, \"action\": \"auth_delete\", \"name\": name }))\n                }\n                Some(\"show\") => {\n                    let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"auth show\".to_string(),\n                        usage: \"agent-browser auth show <name>\",\n                    })?;\n                    Ok(json!({ \"id\": id, \"action\": \"auth_show\", \"name\": name }))\n                }\n                _ => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.unwrap_or(\"(none)\").to_string(),\n                    valid_options: &[\"save\", \"login\", \"list\", \"delete\", \"show\"],\n                }),\n            }\n        }\n\n        // === Action Confirmation ===\n        \"confirm\" => {\n            let cid = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"confirm\".to_string(),\n                usage: \"agent-browser confirm <confirmation-id>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"confirm\", \"confirmationId\": cid }))\n        }\n        \"deny\" => {\n            let cid = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"deny\".to_string(),\n                usage: \"agent-browser deny <confirmation-id>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"deny\", \"confirmationId\": cid }))\n        }\n\n        // === Connect (CDP) ===\n        \"connect\" => {\n            let endpoint = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"connect\".to_string(),\n                usage: \"connect <port|url>\",\n            })?;\n            // Check if it's a URL (ws://, wss://, http://, https://)\n            if endpoint.starts_with(\"ws://\")\n                || endpoint.starts_with(\"wss://\")\n                || endpoint.starts_with(\"http://\")\n                || endpoint.starts_with(\"https://\")\n            {\n                Ok(json!({ \"id\": id, \"action\": \"launch\", \"cdpUrl\": endpoint }))\n            } else {\n                // It's a port number - validate and use cdpPort field\n                let port: u16 = match endpoint.parse::<u32>() {\n                    Ok(0) => {\n                        return Err(ParseError::InvalidValue {\n                            message: \"Invalid port: port must be greater than 0\".to_string(),\n                            usage: \"connect <port|url>\",\n                        });\n                    }\n                    Ok(p) if p > 65535 => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\n                                \"Invalid port: {} is out of range (valid range: 1-65535)\",\n                                p\n                            ),\n                            usage: \"connect <port|url>\",\n                        });\n                    }\n                    Ok(p) => p as u16,\n                    Err(_) => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\n                                \"Invalid value: '{}' is not a valid port number or URL\",\n                                endpoint\n                            ),\n                            usage: \"connect <port|url>\",\n                        });\n                    }\n                };\n                Ok(json!({ \"id\": id, \"action\": \"launch\", \"cdpPort\": port }))\n            }\n        }\n\n        // === Get ===\n        \"get\" => parse_get(&rest, &id),\n\n        // === Is (state checks) ===\n        \"is\" => parse_is(&rest, &id),\n\n        // === Find (locators) ===\n        \"find\" => parse_find(&rest, &id),\n\n        // === Mouse ===\n        \"mouse\" => parse_mouse(&rest, &id),\n\n        // === Set (browser settings) ===\n        \"set\" => parse_set(&rest, &id),\n\n        // === Network ===\n        \"network\" => parse_network(&rest, &id),\n\n        // === Storage ===\n        \"storage\" => parse_storage(&rest, &id),\n\n        // === Cookies ===\n        \"cookies\" => {\n            let op = rest.first().unwrap_or(&\"get\");\n            match *op {\n                \"set\" => {\n                    let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"cookies set\".to_string(),\n                        usage: \"cookies set <name> <value> [--url <url>] [--domain <domain>] [--path <path>] [--httpOnly] [--secure] [--sameSite <Strict|Lax|None>] [--expires <timestamp>]\",\n                    })?;\n                    let value = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"cookies set\".to_string(),\n                        usage: \"cookies set <name> <value> [--url <url>] [--domain <domain>] [--path <path>] [--httpOnly] [--secure] [--sameSite <Strict|Lax|None>] [--expires <timestamp>]\",\n                    })?;\n\n                    let mut cookie = json!({ \"name\": name, \"value\": value });\n\n                    // Parse optional flags\n                    let mut i = 3;\n                    while i < rest.len() {\n                        match rest[i] {\n                            \"--url\" => {\n                                if let Some(url) = rest.get(i + 1) {\n                                    cookie[\"url\"] = json!(url);\n                                    i += 2;\n                                } else {\n                                    return Err(ParseError::MissingArguments {\n                                        context: \"cookies set --url\".to_string(),\n                                        usage: \"--url <url>\",\n                                    });\n                                }\n                            }\n                            \"--domain\" => {\n                                if let Some(domain) = rest.get(i + 1) {\n                                    cookie[\"domain\"] = json!(domain);\n                                    i += 2;\n                                } else {\n                                    return Err(ParseError::MissingArguments {\n                                        context: \"cookies set --domain\".to_string(),\n                                        usage: \"--domain <domain>\",\n                                    });\n                                }\n                            }\n                            \"--path\" => {\n                                if let Some(path) = rest.get(i + 1) {\n                                    cookie[\"path\"] = json!(path);\n                                    i += 2;\n                                } else {\n                                    return Err(ParseError::MissingArguments {\n                                        context: \"cookies set --path\".to_string(),\n                                        usage: \"--path <path>\",\n                                    });\n                                }\n                            }\n                            \"--httpOnly\" => {\n                                cookie[\"httpOnly\"] = json!(true);\n                                i += 1;\n                            }\n                            \"--secure\" => {\n                                cookie[\"secure\"] = json!(true);\n                                i += 1;\n                            }\n                            \"--sameSite\" => {\n                                if let Some(same_site) = rest.get(i + 1) {\n                                    // Validate sameSite value\n                                    if *same_site == \"Strict\"\n                                        || *same_site == \"Lax\"\n                                        || *same_site == \"None\"\n                                    {\n                                        cookie[\"sameSite\"] = json!(same_site);\n                                        i += 2;\n                                    } else {\n                                        return Err(ParseError::MissingArguments {\n                                            context: \"cookies set --sameSite\".to_string(),\n                                            usage: \"--sameSite <Strict|Lax|None>\",\n                                        });\n                                    }\n                                } else {\n                                    return Err(ParseError::MissingArguments {\n                                        context: \"cookies set --sameSite\".to_string(),\n                                        usage: \"--sameSite <Strict|Lax|None>\",\n                                    });\n                                }\n                            }\n                            \"--expires\" => {\n                                if let Some(expires_str) = rest.get(i + 1) {\n                                    if let Ok(expires) = expires_str.parse::<i64>() {\n                                        cookie[\"expires\"] = json!(expires);\n                                        i += 2;\n                                    } else {\n                                        return Err(ParseError::MissingArguments {\n                                            context: \"cookies set --expires\".to_string(),\n                                            usage: \"--expires <timestamp>\",\n                                        });\n                                    }\n                                } else {\n                                    return Err(ParseError::MissingArguments {\n                                        context: \"cookies set --expires\".to_string(),\n                                        usage: \"--expires <timestamp>\",\n                                    });\n                                }\n                            }\n                            _ => {\n                                // Unknown flag, skip it (or could error)\n                                i += 1;\n                            }\n                        }\n                    }\n\n                    Ok(json!({ \"id\": id, \"action\": \"cookies_set\", \"cookies\": [cookie] }))\n                }\n                \"clear\" => Ok(json!({ \"id\": id, \"action\": \"cookies_clear\" })),\n                _ => Ok(json!({ \"id\": id, \"action\": \"cookies_get\" })),\n            }\n        }\n\n        // === Tabs ===\n        \"tab\" => match rest.first().copied() {\n            Some(\"new\") => {\n                let mut cmd = json!({ \"id\": id, \"action\": \"tab_new\" });\n                if let Some(url) = rest.get(1) {\n                    cmd[\"url\"] = json!(url);\n                }\n                Ok(cmd)\n            }\n            Some(\"list\") => Ok(json!({ \"id\": id, \"action\": \"tab_list\" })),\n            Some(\"close\") => {\n                let mut cmd = json!({ \"id\": id, \"action\": \"tab_close\" });\n                if let Some(index) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) {\n                    cmd[\"index\"] = json!(index);\n                }\n                Ok(cmd)\n            }\n            Some(n) if n.parse::<i32>().is_ok() => {\n                let index = n.parse::<i32>().expect(\"already checked parse succeeds\");\n                Ok(json!({ \"id\": id, \"action\": \"tab_switch\", \"index\": index }))\n            }\n            _ => Ok(json!({ \"id\": id, \"action\": \"tab_list\" })),\n        },\n\n        // === Window ===\n        \"window\" => {\n            const VALID: &[&str] = &[\"new\"];\n            match rest.first().copied() {\n                Some(\"new\") => Ok(json!({ \"id\": id, \"action\": \"window_new\" })),\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"window\".to_string(),\n                    usage: \"window <new>\",\n                }),\n            }\n        }\n\n        // === Frame ===\n        \"frame\" => {\n            if rest.first().copied() == Some(\"main\") {\n                Ok(json!({ \"id\": id, \"action\": \"mainframe\" }))\n            } else {\n                let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                    context: \"frame\".to_string(),\n                    usage: \"frame <selector|main>\",\n                })?;\n                Ok(json!({ \"id\": id, \"action\": \"frame\", \"selector\": sel }))\n            }\n        }\n\n        // === Dialog ===\n        \"dialog\" => {\n            const VALID: &[&str] = &[\"accept\", \"dismiss\"];\n            match rest.first().copied() {\n                Some(\"accept\") => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"dialog\", \"response\": \"accept\" });\n                    if let Some(prompt_text) = rest.get(1) {\n                        cmd[\"promptText\"] = json!(prompt_text);\n                    }\n                    Ok(cmd)\n                }\n                Some(\"dismiss\") => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"dialog\", \"response\": \"dismiss\" });\n                    if let Some(prompt_text) = rest.get(1) {\n                        cmd[\"promptText\"] = json!(prompt_text);\n                    }\n                    Ok(cmd)\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"dialog\".to_string(),\n                    usage: \"dialog <accept|dismiss> [text]\",\n                }),\n            }\n        }\n\n        // === Debug ===\n        \"trace\" => {\n            const VALID: &[&str] = &[\"start\", \"stop\"];\n            match rest.first().copied() {\n                Some(\"start\") => Ok(json!({ \"id\": id, \"action\": \"trace_start\" })),\n                Some(\"stop\") => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"trace_stop\" });\n                    if let Some(path) = rest.get(1) {\n                        cmd[\"path\"] = json!(path);\n                    }\n                    Ok(cmd)\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"trace\".to_string(),\n                    usage: \"trace <start|stop> [path]\",\n                }),\n            }\n        }\n\n        // === Profiler (CDP Tracing / Chromium profiling) ===\n        \"profiler\" => {\n            const VALID: &[&str] = &[\"start\", \"stop\"];\n            match rest.first().copied() {\n                Some(\"start\") => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"profiler_start\" });\n                    if let Some(idx) = rest.iter().position(|s| *s == \"--categories\") {\n                        if let Some(cats) = rest.get(idx + 1) {\n                            let categories: Vec<&str> = cats.split(',').collect();\n                            cmd[\"categories\"] = json!(categories);\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"profiler start --categories\".to_string(),\n                                usage: \"--categories <list>\",\n                            });\n                        }\n                    }\n                    Ok(cmd)\n                }\n                Some(\"stop\") => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"profiler_stop\" });\n                    if let Some(path) = rest.get(1) {\n                        cmd[\"path\"] = json!(path);\n                    }\n                    Ok(cmd)\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"profiler\".to_string(),\n                    usage: \"profiler <start|stop> [options]\",\n                }),\n            }\n        }\n\n        // === Recording (browser video recording) ===\n        \"record\" => {\n            const VALID: &[&str] = &[\"start\", \"stop\", \"restart\"];\n            match rest.first().copied() {\n                Some(\"start\") => {\n                    let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"record start\".to_string(),\n                        usage: \"record start <output.webm> [url]\",\n                    })?;\n                    // Optional URL parameter\n                    let url = rest.get(2);\n                    let mut cmd = json!({ \"id\": id, \"action\": \"recording_start\", \"path\": path });\n                    if let Some(u) = url {\n                        // Add https:// prefix if needed (preserve special schemes)\n                        let url_str = if u.starts_with(\"http\") || u.contains(\"://\") {\n                            u.to_string()\n                        } else {\n                            format!(\"https://{}\", u)\n                        };\n                        cmd[\"url\"] = json!(url_str);\n                    }\n                    Ok(cmd)\n                }\n                Some(\"stop\") => Ok(json!({ \"id\": id, \"action\": \"recording_stop\" })),\n                Some(\"restart\") => {\n                    let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"record restart\".to_string(),\n                        usage: \"record restart <output.webm> [url]\",\n                    })?;\n                    // Optional URL parameter\n                    let url = rest.get(2);\n                    let mut cmd = json!({ \"id\": id, \"action\": \"recording_restart\", \"path\": path });\n                    if let Some(u) = url {\n                        // Add https:// prefix if needed (preserve special schemes)\n                        let url_str = if u.starts_with(\"http\") || u.contains(\"://\") {\n                            u.to_string()\n                        } else {\n                            format!(\"https://{}\", u)\n                        };\n                        cmd[\"url\"] = json!(url_str);\n                    }\n                    Ok(cmd)\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"record\".to_string(),\n                    usage: \"record <start|stop|restart> [path] [url]\",\n                }),\n            }\n        }\n        \"console\" => {\n            let clear = rest.contains(&\"--clear\");\n            Ok(json!({ \"id\": id, \"action\": \"console\", \"clear\": clear }))\n        }\n        \"errors\" => {\n            let clear = rest.contains(&\"--clear\");\n            Ok(json!({ \"id\": id, \"action\": \"errors\", \"clear\": clear }))\n        }\n        \"highlight\" => {\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"highlight\".to_string(),\n                usage: \"highlight <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"highlight\", \"selector\": sel }))\n        }\n\n        // === Clipboard ===\n        \"clipboard\" => match rest.first().copied() {\n            Some(\"read\") | None => {\n                Ok(json!({ \"id\": id, \"action\": \"clipboard\", \"operation\": \"read\" }))\n            }\n            Some(\"write\") => {\n                rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                    context: \"clipboard write\".to_string(),\n                    usage: \"clipboard write <text>\",\n                })?;\n                let text = rest[1..].join(\" \");\n                Ok(json!({ \"id\": id, \"action\": \"clipboard\", \"operation\": \"write\", \"text\": text }))\n            }\n            Some(\"copy\") => Ok(json!({ \"id\": id, \"action\": \"clipboard\", \"operation\": \"copy\" })),\n            Some(\"paste\") => Ok(json!({ \"id\": id, \"action\": \"clipboard\", \"operation\": \"paste\" })),\n            Some(sub) => Err(ParseError::UnknownSubcommand {\n                subcommand: sub.to_string(),\n                valid_options: &[\"read\", \"write\", \"copy\", \"paste\"],\n            }),\n        },\n\n        // === State ===\n        \"state\" => {\n            const VALID: &[&str] = &[\"save\", \"load\", \"list\", \"clear\", \"show\", \"clean\", \"rename\"];\n            match rest.first().copied() {\n                Some(\"save\") => {\n                    let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"state save\".to_string(),\n                        usage: \"state save <path>\",\n                    })?;\n                    Ok(json!({ \"id\": id, \"action\": \"state_save\", \"path\": path }))\n                }\n                Some(\"load\") => {\n                    let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"state load\".to_string(),\n                        usage: \"state load <path>\",\n                    })?;\n                    Ok(json!({ \"id\": id, \"action\": \"state_load\", \"path\": path }))\n                }\n                Some(\"list\") => Ok(json!({ \"id\": id, \"action\": \"state_list\" })),\n                Some(\"clear\") => {\n                    let mut session_name: Option<&str> = None;\n                    let mut all = false;\n\n                    let mut i = 1;\n                    while i < rest.len() {\n                        match rest[i] {\n                            \"--all\" | \"-a\" => {\n                                all = true;\n                            }\n                            arg if !arg.starts_with('-') => {\n                                session_name = Some(arg);\n                            }\n                            _ => {}\n                        }\n                        i += 1;\n                    }\n\n                    if let Some(name) = session_name {\n                        if !is_valid_session_name(name) {\n                            return Err(ParseError::InvalidSessionName {\n                                name: name.to_string(),\n                            });\n                        }\n                    }\n\n                    let mut cmd = json!({ \"id\": id, \"action\": \"state_clear\" });\n                    if all {\n                        cmd[\"all\"] = json!(true);\n                    }\n                    if let Some(name) = session_name {\n                        cmd[\"sessionName\"] = json!(name);\n                    }\n                    Ok(cmd)\n                }\n                Some(\"show\") => {\n                    let filename = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"state show\".to_string(),\n                        usage: \"state show <filename>\",\n                    })?;\n                    Ok(json!({ \"id\": id, \"action\": \"state_show\", \"filename\": filename }))\n                }\n                Some(\"clean\") => {\n                    let mut days: Option<i64> = None;\n\n                    let mut i = 1;\n                    while i < rest.len() {\n                        if rest[i] == \"--older-than\" {\n                            if let Some(d) = rest.get(i + 1) {\n                                days = d.parse().ok();\n                                i += 1;\n                            }\n                        }\n                        i += 1;\n                    }\n\n                    let days = days.ok_or_else(|| ParseError::MissingArguments {\n                        context: \"state clean\".to_string(),\n                        usage: \"state clean --older-than <days>\",\n                    })?;\n\n                    Ok(json!({ \"id\": id, \"action\": \"state_clean\", \"days\": days }))\n                }\n                Some(\"rename\") => {\n                    let old_name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"state rename\".to_string(),\n                        usage: \"state rename <old-name> <new-name>\",\n                    })?;\n                    let new_name = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                        context: \"state rename\".to_string(),\n                        usage: \"state rename <old-name> <new-name>\",\n                    })?;\n                    let old_name = old_name.trim_end_matches(\".json\");\n                    let new_name = new_name.trim_end_matches(\".json\");\n\n                    if !is_valid_session_name(old_name) {\n                        return Err(ParseError::InvalidSessionName {\n                            name: old_name.to_string(),\n                        });\n                    }\n                    if !is_valid_session_name(new_name) {\n                        return Err(ParseError::InvalidSessionName {\n                            name: new_name.to_string(),\n                        });\n                    }\n\n                    Ok(\n                        json!({ \"id\": id, \"action\": \"state_rename\", \"oldName\": old_name, \"newName\": new_name }),\n                    )\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"state\".to_string(),\n                    usage: \"state <save|load|list|clear|show|clean|rename> ...\",\n                }),\n            }\n        }\n\n        // === iOS-specific commands ===\n        \"tap\" => {\n            // Alias for click (semantic clarity for touch interfaces)\n            let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"tap\".to_string(),\n                usage: \"tap <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"tap\", \"selector\": sel }))\n        }\n        \"swipe\" => {\n            let direction = rest.first().ok_or_else(|| ParseError::MissingArguments {\n                context: \"swipe\".to_string(),\n                usage: \"swipe <up|down|left|right> [distance]\",\n            })?;\n            let valid_directions = [\"up\", \"down\", \"left\", \"right\"];\n            if !valid_directions.contains(direction) {\n                return Err(ParseError::InvalidValue {\n                    message: format!(\"Invalid swipe direction: {}\", direction),\n                    usage: \"swipe <up|down|left|right> [distance]\",\n                });\n            }\n            let mut cmd = json!({ \"id\": id, \"action\": \"swipe\", \"direction\": direction });\n            if let Some(distance) = rest.get(1) {\n                if let Ok(d) = distance.parse::<u32>() {\n                    cmd.as_object_mut()\n                        .unwrap()\n                        .insert(\"distance\".to_string(), json!(d));\n                }\n            }\n            Ok(cmd)\n        }\n        \"device\" => {\n            match rest.first().copied() {\n                Some(\"list\") | None => {\n                    // List available iOS simulators\n                    Ok(json!({ \"id\": id, \"action\": \"device_list\" }))\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: &[\"list\"],\n                }),\n            }\n        }\n\n        \"diff\" => parse_diff(&rest, &id),\n\n        // === Batch ===\n        \"batch\" => {\n            let bail = rest.contains(&\"--bail\");\n            Ok(json!({ \"id\": id, \"action\": \"batch\", \"bail\": bail }))\n        }\n\n        _ => Err(ParseError::UnknownCommand {\n            command: cmd.to_string(),\n        }),\n    }\n}\n\nfn parse_diff(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\"snapshot\", \"screenshot\", \"url\"];\n\n    match rest.first().copied() {\n        Some(\"snapshot\") => {\n            let mut cmd = json!({ \"id\": id, \"action\": \"diff_snapshot\" });\n            let obj = cmd.as_object_mut().unwrap();\n            let mut i = 1;\n            while i < rest.len() {\n                match rest[i] {\n                    \"-b\" | \"--baseline\" => {\n                        if let Some(path) = rest.get(i + 1) {\n                            obj.insert(\"baseline\".to_string(), json!(path));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff snapshot --baseline\".to_string(),\n                                usage: \"diff snapshot --baseline <file>\",\n                            });\n                        }\n                    }\n                    \"-s\" | \"--selector\" => {\n                        if let Some(s) = rest.get(i + 1) {\n                            obj.insert(\"selector\".to_string(), json!(s));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff snapshot --selector\".to_string(),\n                                usage: \"diff snapshot --selector <sel>\",\n                            });\n                        }\n                    }\n                    \"-c\" | \"--compact\" => {\n                        obj.insert(\"compact\".to_string(), json!(true));\n                    }\n                    \"-d\" | \"--depth\" => {\n                        if let Some(d) = rest.get(i + 1) {\n                            match d.parse::<u32>() {\n                                Ok(n) => {\n                                    obj.insert(\"maxDepth\".to_string(), json!(n));\n                                    i += 1;\n                                }\n                                Err(_) => {\n                                    return Err(ParseError::InvalidValue {\n                                        message: format!(\n                                            \"Depth must be a non-negative integer, got: {}\",\n                                            d\n                                        ),\n                                        usage: \"diff snapshot --depth <n>\",\n                                    });\n                                }\n                            }\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff snapshot --depth\".to_string(),\n                                usage: \"diff snapshot --depth <n>\",\n                            });\n                        }\n                    }\n                    other if other.starts_with('-') => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\"Unknown flag: {}\", other),\n                            usage: \"diff snapshot [--baseline <file>] [--selector <sel>] [--compact] [--depth <n>]\",\n                        });\n                    }\n                    other => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\"Unexpected argument: {}\", other),\n                            usage: \"diff snapshot [--baseline <file>] [--selector <sel>] [--compact] [--depth <n>]\",\n                        });\n                    }\n                }\n                i += 1;\n            }\n            Ok(cmd)\n        }\n        Some(\"screenshot\") => {\n            let mut cmd = json!({ \"id\": id, \"action\": \"diff_screenshot\" });\n            let obj = cmd.as_object_mut().unwrap();\n            let mut i = 1;\n            while i < rest.len() {\n                match rest[i] {\n                    \"-b\" | \"--baseline\" => {\n                        if let Some(path) = rest.get(i + 1) {\n                            obj.insert(\"baseline\".to_string(), json!(path));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff screenshot --baseline\".to_string(),\n                                usage: \"diff screenshot --baseline <file>\",\n                            });\n                        }\n                    }\n                    \"-o\" | \"--output\" => {\n                        if let Some(path) = rest.get(i + 1) {\n                            obj.insert(\"output\".to_string(), json!(path));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff screenshot --output\".to_string(),\n                                usage: \"diff screenshot --output <file>\",\n                            });\n                        }\n                    }\n                    \"-t\" | \"--threshold\" => {\n                        if let Some(t) = rest.get(i + 1) {\n                            match t.parse::<f64>() {\n                                Ok(n) if (0.0..=1.0).contains(&n) => {\n                                    obj.insert(\"threshold\".to_string(), json!(n));\n                                    i += 1;\n                                }\n                                Ok(n) => {\n                                    return Err(ParseError::InvalidValue {\n                                        message: format!(\n                                            \"Threshold must be between 0 and 1, got {}\",\n                                            n\n                                        ),\n                                        usage: \"diff screenshot --threshold <0-1>\",\n                                    });\n                                }\n                                Err(_) => {\n                                    return Err(ParseError::InvalidValue {\n                                        message: format!(\"Invalid threshold value: {}\", t),\n                                        usage: \"diff screenshot --threshold <0-1>\",\n                                    });\n                                }\n                            }\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff screenshot --threshold\".to_string(),\n                                usage: \"diff screenshot --threshold <0-1>\",\n                            });\n                        }\n                    }\n                    \"-s\" | \"--selector\" => {\n                        if let Some(s) = rest.get(i + 1) {\n                            obj.insert(\"selector\".to_string(), json!(s));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff screenshot --selector\".to_string(),\n                                usage: \"diff screenshot --selector <sel>\",\n                            });\n                        }\n                    }\n                    \"--full\" | \"-f\" => {\n                        obj.insert(\"fullPage\".to_string(), json!(true));\n                    }\n                    other if other.starts_with('-') => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\"Unknown flag: {}\", other),\n                            usage: \"diff screenshot --baseline <file> [--output <file>] [--threshold <0-1>] [--selector <sel>] [--full/-f]\",\n                        });\n                    }\n                    other => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\"Unexpected argument: {}\", other),\n                            usage: \"diff screenshot --baseline <file> [--output <file>] [--threshold <0-1>] [--selector <sel>] [--full/-f]\",\n                        });\n                    }\n                }\n                i += 1;\n            }\n            if !obj.contains_key(\"baseline\") {\n                return Err(ParseError::MissingArguments {\n                    context: \"diff screenshot\".to_string(),\n                    usage: \"diff screenshot --baseline <file>\",\n                });\n            }\n            Ok(cmd)\n        }\n        Some(\"url\") => {\n            let url1 = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"diff url\".to_string(),\n                usage: \"diff url <url1> <url2>\",\n            })?;\n            let url2 = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"diff url\".to_string(),\n                usage: \"diff url <url1> <url2>\",\n            })?;\n            let mut cmd = json!({\n                \"id\": id,\n                \"action\": \"diff_url\",\n                \"url1\": url1,\n                \"url2\": url2,\n            });\n            let obj = cmd.as_object_mut().unwrap();\n            let mut i = 3;\n            while i < rest.len() {\n                match rest[i] {\n                    \"--screenshot\" => {\n                        obj.insert(\"screenshot\".to_string(), json!(true));\n                    }\n                    \"--full\" | \"-f\" => {\n                        obj.insert(\"fullPage\".to_string(), json!(true));\n                    }\n                    \"--wait-until\" => {\n                        if let Some(val) = rest.get(i + 1) {\n                            obj.insert(\"waitUntil\".to_string(), json!(val));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff url --wait-until\".to_string(),\n                                usage: \"diff url <url1> <url2> --wait-until <load|domcontentloaded|networkidle>\",\n                            });\n                        }\n                    }\n                    \"-s\" | \"--selector\" => {\n                        if let Some(s) = rest.get(i + 1) {\n                            obj.insert(\"selector\".to_string(), json!(s));\n                            i += 1;\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff url --selector\".to_string(),\n                                usage: \"diff url <url1> <url2> --selector <sel>\",\n                            });\n                        }\n                    }\n                    \"-c\" | \"--compact\" => {\n                        obj.insert(\"compact\".to_string(), json!(true));\n                    }\n                    \"-d\" | \"--depth\" => {\n                        if let Some(d) = rest.get(i + 1) {\n                            match d.parse::<u32>() {\n                                Ok(n) => {\n                                    obj.insert(\"maxDepth\".to_string(), json!(n));\n                                    i += 1;\n                                }\n                                Err(_) => {\n                                    return Err(ParseError::InvalidValue {\n                                        message: format!(\n                                            \"Depth must be a non-negative integer, got: {}\",\n                                            d\n                                        ),\n                                        usage: \"diff url <url1> <url2> --depth <n>\",\n                                    });\n                                }\n                            }\n                        } else {\n                            return Err(ParseError::MissingArguments {\n                                context: \"diff url --depth\".to_string(),\n                                usage: \"diff url <url1> <url2> --depth <n>\",\n                            });\n                        }\n                    }\n                    other if other.starts_with('-') => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\"Unknown flag: {}\", other),\n                            usage: \"diff url <url1> <url2> [--screenshot] [--full/-f] [--wait-until <strategy>] [--selector <sel>] [--compact] [--depth <n>]\",\n                        });\n                    }\n                    other => {\n                        return Err(ParseError::InvalidValue {\n                            message: format!(\"Unexpected argument: {}\", other),\n                            usage: \"diff url <url1> <url2> [--screenshot] [--full/-f] [--wait-until <strategy>] [--selector <sel>] [--compact] [--depth <n>]\",\n                        });\n                    }\n                }\n                i += 1;\n            }\n            Ok(cmd)\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"diff\".to_string(),\n            usage: \"diff <snapshot|screenshot|url>\",\n        }),\n    }\n}\n\nfn parse_get(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\n        \"text\", \"html\", \"value\", \"attr\", \"url\", \"title\", \"count\", \"box\", \"styles\", \"cdp-url\",\n    ];\n\n    match rest.first().copied() {\n        Some(\"text\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get text\".to_string(),\n                usage: \"get text <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"gettext\", \"selector\": sel }))\n        }\n        Some(\"html\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get html\".to_string(),\n                usage: \"get html <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"innerhtml\", \"selector\": sel }))\n        }\n        Some(\"value\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get value\".to_string(),\n                usage: \"get value <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"inputvalue\", \"selector\": sel }))\n        }\n        Some(\"attr\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get attr\".to_string(),\n                usage: \"get attr <selector> <attribute>\",\n            })?;\n            let attr = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get attr\".to_string(),\n                usage: \"get attr <selector> <attribute>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"getattribute\", \"selector\": sel, \"attribute\": attr }))\n        }\n        Some(\"url\") => Ok(json!({ \"id\": id, \"action\": \"url\" })),\n        Some(\"cdp-url\") => Ok(json!({ \"id\": id, \"action\": \"cdp_url\" })),\n        Some(\"title\") => Ok(json!({ \"id\": id, \"action\": \"title\" })),\n        Some(\"count\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get count\".to_string(),\n                usage: \"get count <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"count\", \"selector\": sel }))\n        }\n        Some(\"box\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get box\".to_string(),\n                usage: \"get box <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"boundingbox\", \"selector\": sel }))\n        }\n        Some(\"styles\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"get styles\".to_string(),\n                usage: \"get styles <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"styles\", \"selector\": sel }))\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"get\".to_string(),\n            usage: \"get <text|html|value|attr|url|title|count|box|styles|cdp-url> [args...]\",\n        }),\n    }\n}\n\nfn parse_is(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\"visible\", \"enabled\", \"checked\"];\n\n    match rest.first().copied() {\n        Some(\"visible\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"is visible\".to_string(),\n                usage: \"is visible <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"isvisible\", \"selector\": sel }))\n        }\n        Some(\"enabled\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"is enabled\".to_string(),\n                usage: \"is enabled <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"isenabled\", \"selector\": sel }))\n        }\n        Some(\"checked\") => {\n            let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"is checked\".to_string(),\n                usage: \"is checked <selector>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"ischecked\", \"selector\": sel }))\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"is\".to_string(),\n            usage: \"is <visible|enabled|checked> <selector>\",\n        }),\n    }\n}\n\nfn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\n        \"role\",\n        \"text\",\n        \"label\",\n        \"placeholder\",\n        \"alt\",\n        \"title\",\n        \"testid\",\n        \"first\",\n        \"last\",\n        \"nth\",\n    ];\n\n    let locator = rest.first().ok_or_else(|| ParseError::MissingArguments {\n        context: \"find\".to_string(),\n        usage: \"find <locator> <value> [action] [text]\",\n    })?;\n\n    let name_idx = rest.iter().position(|&s| s == \"--name\");\n    let name = name_idx.and_then(|i| rest.get(i + 1).copied());\n    let exact = rest.contains(&\"--exact\");\n\n    match *locator {\n        \"role\" | \"text\" | \"label\" | \"placeholder\" | \"alt\" | \"title\" | \"testid\" | \"first\"\n        | \"last\" => {\n            let value = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: format!(\"find {}\", locator),\n                usage: match *locator {\n                    \"role\" => \"find role <role> [action] [--name <name>] [--exact]\",\n                    \"text\" => \"find text <text> [action] [--exact]\",\n                    \"label\" => \"find label <label> [action] [text] [--exact]\",\n                    \"placeholder\" => \"find placeholder <text> [action] [text] [--exact]\",\n                    \"alt\" => \"find alt <text> [action] [--exact]\",\n                    \"title\" => \"find title <text> [action] [--exact]\",\n                    \"testid\" => \"find testid <id> [action] [text]\",\n                    \"first\" => \"find first <selector> [action] [text]\",\n                    \"last\" => \"find last <selector> [action] [text]\",\n                    _ => \"find <locator> <value> [action] [text]\",\n                },\n            })?;\n            let subaction = rest.get(2).unwrap_or(&\"click\");\n            let fill_value = if rest.len() > 3 {\n                Some(rest[3..].join(\" \"))\n            } else {\n                None\n            };\n\n            match *locator {\n                \"role\" => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"getbyrole\", \"role\": value, \"subaction\": subaction, \"name\": name, \"exact\": exact });\n                    if let Some(v) = fill_value {\n                        cmd[\"value\"] = json!(v);\n                    }\n                    Ok(cmd)\n                }\n                \"text\" => Ok(\n                    json!({ \"id\": id, \"action\": \"getbytext\", \"text\": value, \"subaction\": subaction, \"exact\": exact }),\n                ),\n                \"label\" => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"getbylabel\", \"label\": value, \"subaction\": subaction, \"exact\": exact });\n                    if let Some(v) = fill_value {\n                        cmd[\"value\"] = json!(v);\n                    }\n                    Ok(cmd)\n                }\n                \"placeholder\" => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"getbyplaceholder\", \"placeholder\": value, \"subaction\": subaction, \"exact\": exact });\n                    if let Some(v) = fill_value {\n                        cmd[\"value\"] = json!(v);\n                    }\n                    Ok(cmd)\n                }\n                \"alt\" => Ok(\n                    json!({ \"id\": id, \"action\": \"getbyalttext\", \"text\": value, \"subaction\": subaction, \"exact\": exact }),\n                ),\n                \"title\" => Ok(\n                    json!({ \"id\": id, \"action\": \"getbytitle\", \"text\": value, \"subaction\": subaction, \"exact\": exact }),\n                ),\n                \"testid\" => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"getbytestid\", \"testId\": value, \"subaction\": subaction });\n                    if let Some(v) = fill_value {\n                        cmd[\"value\"] = json!(v);\n                    }\n                    Ok(cmd)\n                }\n                \"first\" => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"nth\", \"selector\": value, \"index\": 0, \"subaction\": subaction });\n                    if let Some(v) = fill_value {\n                        cmd[\"value\"] = json!(v);\n                    }\n                    Ok(cmd)\n                }\n                \"last\" => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"nth\", \"selector\": value, \"index\": -1, \"subaction\": subaction });\n                    if let Some(v) = fill_value {\n                        cmd[\"value\"] = json!(v);\n                    }\n                    Ok(cmd)\n                }\n                _ => unreachable!(),\n            }\n        }\n        \"nth\" => {\n            let idx_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"find nth\".to_string(),\n                usage: \"find nth <index> <selector> [action] [text]\",\n            })?;\n            let idx = idx_str\n                .parse::<i32>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"find nth\".to_string(),\n                    usage: \"find nth <index> <selector> [action] [text]\",\n                })?;\n            let sel = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"find nth\".to_string(),\n                usage: \"find nth <index> <selector> [action] [text]\",\n            })?;\n            let sub = rest.get(3).unwrap_or(&\"click\");\n            let fv = if rest.len() > 4 {\n                Some(rest[4..].join(\" \"))\n            } else {\n                None\n            };\n            let mut cmd = json!({ \"id\": id, \"action\": \"nth\", \"selector\": sel, \"index\": idx, \"subaction\": sub });\n            if let Some(v) = fv {\n                cmd[\"value\"] = json!(v);\n            }\n            Ok(cmd)\n        }\n        _ => Err(ParseError::UnknownSubcommand {\n            subcommand: locator.to_string(),\n            valid_options: VALID,\n        }),\n    }\n}\n\nfn parse_mouse(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\"move\", \"down\", \"up\", \"wheel\"];\n\n    match rest.first().copied() {\n        Some(\"move\") => {\n            let x_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"mouse move\".to_string(),\n                usage: \"mouse move <x> <y>\",\n            })?;\n            let y_str = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"mouse move\".to_string(),\n                usage: \"mouse move <x> <y>\",\n            })?;\n            let x = x_str\n                .parse::<i32>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"mouse move\".to_string(),\n                    usage: \"mouse move <x> <y>\",\n                })?;\n            let y = y_str\n                .parse::<i32>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"mouse move\".to_string(),\n                    usage: \"mouse move <x> <y>\",\n                })?;\n            Ok(json!({ \"id\": id, \"action\": \"mousemove\", \"x\": x, \"y\": y }))\n        }\n        Some(\"down\") => {\n            Ok(json!({ \"id\": id, \"action\": \"mousedown\", \"button\": rest.get(1).unwrap_or(&\"left\") }))\n        }\n        Some(\"up\") => {\n            Ok(json!({ \"id\": id, \"action\": \"mouseup\", \"button\": rest.get(1).unwrap_or(&\"left\") }))\n        }\n        Some(\"wheel\") => {\n            let dy = rest\n                .get(1)\n                .and_then(|s| s.parse::<i32>().ok())\n                .unwrap_or(100);\n            let dx = rest.get(2).and_then(|s| s.parse::<i32>().ok()).unwrap_or(0);\n            Ok(json!({ \"id\": id, \"action\": \"wheel\", \"deltaX\": dx, \"deltaY\": dy }))\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"mouse\".to_string(),\n            usage: \"mouse <move|down|up|wheel> [args...]\",\n        }),\n    }\n}\n\nfn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\n        \"viewport\",\n        \"device\",\n        \"geo\",\n        \"geolocation\",\n        \"offline\",\n        \"headers\",\n        \"credentials\",\n        \"auth\",\n        \"media\",\n    ];\n\n    match rest.first().copied() {\n        Some(\"viewport\") => {\n            let w_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set viewport\".to_string(),\n                usage: \"set viewport <width> <height> [scale]\",\n            })?;\n            let h_str = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set viewport\".to_string(),\n                usage: \"set viewport <width> <height> [scale]\",\n            })?;\n            let w = w_str\n                .parse::<i32>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"set viewport\".to_string(),\n                    usage: \"set viewport <width> <height> [scale]\",\n                })?;\n            let h = h_str\n                .parse::<i32>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"set viewport\".to_string(),\n                    usage: \"set viewport <width> <height> [scale]\",\n                })?;\n            let mut cmd = json!({ \"id\": id, \"action\": \"viewport\", \"width\": w, \"height\": h });\n            if let Some(scale_str) = rest.get(3) {\n                let scale = scale_str\n                    .parse::<f64>()\n                    .map_err(|_| ParseError::MissingArguments {\n                        context: \"set viewport\".to_string(),\n                        usage: \"set viewport <width> <height> [scale]\",\n                    })?;\n                cmd[\"deviceScaleFactor\"] = json!(scale);\n            }\n            Ok(cmd)\n        }\n        Some(\"device\") => {\n            let dev = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set device\".to_string(),\n                usage: \"set device <name>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"device\", \"device\": dev }))\n        }\n        Some(\"geo\") | Some(\"geolocation\") => {\n            let lat_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set geo\".to_string(),\n                usage: \"set geo <latitude> <longitude>\",\n            })?;\n            let lng_str = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set geo\".to_string(),\n                usage: \"set geo <latitude> <longitude>\",\n            })?;\n            let lat = lat_str\n                .parse::<f64>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"set geo\".to_string(),\n                    usage: \"set geo <latitude> <longitude>\",\n                })?;\n            let lng = lng_str\n                .parse::<f64>()\n                .map_err(|_| ParseError::MissingArguments {\n                    context: \"set geo\".to_string(),\n                    usage: \"set geo <latitude> <longitude>\",\n                })?;\n            Ok(json!({ \"id\": id, \"action\": \"geolocation\", \"latitude\": lat, \"longitude\": lng }))\n        }\n        Some(\"offline\") => {\n            let off = rest\n                .get(1)\n                .map(|s| *s != \"off\" && *s != \"false\")\n                .unwrap_or(true);\n            Ok(json!({ \"id\": id, \"action\": \"offline\", \"offline\": off }))\n        }\n        Some(\"headers\") => {\n            let headers_json = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set headers\".to_string(),\n                usage: \"set headers <json>\",\n            })?;\n            // Parse the JSON string into an object\n            let headers: serde_json::Value =\n                serde_json::from_str(headers_json).map_err(|_| ParseError::MissingArguments {\n                    context: \"set headers\".to_string(),\n                    usage: \"set headers <json> (must be valid JSON object)\",\n                })?;\n            Ok(json!({ \"id\": id, \"action\": \"headers\", \"headers\": headers }))\n        }\n        Some(\"credentials\") | Some(\"auth\") => {\n            let user = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set credentials\".to_string(),\n                usage: \"set credentials <username> <password>\",\n            })?;\n            let pass = rest.get(2).ok_or_else(|| ParseError::MissingArguments {\n                context: \"set credentials\".to_string(),\n                usage: \"set credentials <username> <password>\",\n            })?;\n            Ok(json!({ \"id\": id, \"action\": \"credentials\", \"username\": user, \"password\": pass }))\n        }\n        Some(\"media\") => {\n            let color = if rest.contains(&\"dark\") {\n                \"dark\"\n            } else if rest.contains(&\"light\") {\n                \"light\"\n            } else {\n                \"no-preference\"\n            };\n            let reduced = if rest.contains(&\"reduced-motion\") {\n                \"reduce\"\n            } else {\n                \"no-preference\"\n            };\n            Ok(\n                json!({ \"id\": id, \"action\": \"emulatemedia\", \"colorScheme\": color, \"reducedMotion\": reduced }),\n            )\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"set\".to_string(),\n            usage: \"set <viewport|device|geo|offline|headers|credentials|media> [args...]\",\n        }),\n    }\n}\n\n/// Parse network interception, request inspection, and HAR recording commands.\nfn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\"route\", \"unroute\", \"requests\", \"har\"];\n\n    match rest.first().copied() {\n        Some(\"route\") => {\n            let url = rest.get(1).ok_or_else(|| ParseError::MissingArguments {\n                context: \"network route\".to_string(),\n                usage: \"network route <url> [--abort|--body <json>]\",\n            })?;\n            let abort = rest.contains(&\"--abort\");\n            let body_idx = rest.iter().position(|&s| s == \"--body\");\n            let body = body_idx.and_then(|i| rest.get(i + 1).copied());\n            Ok(json!({ \"id\": id, \"action\": \"route\", \"url\": url, \"abort\": abort, \"body\": body }))\n        }\n        Some(\"unroute\") => {\n            let mut cmd = json!({ \"id\": id, \"action\": \"unroute\" });\n            if let Some(url) = rest.get(1) {\n                cmd[\"url\"] = json!(url);\n            }\n            Ok(cmd)\n        }\n        Some(\"requests\") => {\n            let clear = rest.contains(&\"--clear\");\n            let filter_idx = rest.iter().position(|&s| s == \"--filter\");\n            let filter = filter_idx.and_then(|i| rest.get(i + 1).copied());\n            let mut cmd = json!({ \"id\": id, \"action\": \"requests\", \"clear\": clear });\n            if let Some(f) = filter {\n                cmd[\"filter\"] = json!(f);\n            }\n            Ok(cmd)\n        }\n        Some(\"har\") => {\n            const HAR_VALID: &[&str] = &[\"start\", \"stop\"];\n            match rest.get(1).copied() {\n                Some(\"start\") => Ok(json!({ \"id\": id, \"action\": \"har_start\" })),\n                Some(\"stop\") => {\n                    let mut cmd = json!({ \"id\": id, \"action\": \"har_stop\" });\n                    if let Some(path) = rest.get(2) {\n                        cmd[\"path\"] = json!(path);\n                    }\n                    Ok(cmd)\n                }\n                Some(sub) => Err(ParseError::UnknownSubcommand {\n                    subcommand: sub.to_string(),\n                    valid_options: HAR_VALID,\n                }),\n                None => Err(ParseError::MissingArguments {\n                    context: \"network har\".to_string(),\n                    usage: \"network har <start|stop> [path]\",\n                }),\n            }\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"network\".to_string(),\n            usage: \"network <route|unroute|requests|har> [args...]\",\n        }),\n    }\n}\n\nfn parse_storage(rest: &[&str], id: &str) -> Result<Value, ParseError> {\n    const VALID: &[&str] = &[\"local\", \"session\"];\n\n    match rest.first().copied() {\n        Some(\"local\") | Some(\"session\") => {\n            let storage_type = rest.first().unwrap();\n            let (op, key, value) = match rest.get(1) {\n                Some(&\"get\") => (\"get\", rest.get(2), rest.get(3)),\n                Some(&\"set\") => (\"set\", rest.get(2), rest.get(3)),\n                Some(&\"clear\") => (\"clear\", rest.get(2), rest.get(3)),\n                Some(_) => (\"get\", rest.get(1), rest.get(2)),\n                None => (\"get\", None, None),\n            };\n            match op {\n                \"set\" => {\n                    let k = key.ok_or_else(|| ParseError::MissingArguments {\n                        context: format!(\"storage {} set\", storage_type),\n                        usage: \"storage <local|session> set <key> <value>\",\n                    })?;\n                    let v = value.ok_or_else(|| ParseError::MissingArguments {\n                        context: format!(\"storage {} set\", storage_type),\n                        usage: \"storage <local|session> set <key> <value>\",\n                    })?;\n                    Ok(\n                        json!({ \"id\": id, \"action\": \"storage_set\", \"type\": storage_type, \"key\": k, \"value\": v }),\n                    )\n                }\n                \"clear\" => Ok(json!({ \"id\": id, \"action\": \"storage_clear\", \"type\": storage_type })),\n                _ => {\n                    let mut cmd =\n                        json!({ \"id\": id, \"action\": \"storage_get\", \"type\": storage_type });\n                    if let Some(k) = key {\n                        cmd.as_object_mut()\n                            .unwrap()\n                            .insert(\"key\".to_string(), json!(k));\n                    }\n                    Ok(cmd)\n                }\n            }\n        }\n        Some(sub) => Err(ParseError::UnknownSubcommand {\n            subcommand: sub.to_string(),\n            valid_options: VALID,\n        }),\n        None => Err(ParseError::MissingArguments {\n            context: \"storage\".to_string(),\n            usage: \"storage <local|session> [get|set|clear] [key] [value]\",\n        }),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn default_flags() -> Flags {\n        Flags {\n            session: \"test\".to_string(),\n            json: false,\n            headed: false,\n            debug: false,\n            headers: None,\n            executable_path: None,\n            extensions: Vec::new(),\n            cdp: None,\n            profile: None,\n            state: None,\n            proxy: None,\n            proxy_bypass: None,\n            args: None,\n            user_agent: None,\n            provider: None,\n            ignore_https_errors: false,\n            allow_file_access: false,\n            device: None,\n            auto_connect: false,\n            session_name: None,\n            cli_executable_path: false,\n            cli_extensions: false,\n            cli_profile: false,\n            cli_state: false,\n            cli_args: false,\n            cli_user_agent: false,\n            cli_proxy: false,\n            cli_proxy_bypass: false,\n            cli_allow_file_access: false,\n            cli_annotate: false,\n            cli_download_path: false,\n            cli_headed: false,\n            annotate: false,\n            color_scheme: None,\n            download_path: None,\n            content_boundaries: false,\n            max_output: None,\n            allowed_domains: None,\n            action_policy: None,\n            confirm_actions: None,\n            confirm_interactive: false,\n            engine: None,\n            screenshot_dir: None,\n            screenshot_quality: None,\n            screenshot_format: None,\n            idle_timeout: None,\n        }\n    }\n\n    fn args(s: &str) -> Vec<String> {\n        s.split_whitespace().map(String::from).collect()\n    }\n\n    // === Cookies Tests ===\n\n    #[test]\n    fn test_cookies_get() {\n        let cmd = parse_command(&args(\"cookies\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_get\");\n    }\n\n    #[test]\n    fn test_cookies_get_explicit() {\n        let cmd = parse_command(&args(\"cookies get\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_get\");\n    }\n\n    #[test]\n    fn test_cookies_set() {\n        let cmd = parse_command(&args(\"cookies set mycookie myvalue\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n    }\n\n    #[test]\n    fn test_cookies_set_missing_value() {\n        let result = parse_command(&args(\"cookies set mycookie\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_cookies_clear() {\n        let cmd = parse_command(&args(\"cookies clear\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_clear\");\n    }\n\n    #[test]\n    fn test_cookies_set_with_url() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --url https://example.com\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_cookies_set_with_domain() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --domain example.com\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"domain\"], \"example.com\");\n    }\n\n    #[test]\n    fn test_cookies_set_with_path() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --path /api\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"path\"], \"/api\");\n    }\n\n    #[test]\n    fn test_cookies_set_with_httponly() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --httpOnly\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"httpOnly\"], true);\n    }\n\n    #[test]\n    fn test_cookies_set_with_secure() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --secure\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"secure\"], true);\n    }\n\n    #[test]\n    fn test_cookies_set_with_samesite() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --sameSite Strict\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"sameSite\"], \"Strict\");\n    }\n\n    #[test]\n    fn test_cookies_set_with_expires() {\n        let cmd = parse_command(\n            &args(\"cookies set mycookie myvalue --expires 1234567890\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"expires\"], 1234567890);\n    }\n\n    #[test]\n    fn test_cookies_set_with_multiple_flags() {\n        let cmd = parse_command(&args(\"cookies set mycookie myvalue --url https://example.com --httpOnly --secure --sameSite Lax\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"url\"], \"https://example.com\");\n        assert_eq!(cmd[\"cookies\"][0][\"httpOnly\"], true);\n        assert_eq!(cmd[\"cookies\"][0][\"secure\"], true);\n        assert_eq!(cmd[\"cookies\"][0][\"sameSite\"], \"Lax\");\n    }\n\n    #[test]\n    fn test_cookies_set_with_all_flags() {\n        let cmd = parse_command(&args(\"cookies set mycookie myvalue --url https://example.com --domain example.com --path /api --httpOnly --secure --sameSite None --expires 9999999999\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cookies_set\");\n        assert_eq!(cmd[\"cookies\"][0][\"name\"], \"mycookie\");\n        assert_eq!(cmd[\"cookies\"][0][\"value\"], \"myvalue\");\n        assert_eq!(cmd[\"cookies\"][0][\"url\"], \"https://example.com\");\n        assert_eq!(cmd[\"cookies\"][0][\"domain\"], \"example.com\");\n        assert_eq!(cmd[\"cookies\"][0][\"path\"], \"/api\");\n        assert_eq!(cmd[\"cookies\"][0][\"httpOnly\"], true);\n        assert_eq!(cmd[\"cookies\"][0][\"secure\"], true);\n        assert_eq!(cmd[\"cookies\"][0][\"sameSite\"], \"None\");\n        assert_eq!(cmd[\"cookies\"][0][\"expires\"], 9999999999i64);\n    }\n\n    #[test]\n    fn test_cookies_set_invalid_samesite() {\n        let result = parse_command(\n            &args(\"cookies set mycookie myvalue --sameSite Invalid\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n    }\n\n    // === Storage Tests ===\n\n    #[test]\n    fn test_storage_local_get() {\n        let cmd = parse_command(&args(\"storage local\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_get\");\n        assert_eq!(cmd[\"type\"], \"local\");\n        assert!(cmd.get(\"key\").is_none());\n    }\n\n    #[test]\n    fn test_storage_local_get_key() {\n        let cmd = parse_command(&args(\"storage local get mykey\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_get\");\n        assert_eq!(cmd[\"type\"], \"local\");\n        assert_eq!(cmd[\"key\"], \"mykey\");\n    }\n\n    #[test]\n    fn test_storage_local_get_implicit_key() {\n        let cmd = parse_command(&args(\"storage local mykey\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_get\");\n        assert_eq!(cmd[\"type\"], \"local\");\n        assert_eq!(cmd[\"key\"], \"mykey\");\n    }\n\n    #[test]\n    fn test_storage_session_get() {\n        let cmd = parse_command(&args(\"storage session\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_get\");\n        assert_eq!(cmd[\"type\"], \"session\");\n    }\n\n    #[test]\n    fn test_storage_session_get_implicit_key() {\n        let cmd = parse_command(&args(\"storage session mykey\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_get\");\n        assert_eq!(cmd[\"type\"], \"session\");\n        assert_eq!(cmd[\"key\"], \"mykey\");\n    }\n\n    #[test]\n    fn test_storage_local_set() {\n        let cmd =\n            parse_command(&args(\"storage local set mykey myvalue\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_set\");\n        assert_eq!(cmd[\"type\"], \"local\");\n        assert_eq!(cmd[\"key\"], \"mykey\");\n        assert_eq!(cmd[\"value\"], \"myvalue\");\n    }\n\n    #[test]\n    fn test_storage_session_set() {\n        let cmd =\n            parse_command(&args(\"storage session set skey svalue\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_set\");\n        assert_eq!(cmd[\"type\"], \"session\");\n        assert_eq!(cmd[\"key\"], \"skey\");\n        assert_eq!(cmd[\"value\"], \"svalue\");\n    }\n\n    #[test]\n    fn test_storage_set_missing_value() {\n        let result = parse_command(&args(\"storage local set mykey\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_storage_local_clear() {\n        let cmd = parse_command(&args(\"storage local clear\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_clear\");\n        assert_eq!(cmd[\"type\"], \"local\");\n    }\n\n    #[test]\n    fn test_storage_session_clear() {\n        let cmd = parse_command(&args(\"storage session clear\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"storage_clear\");\n        assert_eq!(cmd[\"type\"], \"session\");\n    }\n\n    #[test]\n    fn test_storage_invalid_type() {\n        let result = parse_command(&args(\"storage invalid\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    // === Navigation Tests ===\n\n    #[test]\n    fn test_navigate_with_https() {\n        let cmd = parse_command(&args(\"open https://example.com\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"navigate\");\n        assert_eq!(cmd[\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_navigate_without_protocol() {\n        let cmd = parse_command(&args(\"open example.com\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"navigate\");\n        assert_eq!(cmd[\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_navigate_with_headers() {\n        let mut flags = default_flags();\n        flags.headers = Some(r#\"{\"Authorization\": \"Bearer token\"}\"#.to_string());\n        let cmd = parse_command(&args(\"open api.example.com\"), &flags).unwrap();\n        assert_eq!(cmd[\"action\"], \"navigate\");\n        assert_eq!(cmd[\"url\"], \"https://api.example.com\");\n        assert_eq!(cmd[\"headers\"][\"Authorization\"], \"Bearer token\");\n    }\n\n    #[test]\n    fn test_navigate_with_multiple_headers() {\n        let mut flags = default_flags();\n        flags.headers =\n            Some(r#\"{\"Authorization\": \"Bearer token\", \"X-Custom\": \"value\"}\"#.to_string());\n        let cmd = parse_command(&args(\"open api.example.com\"), &flags).unwrap();\n        assert_eq!(cmd[\"headers\"][\"Authorization\"], \"Bearer token\");\n        assert_eq!(cmd[\"headers\"][\"X-Custom\"], \"value\");\n    }\n\n    #[test]\n    fn test_navigate_without_headers_flag() {\n        let cmd = parse_command(&args(\"open example.com\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"navigate\");\n        // headers should not be present when flag is not set\n        assert!(cmd.get(\"headers\").is_none());\n    }\n\n    #[test]\n    fn test_navigate_with_invalid_headers_json() {\n        let mut flags = default_flags();\n        flags.headers = Some(\"not valid json\".to_string());\n        let result = parse_command(&args(\"open api.example.com\"), &flags);\n        // Invalid JSON should return a ParseError, not silently drop headers\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        let msg = err.format();\n        assert!(msg.contains(\"Invalid JSON for --headers\"));\n    }\n\n    #[test]\n    fn test_navigate_chrome_extension_url() {\n        let cmd = parse_command(\n            &args(\"open chrome-extension://abcdefghijklmnop/popup.html\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"navigate\");\n        assert_eq!(cmd[\"url\"], \"chrome-extension://abcdefghijklmnop/popup.html\");\n    }\n\n    #[test]\n    fn test_navigate_chrome_url() {\n        let cmd = parse_command(&args(\"open chrome://extensions\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"navigate\");\n        assert_eq!(cmd[\"url\"], \"chrome://extensions\");\n    }\n\n    // === Set Headers Tests ===\n\n    #[test]\n    fn test_set_headers_parses_json() {\n        let input: Vec<String> = vec![\n            \"set\".to_string(),\n            \"headers\".to_string(),\n            r#\"{\"Authorization\":\"Bearer token\"}\"#.to_string(),\n        ];\n        let cmd = parse_command(&input, &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"headers\");\n        // Headers should be an object, not a string\n        assert!(cmd[\"headers\"].is_object());\n        assert_eq!(cmd[\"headers\"][\"Authorization\"], \"Bearer token\");\n    }\n\n    #[test]\n    fn test_set_headers_with_multiple_values() {\n        let input: Vec<String> = vec![\n            \"set\".to_string(),\n            \"headers\".to_string(),\n            r#\"{\"Authorization\": \"Bearer token\", \"X-Custom\": \"value\"}\"#.to_string(),\n        ];\n        let cmd = parse_command(&input, &default_flags()).unwrap();\n        assert_eq!(cmd[\"headers\"][\"Authorization\"], \"Bearer token\");\n        assert_eq!(cmd[\"headers\"][\"X-Custom\"], \"value\");\n    }\n\n    #[test]\n    fn test_set_headers_invalid_json_error() {\n        let input: Vec<String> = vec![\n            \"set\".to_string(),\n            \"headers\".to_string(),\n            \"not-valid-json\".to_string(),\n        ];\n        let result = parse_command(&input, &default_flags());\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_back() {\n        let cmd = parse_command(&args(\"back\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"back\");\n    }\n\n    #[test]\n    fn test_forward() {\n        let cmd = parse_command(&args(\"forward\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"forward\");\n    }\n\n    #[test]\n    fn test_reload() {\n        let cmd = parse_command(&args(\"reload\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"reload\");\n    }\n\n    // === Core Actions ===\n\n    #[test]\n    fn test_click() {\n        let cmd = parse_command(&args(\"click #button\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"click\");\n        assert_eq!(cmd[\"selector\"], \"#button\");\n    }\n\n    #[test]\n    fn test_fill() {\n        let cmd = parse_command(&args(\"fill #input hello world\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"fill\");\n        assert_eq!(cmd[\"selector\"], \"#input\");\n        assert_eq!(cmd[\"value\"], \"hello world\");\n    }\n\n    #[test]\n    fn test_type_command() {\n        let cmd = parse_command(&args(\"type #input some text\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"type\");\n        assert_eq!(cmd[\"selector\"], \"#input\");\n        assert_eq!(cmd[\"text\"], \"some text\");\n    }\n\n    #[test]\n    fn test_select() {\n        let cmd = parse_command(&args(\"select #menu option1\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"select\");\n        assert_eq!(cmd[\"selector\"], \"#menu\");\n        assert_eq!(cmd[\"values\"], \"option1\");\n    }\n\n    #[test]\n    fn test_select_multiple_values() {\n        let cmd = parse_command(&args(\"select #menu opt1 opt2 opt3\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"select\");\n        assert_eq!(cmd[\"selector\"], \"#menu\");\n        assert_eq!(cmd[\"values\"], json!([\"opt1\", \"opt2\", \"opt3\"]));\n    }\n\n    #[test]\n    fn test_frame_main() {\n        let cmd = parse_command(&args(\"frame main\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"mainframe\");\n    }\n\n    // === Tabs ===\n\n    #[test]\n    fn test_tab_new() {\n        let cmd = parse_command(&args(\"tab new\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"tab_new\");\n        assert!(\n            cmd.get(\"url\").is_none(),\n            \"url should not be present when not provided\"\n        );\n    }\n\n    #[test]\n    fn test_tab_new_with_url() {\n        let cmd = parse_command(&args(\"tab new https://example.com\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"tab_new\");\n        assert_eq!(cmd[\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_tab_list() {\n        let cmd = parse_command(&args(\"tab list\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"tab_list\");\n    }\n\n    #[test]\n    fn test_tab_switch() {\n        let cmd = parse_command(&args(\"tab 2\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"tab_switch\");\n        assert_eq!(cmd[\"index\"], 2);\n    }\n\n    #[test]\n    fn test_tab_close() {\n        let cmd = parse_command(&args(\"tab close\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"tab_close\");\n    }\n\n    // === Network ===\n\n    #[test]\n    fn test_network_har_start() {\n        let cmd = parse_command(&args(\"network har start\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"har_start\");\n    }\n\n    #[test]\n    fn test_network_har_stop_with_path() {\n        let cmd = parse_command(&args(\"network har stop ./capture.har\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"har_stop\");\n        assert_eq!(cmd[\"path\"], \"./capture.har\");\n    }\n\n    #[test]\n    fn test_network_har_stop_without_path() {\n        let cmd = parse_command(&args(\"network har stop\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"har_stop\");\n        assert!(cmd.get(\"path\").is_none());\n    }\n\n    #[test]\n    fn test_network_har_requires_subcommand() {\n        let result = parse_command(&args(\"network har\"), &default_flags());\n        assert!(matches!(result, Err(ParseError::MissingArguments { .. })));\n    }\n\n    // === Screenshot ===\n\n    #[test]\n    fn test_screenshot() {\n        let cmd = parse_command(&args(\"screenshot\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"path\"], serde_json::Value::Null);\n        assert_eq!(cmd[\"selector\"], serde_json::Value::Null);\n    }\n\n    #[test]\n    fn test_screenshot_path() {\n        let cmd = parse_command(&args(\"screenshot out.png\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"path\"], \"out.png\");\n    }\n\n    #[test]\n    fn test_screenshot_full_page() {\n        let cmd = parse_command(&args(\"screenshot --full\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_screenshot_full_page_shorthand() {\n        let cmd = parse_command(&args(\"screenshot -f\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_screenshot_with_ref() {\n        let cmd = parse_command(&args(\"screenshot @e1\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"selector\"], \"@e1\");\n        assert_eq!(cmd[\"path\"], serde_json::Value::Null);\n    }\n\n    #[test]\n    fn test_screenshot_with_css_class() {\n        let cmd = parse_command(&args(\"screenshot .my-button\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"selector\"], \".my-button\");\n        assert_eq!(cmd[\"path\"], serde_json::Value::Null);\n    }\n\n    #[test]\n    fn test_screenshot_with_css_id() {\n        let cmd = parse_command(&args(\"screenshot #header\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"selector\"], \"#header\");\n        assert_eq!(cmd[\"path\"], serde_json::Value::Null);\n    }\n\n    #[test]\n    fn test_screenshot_with_path() {\n        let cmd = parse_command(&args(\"screenshot ./output.png\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"selector\"], serde_json::Value::Null);\n        assert_eq!(cmd[\"path\"], \"./output.png\");\n    }\n\n    #[test]\n    fn test_screenshot_with_selector_and_path() {\n        let cmd = parse_command(&args(\"screenshot .btn ./button.png\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"screenshot\");\n        assert_eq!(cmd[\"selector\"], \".btn\");\n        assert_eq!(cmd[\"path\"], \"./button.png\");\n    }\n\n    // === Snapshot ===\n\n    #[test]\n    fn test_snapshot() {\n        let cmd = parse_command(&args(\"snapshot\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"snapshot\");\n    }\n\n    #[test]\n    fn test_snapshot_interactive() {\n        let cmd = parse_command(&args(\"snapshot -i\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"snapshot\");\n        assert_eq!(cmd[\"interactive\"], true);\n    }\n\n    #[test]\n    fn test_snapshot_cursor() {\n        let cmd = parse_command(&args(\"snapshot -C\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"snapshot\");\n        assert_eq!(cmd[\"cursor\"], true);\n    }\n\n    #[test]\n    fn test_snapshot_interactive_cursor() {\n        let cmd = parse_command(&args(\"snapshot -i -C\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"snapshot\");\n        assert_eq!(cmd[\"interactive\"], true);\n        assert_eq!(cmd[\"cursor\"], true);\n    }\n\n    #[test]\n    fn test_snapshot_compact() {\n        let cmd = parse_command(&args(\"snapshot --compact\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"snapshot\");\n        assert_eq!(cmd[\"compact\"], true);\n    }\n\n    #[test]\n    fn test_snapshot_depth() {\n        let cmd = parse_command(&args(\"snapshot -d 3\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"snapshot\");\n        assert_eq!(cmd[\"maxDepth\"], 3);\n    }\n\n    // === Wait ===\n\n    #[test]\n    fn test_wait_selector() {\n        let cmd = parse_command(&args(\"wait #element\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"wait\");\n        assert_eq!(cmd[\"selector\"], \"#element\");\n    }\n\n    #[test]\n    fn test_wait_timeout() {\n        let cmd = parse_command(&args(\"wait 5000\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"wait\");\n        assert_eq!(cmd[\"timeout\"], 5000);\n    }\n\n    #[test]\n    fn test_wait_url() {\n        let cmd = parse_command(&args(\"wait --url **/dashboard\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitforurl\");\n        assert_eq!(cmd[\"url\"], \"**/dashboard\");\n    }\n\n    #[test]\n    fn test_wait_load() {\n        let cmd = parse_command(&args(\"wait --load networkidle\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitforloadstate\");\n        assert_eq!(cmd[\"state\"], \"networkidle\");\n    }\n\n    #[test]\n    fn test_wait_load_missing_state() {\n        let result = parse_command(&args(\"wait --load\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_wait_fn() {\n        let cmd = parse_command(&args(\"wait --fn window.ready\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitforfunction\");\n        assert_eq!(cmd[\"expression\"], \"window.ready\");\n    }\n\n    #[test]\n    fn test_wait_text() {\n        let cmd = parse_command(&args(\"wait --text Welcome\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"wait\");\n        assert_eq!(cmd[\"text\"], \"Welcome\");\n        assert!(cmd.get(\"timeout\").is_none());\n    }\n\n    #[test]\n    fn test_wait_text_with_timeout() {\n        let cmd = parse_command(\n            &args(\"wait --text Welcome --timeout 5000\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"wait\");\n        assert_eq!(cmd[\"text\"], \"Welcome\");\n        assert_eq!(cmd[\"timeout\"], 5000);\n    }\n\n    // === Clipboard Tests ===\n\n    #[test]\n    fn test_clipboard_read_default() {\n        let cmd = parse_command(&args(\"clipboard\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"clipboard\");\n        assert_eq!(cmd[\"operation\"], \"read\");\n    }\n\n    #[test]\n    fn test_clipboard_read_explicit() {\n        let cmd = parse_command(&args(\"clipboard read\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"clipboard\");\n        assert_eq!(cmd[\"operation\"], \"read\");\n    }\n\n    #[test]\n    fn test_clipboard_write() {\n        let cmd = parse_command(&args(\"clipboard write hello\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"clipboard\");\n        assert_eq!(cmd[\"operation\"], \"write\");\n        assert_eq!(cmd[\"text\"], \"hello\");\n    }\n\n    #[test]\n    fn test_clipboard_write_multi_word() {\n        let cmd = parse_command(&args(\"clipboard write hello world\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"clipboard\");\n        assert_eq!(cmd[\"operation\"], \"write\");\n        assert_eq!(cmd[\"text\"], \"hello world\");\n    }\n\n    #[test]\n    fn test_clipboard_copy() {\n        let cmd = parse_command(&args(\"clipboard copy\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"clipboard\");\n        assert_eq!(cmd[\"operation\"], \"copy\");\n    }\n\n    #[test]\n    fn test_clipboard_paste() {\n        let cmd = parse_command(&args(\"clipboard paste\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"clipboard\");\n        assert_eq!(cmd[\"operation\"], \"paste\");\n    }\n\n    #[test]\n    fn test_clipboard_write_missing_text() {\n        let result = parse_command(&args(\"clipboard write\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_clipboard_unknown_subcommand() {\n        let result = parse_command(&args(\"clipboard clear\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    // === Unknown command ===\n\n    // === Record Tests ===\n\n    #[test]\n    fn test_record_start() {\n        let cmd = parse_command(&args(\"record start output.webm\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_start\");\n        assert_eq!(cmd[\"path\"], \"output.webm\");\n        assert!(cmd.get(\"url\").is_none());\n    }\n\n    #[test]\n    fn test_record_start_with_url() {\n        let cmd = parse_command(\n            &args(\"record start demo.webm https://example.com\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_start\");\n        assert_eq!(cmd[\"path\"], \"demo.webm\");\n        assert_eq!(cmd[\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_record_start_with_url_no_protocol() {\n        let cmd = parse_command(\n            &args(\"record start demo.webm example.com\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_start\");\n        assert_eq!(cmd[\"path\"], \"demo.webm\");\n        assert_eq!(cmd[\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_record_start_with_chrome_extension_url() {\n        let cmd = parse_command(\n            &args(\"record start demo.webm chrome-extension://abcdef/popup.html\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_start\");\n        assert_eq!(cmd[\"path\"], \"demo.webm\");\n        assert_eq!(cmd[\"url\"], \"chrome-extension://abcdef/popup.html\");\n    }\n\n    #[test]\n    fn test_record_start_missing_path() {\n        let result = parse_command(&args(\"record start\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_record_stop() {\n        let cmd = parse_command(&args(\"record stop\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_stop\");\n    }\n\n    #[test]\n    fn test_record_restart() {\n        let cmd = parse_command(&args(\"record restart output.webm\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_restart\");\n        assert_eq!(cmd[\"path\"], \"output.webm\");\n        assert!(cmd.get(\"url\").is_none());\n    }\n\n    #[test]\n    fn test_record_restart_with_url() {\n        let cmd = parse_command(\n            &args(\"record restart demo.webm https://example.com\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"recording_restart\");\n        assert_eq!(cmd[\"path\"], \"demo.webm\");\n        assert_eq!(cmd[\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_record_restart_missing_path() {\n        let result = parse_command(&args(\"record restart\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_record_invalid_subcommand() {\n        let result = parse_command(&args(\"record foo\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::UnknownSubcommand { .. }\n        ));\n    }\n\n    #[test]\n    fn test_record_missing_subcommand() {\n        let result = parse_command(&args(\"record\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    // === Profile (CDP Tracing) Tests ===\n\n    #[test]\n    fn test_profiler_start() {\n        let cmd = parse_command(&args(\"profiler start\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"profiler_start\");\n        assert!(cmd.get(\"categories\").is_none());\n    }\n\n    #[test]\n    fn test_profiler_start_with_categories() {\n        let cmd = parse_command(\n            &args(\"profiler start --categories devtools.timeline,v8.execute\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"profiler_start\");\n        let categories = cmd[\"categories\"].as_array().unwrap();\n        assert_eq!(categories.len(), 2);\n        assert_eq!(categories[0], \"devtools.timeline\");\n        assert_eq!(categories[1], \"v8.execute\");\n    }\n\n    #[test]\n    fn test_profiler_start_categories_missing_value() {\n        let result = parse_command(&args(\"profiler start --categories\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_profiler_stop_with_path() {\n        let cmd = parse_command(&args(\"profiler stop trace.json\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"profiler_stop\");\n        assert_eq!(cmd[\"path\"], \"trace.json\");\n    }\n\n    #[test]\n    fn test_profiler_stop_no_path() {\n        let cmd = parse_command(&args(\"profiler stop\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"profiler_stop\");\n        assert!(cmd.get(\"path\").is_none());\n    }\n\n    #[test]\n    fn test_profiler_invalid_subcommand() {\n        let result = parse_command(&args(\"profiler foo\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::UnknownSubcommand { .. }\n        ));\n    }\n\n    #[test]\n    fn test_profiler_missing_subcommand() {\n        let result = parse_command(&args(\"profiler\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    // === Eval Tests ===\n\n    #[test]\n    fn test_eval_basic() {\n        let cmd = parse_command(&args(\"eval document.title\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"evaluate\");\n        assert_eq!(cmd[\"script\"], \"document.title\");\n    }\n\n    #[test]\n    fn test_eval_base64_short_flag() {\n        // \"document.title\" in base64\n        let cmd = parse_command(&args(\"eval -b ZG9jdW1lbnQudGl0bGU=\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"evaluate\");\n        assert_eq!(cmd[\"script\"], \"document.title\");\n    }\n\n    #[test]\n    fn test_eval_base64_long_flag() {\n        // \"document.title\" in base64\n        let cmd = parse_command(\n            &args(\"eval --base64 ZG9jdW1lbnQudGl0bGU=\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"evaluate\");\n        assert_eq!(cmd[\"script\"], \"document.title\");\n    }\n\n    #[test]\n    fn test_eval_base64_with_special_chars() {\n        // \"document.querySelector('[src*=\\\"_next\\\"]')\" in base64\n        let cmd = parse_command(\n            &args(\"eval -b ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ==\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"evaluate\");\n        assert_eq!(cmd[\"script\"], \"document.querySelector('[src*=\\\"_next\\\"]')\");\n    }\n\n    #[test]\n    fn test_eval_base64_invalid() {\n        let result = parse_command(&args(\"eval -b !!!invalid!!!\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::InvalidValue { .. }));\n        assert!(err.format().contains(\"Invalid base64\"));\n    }\n\n    #[test]\n    fn test_unknown_command() {\n        let result = parse_command(&args(\"unknowncommand\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::UnknownCommand { .. }\n        ));\n    }\n\n    #[test]\n    fn test_empty_args() {\n        let result = parse_command(&[], &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    // === Error message tests ===\n\n    #[test]\n    fn test_get_missing_subcommand() {\n        let result = parse_command(&args(\"get\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::MissingArguments { .. }));\n        assert!(err.format().contains(\"get\"));\n    }\n\n    #[test]\n    fn test_get_unknown_subcommand() {\n        let result = parse_command(&args(\"get foo\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::UnknownSubcommand { .. }));\n        assert!(err.format().contains(\"foo\"));\n        assert!(err.format().contains(\"text\"));\n    }\n\n    #[test]\n    fn test_get_text_missing_selector() {\n        let result = parse_command(&args(\"get text\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::MissingArguments { .. }));\n        assert!(err.format().contains(\"get text\"));\n    }\n\n    // === Protocol alignment tests ===\n\n    #[test]\n    fn test_mouse_wheel() {\n        let cmd = parse_command(&args(\"mouse wheel 100 50\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"wheel\");\n        assert_eq!(cmd[\"deltaY\"], 100);\n        assert_eq!(cmd[\"deltaX\"], 50);\n    }\n\n    #[test]\n    fn test_set_media() {\n        let cmd = parse_command(&args(\"set media dark\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"emulatemedia\");\n        assert_eq!(cmd[\"colorScheme\"], \"dark\");\n        assert_eq!(cmd[\"reducedMotion\"], \"no-preference\");\n    }\n\n    #[test]\n    fn test_set_media_reduced_motion() {\n        let cmd = parse_command(&args(\"set media light reduced-motion\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"emulatemedia\");\n        assert_eq!(cmd[\"colorScheme\"], \"light\");\n        assert_eq!(cmd[\"reducedMotion\"], \"reduce\");\n    }\n\n    #[test]\n    fn test_set_viewport() {\n        let cmd = parse_command(&args(\"set viewport 1920 1080\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"viewport\");\n        assert_eq!(cmd[\"width\"], 1920);\n        assert_eq!(cmd[\"height\"], 1080);\n        assert!(cmd.get(\"deviceScaleFactor\").is_none());\n    }\n\n    #[test]\n    fn test_set_viewport_with_scale() {\n        let cmd = parse_command(&args(\"set viewport 1920 1080 2\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"viewport\");\n        assert_eq!(cmd[\"width\"], 1920);\n        assert_eq!(cmd[\"height\"], 1080);\n        assert_eq!(cmd[\"deviceScaleFactor\"], 2.0);\n    }\n\n    #[test]\n    fn test_set_viewport_with_fractional_scale() {\n        let cmd = parse_command(&args(\"set viewport 375 812 3\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"viewport\");\n        assert_eq!(cmd[\"width\"], 375);\n        assert_eq!(cmd[\"height\"], 812);\n        assert_eq!(cmd[\"deviceScaleFactor\"], 3.0);\n    }\n\n    #[test]\n    fn test_set_viewport_missing_height() {\n        let result = parse_command(&args(\"set viewport 1920\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_set_viewport_invalid_scale() {\n        let result = parse_command(&args(\"set viewport 1920 1080 abc\"), &default_flags());\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_find_first_no_value() {\n        let cmd = parse_command(&args(\"find first a click\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"nth\");\n        assert_eq!(cmd[\"index\"], 0);\n        assert!(cmd.get(\"value\").is_none());\n    }\n\n    #[test]\n    fn test_find_first_with_value() {\n        let cmd = parse_command(&args(\"find first input fill hello\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"nth\");\n        assert_eq!(cmd[\"index\"], 0);\n        assert_eq!(cmd[\"value\"], \"hello\");\n    }\n\n    #[test]\n    fn test_find_nth_no_value() {\n        let cmd = parse_command(&args(\"find nth 2 a click\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"nth\");\n        assert_eq!(cmd[\"index\"], 2);\n        assert!(cmd.get(\"value\").is_none());\n    }\n\n    // === Download Tests ===\n\n    #[test]\n    fn test_download() {\n        let cmd = parse_command(&args(\"download #btn ./file.pdf\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"download\");\n        assert_eq!(cmd[\"selector\"], \"#btn\");\n        assert_eq!(cmd[\"path\"], \"./file.pdf\");\n    }\n\n    #[test]\n    fn test_download_with_ref() {\n        let cmd = parse_command(&args(\"download @e5 ./report.xlsx\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"download\");\n        assert_eq!(cmd[\"selector\"], \"@e5\");\n        assert_eq!(cmd[\"path\"], \"./report.xlsx\");\n    }\n\n    #[test]\n    fn test_download_missing_path() {\n        let result = parse_command(&args(\"download #btn\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_download_missing_selector() {\n        let result = parse_command(&args(\"download\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    // === Wait for Download Tests ===\n\n    #[test]\n    fn test_wait_download() {\n        let cmd = parse_command(&args(\"wait --download\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitfordownload\");\n        assert!(cmd.get(\"path\").is_none());\n    }\n\n    #[test]\n    fn test_wait_download_with_path() {\n        let cmd = parse_command(&args(\"wait --download ./file.pdf\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitfordownload\");\n        assert_eq!(cmd[\"path\"], \"./file.pdf\");\n    }\n\n    #[test]\n    fn test_wait_download_with_timeout() {\n        let cmd =\n            parse_command(&args(\"wait --download --timeout 30000\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitfordownload\");\n        assert_eq!(cmd[\"timeout\"], 30000);\n    }\n\n    #[test]\n    fn test_wait_download_with_path_and_timeout() {\n        let cmd = parse_command(\n            &args(\"wait --download ./file.pdf --timeout 30000\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"waitfordownload\");\n        assert_eq!(cmd[\"path\"], \"./file.pdf\");\n        assert_eq!(cmd[\"timeout\"], 30000);\n    }\n\n    #[test]\n    fn test_wait_download_short_flag() {\n        let cmd = parse_command(&args(\"wait -d ./file.pdf\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"waitfordownload\");\n        assert_eq!(cmd[\"path\"], \"./file.pdf\");\n    }\n\n    // === Connect (CDP) tests ===\n\n    #[test]\n    fn test_connect_with_port() {\n        let cmd = parse_command(&args(\"connect 9222\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"launch\");\n        assert_eq!(cmd[\"cdpPort\"], 9222);\n        assert!(cmd.get(\"cdpUrl\").is_none());\n    }\n\n    #[test]\n    fn test_connect_with_ws_url() {\n        let input: Vec<String> = vec![\n            \"connect\".to_string(),\n            \"ws://localhost:9222/devtools/browser/abc123\".to_string(),\n        ];\n        let cmd = parse_command(&input, &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"launch\");\n        assert_eq!(cmd[\"cdpUrl\"], \"ws://localhost:9222/devtools/browser/abc123\");\n        assert!(cmd.get(\"cdpPort\").is_none());\n    }\n\n    #[test]\n    fn test_connect_with_wss_url() {\n        let input: Vec<String> = vec![\n            \"connect\".to_string(),\n            \"wss://remote-browser.example.com/cdp?token=xyz\".to_string(),\n        ];\n        let cmd = parse_command(&input, &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"launch\");\n        assert_eq!(\n            cmd[\"cdpUrl\"],\n            \"wss://remote-browser.example.com/cdp?token=xyz\"\n        );\n        assert!(cmd.get(\"cdpPort\").is_none());\n    }\n\n    #[test]\n    fn test_connect_with_http_url() {\n        let input: Vec<String> = vec![\"connect\".to_string(), \"http://localhost:9222\".to_string()];\n        let cmd = parse_command(&input, &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"launch\");\n        assert_eq!(cmd[\"cdpUrl\"], \"http://localhost:9222\");\n        assert!(cmd.get(\"cdpPort\").is_none());\n    }\n\n    #[test]\n    fn test_connect_missing_argument() {\n        let result = parse_command(&args(\"connect\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_connect_invalid_port() {\n        let result = parse_command(&args(\"connect notanumber\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::InvalidValue { .. }));\n        assert!(err.format().contains(\"not a valid port number or URL\"));\n    }\n\n    #[test]\n    fn test_connect_port_zero() {\n        let result = parse_command(&args(\"connect 0\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::InvalidValue { .. }));\n        assert!(err.format().contains(\"port must be greater than 0\"));\n    }\n\n    #[test]\n    fn test_connect_port_out_of_range() {\n        let result = parse_command(&args(\"connect 65536\"), &default_flags());\n        assert!(result.is_err());\n        let err = result.unwrap_err();\n        assert!(matches!(err, ParseError::InvalidValue { .. }));\n        assert!(err.format().contains(\"out of range\"));\n        assert!(err.format().contains(\"1-65535\"));\n    }\n\n    #[test]\n    fn test_connect_port_max_valid() {\n        let cmd = parse_command(&args(\"connect 65535\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"launch\");\n        assert_eq!(cmd[\"cdpPort\"], 65535);\n    }\n\n    #[test]\n    fn test_connect_port_min_valid() {\n        let cmd = parse_command(&args(\"connect 1\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"launch\");\n        assert_eq!(cmd[\"cdpPort\"], 1);\n    }\n\n    // === Trace Tests ===\n\n    #[test]\n    fn test_trace_start() {\n        let cmd = parse_command(&args(\"trace start\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"trace_start\");\n    }\n\n    #[test]\n    fn test_trace_stop_with_path() {\n        let cmd = parse_command(&args(\"trace stop ./trace.zip\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"trace_stop\");\n        assert_eq!(cmd[\"path\"], \"./trace.zip\");\n    }\n\n    #[test]\n    fn test_trace_stop_without_path() {\n        let cmd = parse_command(&args(\"trace stop\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"trace_stop\");\n        assert!(cmd.get(\"path\").is_none() || cmd[\"path\"].is_null());\n    }\n\n    // === Diff Tests ===\n\n    #[test]\n    fn test_diff_snapshot_basic() {\n        let cmd = parse_command(&args(\"diff snapshot\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_snapshot\");\n    }\n\n    #[test]\n    fn test_diff_snapshot_baseline() {\n        let cmd = parse_command(\n            &args(\"diff snapshot --baseline before.txt\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_snapshot\");\n        assert_eq!(cmd[\"baseline\"], \"before.txt\");\n    }\n\n    #[test]\n    fn test_diff_snapshot_selector_compact_depth() {\n        let cmd = parse_command(\n            &args(\"diff snapshot --selector #main --compact --depth 3\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_snapshot\");\n        assert_eq!(cmd[\"selector\"], \"#main\");\n        assert_eq!(cmd[\"compact\"], true);\n        assert_eq!(cmd[\"maxDepth\"], 3);\n    }\n\n    #[test]\n    fn test_diff_snapshot_short_flags() {\n        let cmd = parse_command(\n            &args(\"diff snapshot -b snap.txt -s .content -c -d 2\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_snapshot\");\n        assert_eq!(cmd[\"baseline\"], \"snap.txt\");\n        assert_eq!(cmd[\"selector\"], \".content\");\n        assert_eq!(cmd[\"compact\"], true);\n        assert_eq!(cmd[\"maxDepth\"], 2);\n    }\n\n    #[test]\n    fn test_diff_screenshot_baseline() {\n        let cmd = parse_command(\n            &args(\"diff screenshot --baseline before.png\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_screenshot\");\n        assert_eq!(cmd[\"baseline\"], \"before.png\");\n    }\n\n    #[test]\n    fn test_diff_screenshot_all_options() {\n        let cmd = parse_command(\n            &args(\"diff screenshot --baseline b.png --output d.png --threshold 0.2 --selector #hero --full\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_screenshot\");\n        assert_eq!(cmd[\"baseline\"], \"b.png\");\n        assert_eq!(cmd[\"output\"], \"d.png\");\n        assert_eq!(cmd[\"threshold\"], 0.2);\n        assert_eq!(cmd[\"selector\"], \"#hero\");\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_diff_screenshot_missing_baseline() {\n        let result = parse_command(&args(\"diff screenshot\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_command_full_flag() {\n        let cmd = parse_command(\n            &args(\"diff screenshot --baseline b.png --full\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_screenshot\");\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_diff_screenshot_command_full_flag_shorthand() {\n        let cmd = parse_command(\n            &args(\"diff screenshot --baseline b.png -f\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_screenshot\");\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_diff_url_basic() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_url\");\n        assert_eq!(cmd[\"url1\"], \"https://a.com\");\n        assert_eq!(cmd[\"url2\"], \"https://b.com\");\n    }\n\n    #[test]\n    fn test_diff_url_with_screenshot_full() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com --screenshot --full\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_url\");\n        assert_eq!(cmd[\"screenshot\"], true);\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_diff_url_with_wait_until() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com --wait-until networkidle\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_url\");\n        assert_eq!(cmd[\"waitUntil\"], \"networkidle\");\n    }\n\n    #[test]\n    fn test_diff_url_command_full_flag() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com --full\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"fullPage\"], true);\n    }\n\n    #[test]\n    fn test_diff_missing_subcommand() {\n        let result = parse_command(&args(\"diff\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_unknown_subcommand() {\n        let result = parse_command(&args(\"diff invalid\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::UnknownSubcommand { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_baseline_missing_value() {\n        let result = parse_command(&args(\"diff snapshot --baseline\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_selector_missing_value() {\n        let result = parse_command(&args(\"diff snapshot --selector\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_depth_missing_value() {\n        let result = parse_command(&args(\"diff snapshot --depth\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_threshold_missing_value() {\n        let result = parse_command(\n            &args(\"diff screenshot --baseline b.png --threshold\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_output_missing_value() {\n        let result = parse_command(\n            &args(\"diff screenshot --baseline b.png --output\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_url_wait_until_missing_value() {\n        let result = parse_command(\n            &args(\"diff url https://a.com https://b.com --wait-until\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_unexpected_arg() {\n        let result = parse_command(&args(\"diff snapshot foo\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_unexpected_arg() {\n        let result = parse_command(\n            &args(\"diff screenshot --baseline b.png unexpected\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_url_unexpected_arg() {\n        let result = parse_command(\n            &args(\"diff url https://a.com https://b.com extra\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_unknown_flag() {\n        let result = parse_command(&args(\"diff snapshot --invalid\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_url_missing_urls() {\n        let result = parse_command(&args(\"diff url\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_url_missing_second_url() {\n        let result = parse_command(&args(\"diff url https://a.com\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_depth_invalid_value() {\n        let result = parse_command(&args(\"diff snapshot --depth abc\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_threshold_invalid_value() {\n        let result = parse_command(\n            &args(\"diff screenshot --baseline b.png --threshold abc\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_threshold_out_of_range() {\n        let result = parse_command(\n            &args(\"diff screenshot --baseline b.png --threshold 1.5\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_screenshot_threshold_negative() {\n        let result = parse_command(\n            &args(\"diff screenshot --baseline b.png --threshold -0.5\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_diff_url_with_selector() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com --selector #main\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_url\");\n        assert_eq!(cmd[\"selector\"], \"#main\");\n    }\n\n    #[test]\n    fn test_diff_url_with_compact_depth() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com --compact --depth 3\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_url\");\n        assert_eq!(cmd[\"compact\"], true);\n        assert_eq!(cmd[\"maxDepth\"], 3);\n    }\n\n    #[test]\n    fn test_diff_url_with_short_snapshot_flags() {\n        let cmd = parse_command(\n            &args(\"diff url https://a.com https://b.com -s .content -c -d 2\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"diff_url\");\n        assert_eq!(cmd[\"selector\"], \".content\");\n        assert_eq!(cmd[\"compact\"], true);\n        assert_eq!(cmd[\"maxDepth\"], 2);\n    }\n\n    #[test]\n    fn test_diff_url_depth_invalid_value() {\n        let result = parse_command(\n            &args(\"diff url https://a.com https://b.com --depth abc\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_snapshot_depth_negative_value() {\n        let result = parse_command(&args(\"diff snapshot --depth -1\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_url_depth_negative_value() {\n        let result = parse_command(\n            &args(\"diff url https://a.com https://b.com --depth -1\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::InvalidValue { .. }\n        ));\n    }\n\n    #[test]\n    fn test_diff_url_selector_missing_value() {\n        let result = parse_command(\n            &args(\"diff url https://a.com https://b.com --selector\"),\n            &default_flags(),\n        );\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    // === Scroll Tests ===\n\n    #[test]\n    fn test_scroll_defaults() {\n        let cmd = parse_command(&args(\"scroll\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"scroll\");\n        assert_eq!(cmd[\"direction\"], \"down\");\n        assert_eq!(cmd[\"amount\"], 300);\n        assert!(cmd.get(\"selector\").is_none());\n    }\n\n    #[test]\n    fn test_scroll_direction_and_amount() {\n        let cmd = parse_command(&args(\"scroll up 200\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"scroll\");\n        assert_eq!(cmd[\"direction\"], \"up\");\n        assert_eq!(cmd[\"amount\"], 200);\n    }\n\n    #[test]\n    fn test_scroll_with_selector() {\n        let cmd = parse_command(\n            &args(\"scroll down 500 --selector div.scroll-container\"),\n            &default_flags(),\n        )\n        .unwrap();\n        assert_eq!(cmd[\"action\"], \"scroll\");\n        assert_eq!(cmd[\"direction\"], \"down\");\n        assert_eq!(cmd[\"amount\"], 500);\n        assert_eq!(cmd[\"selector\"], \"div.scroll-container\");\n    }\n\n    #[test]\n    fn test_scroll_with_selector_short_flag() {\n        let cmd = parse_command(&args(\"scroll left 100 -s .sidebar\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"scroll\");\n        assert_eq!(cmd[\"direction\"], \"left\");\n        assert_eq!(cmd[\"amount\"], 100);\n        assert_eq!(cmd[\"selector\"], \".sidebar\");\n    }\n\n    #[test]\n    fn test_scroll_selector_before_positional() {\n        let cmd =\n            parse_command(&args(\"scroll --selector .panel down 400\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"scroll\");\n        assert_eq!(cmd[\"direction\"], \"down\");\n        assert_eq!(cmd[\"amount\"], 400);\n        assert_eq!(cmd[\"selector\"], \".panel\");\n    }\n\n    #[test]\n    fn test_scroll_selector_only() {\n        let cmd = parse_command(&args(\"scroll --selector .content\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"scroll\");\n        assert_eq!(cmd[\"direction\"], \"down\");\n        assert_eq!(cmd[\"amount\"], 300);\n        assert_eq!(cmd[\"selector\"], \".content\");\n    }\n\n    #[test]\n    fn test_scroll_selector_missing_value() {\n        let result = parse_command(&args(\"scroll down 500 --selector\"), &default_flags());\n        assert!(result.is_err());\n        assert!(matches!(\n            result.unwrap_err(),\n            ParseError::MissingArguments { .. }\n        ));\n    }\n\n    // === Inspect / CDP URL ===\n\n    #[test]\n    fn test_inspect() {\n        let cmd = parse_command(&args(\"inspect\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"inspect\");\n    }\n\n    #[test]\n    fn test_get_cdp_url() {\n        let cmd = parse_command(&args(\"get cdp-url\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"cdp_url\");\n    }\n\n    // === Batch Tests ===\n\n    #[test]\n    fn test_batch_default() {\n        let cmd = parse_command(&args(\"batch\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"batch\");\n        assert_eq!(cmd[\"bail\"], false);\n    }\n\n    #[test]\n    fn test_batch_with_bail() {\n        let cmd = parse_command(&args(\"batch --bail\"), &default_flags()).unwrap();\n        assert_eq!(cmd[\"action\"], \"batch\");\n        assert_eq!(cmd[\"bail\"], true);\n    }\n}\n"
  },
  {
    "path": "cli/src/connection.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::env;\nuse std::fs;\nuse std::io::{BufRead, BufReader, Read, Write};\nuse std::net::TcpStream;\nuse std::path::PathBuf;\nuse std::process::{Command, Stdio};\nuse std::thread;\nuse std::time::Duration;\n\n#[cfg(unix)]\nuse std::os::unix::net::UnixStream;\n\n#[derive(Serialize)]\n#[allow(dead_code)]\npub struct Request {\n    pub id: String,\n    pub action: String,\n    #[serde(flatten)]\n    pub extra: Value,\n}\n\n#[derive(Deserialize, Serialize, Default)]\npub struct Response {\n    pub success: bool,\n    pub data: Option<Value>,\n    pub error: Option<String>,\n}\n\n#[allow(dead_code)]\npub enum Connection {\n    #[cfg(unix)]\n    Unix(UnixStream),\n    Tcp(TcpStream),\n}\n\nimpl Read for Connection {\n    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {\n        match self {\n            #[cfg(unix)]\n            Connection::Unix(s) => s.read(buf),\n            Connection::Tcp(s) => s.read(buf),\n        }\n    }\n}\n\nimpl Write for Connection {\n    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {\n        match self {\n            #[cfg(unix)]\n            Connection::Unix(s) => s.write(buf),\n            Connection::Tcp(s) => s.write(buf),\n        }\n    }\n\n    fn flush(&mut self) -> std::io::Result<()> {\n        match self {\n            #[cfg(unix)]\n            Connection::Unix(s) => s.flush(),\n            Connection::Tcp(s) => s.flush(),\n        }\n    }\n}\n\nimpl Connection {\n    pub fn set_read_timeout(&self, dur: Option<Duration>) -> std::io::Result<()> {\n        match self {\n            #[cfg(unix)]\n            Connection::Unix(s) => s.set_read_timeout(dur),\n            Connection::Tcp(s) => s.set_read_timeout(dur),\n        }\n    }\n\n    pub fn set_write_timeout(&self, dur: Option<Duration>) -> std::io::Result<()> {\n        match self {\n            #[cfg(unix)]\n            Connection::Unix(s) => s.set_write_timeout(dur),\n            Connection::Tcp(s) => s.set_write_timeout(dur),\n        }\n    }\n}\n\n/// Get the base directory for socket/pid files.\n/// Priority: AGENT_BROWSER_SOCKET_DIR > XDG_RUNTIME_DIR > ~/.agent-browser > tmpdir\npub fn get_socket_dir() -> PathBuf {\n    // 1. Explicit override (ignore empty string)\n    if let Ok(dir) = env::var(\"AGENT_BROWSER_SOCKET_DIR\") {\n        if !dir.is_empty() {\n            return PathBuf::from(dir);\n        }\n    }\n\n    // 2. XDG_RUNTIME_DIR (Linux standard, ignore empty string)\n    if let Ok(runtime_dir) = env::var(\"XDG_RUNTIME_DIR\") {\n        if !runtime_dir.is_empty() {\n            return PathBuf::from(runtime_dir).join(\"agent-browser\");\n        }\n    }\n\n    // 3. Home directory fallback (like Docker Desktop's ~/.docker/run/)\n    if let Some(home) = dirs::home_dir() {\n        return home.join(\".agent-browser\");\n    }\n\n    // 4. Last resort: temp dir\n    env::temp_dir().join(\"agent-browser\")\n}\n\n#[cfg(unix)]\nfn get_socket_path(session: &str) -> PathBuf {\n    get_socket_dir().join(format!(\"{}.sock\", session))\n}\n\nfn get_pid_path(session: &str) -> PathBuf {\n    get_socket_dir().join(format!(\"{}.pid\", session))\n}\n\n/// Clean up stale socket and PID files for a session\nfn cleanup_stale_files(session: &str) {\n    let pid_path = get_pid_path(session);\n    let _ = fs::remove_file(&pid_path);\n\n    #[cfg(unix)]\n    {\n        let socket_path = get_socket_path(session);\n        let _ = fs::remove_file(&socket_path);\n    }\n\n    #[cfg(windows)]\n    {\n        let port_path = get_port_path(session);\n        let _ = fs::remove_file(&port_path);\n    }\n}\n\n#[cfg(windows)]\nfn get_port_path(session: &str) -> PathBuf {\n    get_socket_dir().join(format!(\"{}.port\", session))\n}\n\n#[cfg(windows)]\nfn get_port_for_session(session: &str) -> u16 {\n    let mut hash: i32 = 0;\n    for c in session.chars() {\n        hash = ((hash << 5).wrapping_sub(hash)).wrapping_add(c as i32);\n    }\n    // Correct logic: first take absolute modulo, then cast to u16\n    // Using unsigned_abs() to safely handle i32::MIN\n    49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)\n}\n\nfn daemon_ready(session: &str) -> bool {\n    #[cfg(unix)]\n    {\n        let socket_path = get_socket_path(session);\n        UnixStream::connect(&socket_path).is_ok()\n    }\n    #[cfg(windows)]\n    {\n        let port = get_port_for_session(session);\n        TcpStream::connect_timeout(\n            &format!(\"127.0.0.1:{}\", port).parse().unwrap(),\n            Duration::from_millis(50),\n        )\n        .is_ok()\n    }\n}\n\n/// Result of ensure_daemon indicating whether a new daemon was started\npub struct DaemonResult {\n    /// True if we connected to an existing daemon, false if we started a new one\n    pub already_running: bool,\n}\n\n/// Options forwarded to the daemon process as environment variables.\n/// Note: `confirm_interactive` is intentionally absent -- it is a CLI-side\n/// UX concern (prompting the user on stdin) and not a daemon configuration.\n/// The daemon only needs `confirm_actions` to gate action categories.\npub struct DaemonOptions<'a> {\n    pub headed: bool,\n    pub debug: bool,\n    pub executable_path: Option<&'a str>,\n    pub extensions: &'a [String],\n    pub args: Option<&'a str>,\n    pub user_agent: Option<&'a str>,\n    pub proxy: Option<&'a str>,\n    pub proxy_bypass: Option<&'a str>,\n    pub ignore_https_errors: bool,\n    pub allow_file_access: bool,\n    pub profile: Option<&'a str>,\n    pub state: Option<&'a str>,\n    pub provider: Option<&'a str>,\n    pub device: Option<&'a str>,\n    pub session_name: Option<&'a str>,\n    pub download_path: Option<&'a str>,\n    pub allowed_domains: Option<&'a [String]>,\n    pub action_policy: Option<&'a str>,\n    pub confirm_actions: Option<&'a str>,\n    pub engine: Option<&'a str>,\n    pub auto_connect: bool,\n    pub idle_timeout: Option<&'a str>,\n    pub cdp: Option<&'a str>,\n}\n\nfn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) {\n    cmd.env(\"AGENT_BROWSER_DAEMON\", \"1\")\n        .env(\"AGENT_BROWSER_SESSION\", session);\n\n    if opts.headed {\n        cmd.env(\"AGENT_BROWSER_HEADED\", \"1\");\n    }\n    if opts.debug {\n        cmd.env(\"AGENT_BROWSER_DEBUG\", \"1\");\n    }\n    if let Some(path) = opts.executable_path {\n        cmd.env(\"AGENT_BROWSER_EXECUTABLE_PATH\", path);\n    }\n    if !opts.extensions.is_empty() {\n        cmd.env(\"AGENT_BROWSER_EXTENSIONS\", opts.extensions.join(\",\"));\n    }\n    if let Some(a) = opts.args {\n        cmd.env(\"AGENT_BROWSER_ARGS\", a);\n    }\n    if let Some(ua) = opts.user_agent {\n        cmd.env(\"AGENT_BROWSER_USER_AGENT\", ua);\n    }\n    if let Some(p) = opts.proxy {\n        cmd.env(\"AGENT_BROWSER_PROXY\", p);\n    }\n    if let Some(pb) = opts.proxy_bypass {\n        cmd.env(\"AGENT_BROWSER_PROXY_BYPASS\", pb);\n    }\n    if opts.ignore_https_errors {\n        cmd.env(\"AGENT_BROWSER_IGNORE_HTTPS_ERRORS\", \"1\");\n    }\n    if opts.allow_file_access {\n        cmd.env(\"AGENT_BROWSER_ALLOW_FILE_ACCESS\", \"1\");\n    }\n    if let Some(prof) = opts.profile {\n        cmd.env(\"AGENT_BROWSER_PROFILE\", prof);\n    }\n    if let Some(st) = opts.state {\n        cmd.env(\"AGENT_BROWSER_STATE\", st);\n    }\n    if let Some(p) = opts.provider {\n        cmd.env(\"AGENT_BROWSER_PROVIDER\", p);\n    }\n    if let Some(d) = opts.device {\n        cmd.env(\"AGENT_BROWSER_IOS_DEVICE\", d);\n    }\n    if let Some(sn) = opts.session_name {\n        cmd.env(\"AGENT_BROWSER_SESSION_NAME\", sn);\n    }\n    if let Some(dp) = opts.download_path {\n        cmd.env(\"AGENT_BROWSER_DOWNLOAD_PATH\", dp);\n    }\n    if let Some(ad) = opts.allowed_domains {\n        cmd.env(\"AGENT_BROWSER_ALLOWED_DOMAINS\", ad.join(\",\"));\n    }\n    if let Some(ap) = opts.action_policy {\n        cmd.env(\"AGENT_BROWSER_ACTION_POLICY\", ap);\n    }\n    if let Some(ca) = opts.confirm_actions {\n        cmd.env(\"AGENT_BROWSER_CONFIRM_ACTIONS\", ca);\n    }\n    if let Some(engine) = opts.engine {\n        cmd.env(\"AGENT_BROWSER_ENGINE\", engine);\n    }\n    if opts.auto_connect {\n        cmd.env(\"AGENT_BROWSER_AUTO_CONNECT\", \"1\");\n    }\n    if let Some(idle) = opts.idle_timeout {\n        cmd.env(\"AGENT_BROWSER_IDLE_TIMEOUT_MS\", idle);\n    }\n    if let Some(cdp) = opts.cdp {\n        cmd.env(\"AGENT_BROWSER_CDP\", cdp);\n    }\n}\n\npub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result<DaemonResult, String> {\n    // Socket connectivity is the sole liveness check — no PID check — so\n    // callers in a different PID namespace (e.g. unshare) can still reuse\n    // an existing daemon they can reach over the socket.\n    if daemon_ready(session) {\n        // Double-check it's actually responsive by waiting and checking again\n        // This handles the race condition where daemon is shutting down\n        // (daemon has a 100ms shutdown delay, so we wait longer)\n        thread::sleep(Duration::from_millis(150));\n        if daemon_ready(session) {\n            return Ok(DaemonResult {\n                already_running: true,\n            });\n        }\n    }\n\n    // Clean up any stale socket/pid files before starting fresh\n    cleanup_stale_files(session);\n\n    // Ensure socket directory exists\n    let socket_dir = get_socket_dir();\n    if !socket_dir.exists() {\n        fs::create_dir_all(&socket_dir)\n            .map_err(|e| format!(\"Failed to create socket directory: {}\", e))?;\n    }\n\n    // Pre-flight check: Validate socket path length (Unix limit is 104 bytes including null terminator)\n    #[cfg(unix)]\n    {\n        let socket_path = get_socket_path(session);\n        let path_len = socket_path.as_os_str().len();\n        if path_len > 103 {\n            return Err(format!(\n                \"Session name '{}' is too long. Socket path would be {} bytes (max 103).\\n\\\n                 Use a shorter session name or set AGENT_BROWSER_SOCKET_DIR to a shorter path.\",\n                session, path_len\n            ));\n        }\n    }\n\n    // Pre-flight check: Verify socket directory is writable\n    {\n        let test_file = socket_dir.join(\".write_test\");\n        match fs::write(&test_file, b\"\") {\n            Ok(_) => {\n                let _ = fs::remove_file(&test_file);\n            }\n            Err(e) => {\n                return Err(format!(\n                    \"Socket directory '{}' is not writable: {}\",\n                    socket_dir.display(),\n                    e\n                ));\n            }\n        }\n    }\n\n    let exe_path = env::current_exe().map_err(|e| e.to_string())?;\n    let exe_path = exe_path.canonicalize().unwrap_or(exe_path);\n\n    #[allow(unused_assignments)]\n    let mut daemon_child: Option<std::process::Child> = None;\n\n    #[cfg(unix)]\n    {\n        use std::os::unix::process::CommandExt;\n\n        let mut cmd = Command::new(&exe_path);\n        cmd.env(\"AGENT_BROWSER_DAEMON\", \"1\");\n        apply_daemon_env(&mut cmd, session, opts);\n\n        unsafe {\n            cmd.pre_exec(|| {\n                libc::setsid();\n                Ok(())\n            });\n        }\n\n        daemon_child = Some(\n            cmd.stdin(Stdio::null())\n                .stdout(Stdio::null())\n                .stderr(Stdio::piped())\n                .spawn()\n                .map_err(|e| format!(\"Failed to start daemon: {}\", e))?,\n        );\n    }\n\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n\n        let mut cmd = Command::new(&exe_path);\n        cmd.env(\"AGENT_BROWSER_DAEMON\", \"1\");\n        apply_daemon_env(&mut cmd, session, opts);\n\n        const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;\n        const DETACHED_PROCESS: u32 = 0x00000008;\n\n        daemon_child = Some(\n            cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)\n                .stdin(Stdio::null())\n                .stdout(Stdio::null())\n                .stderr(Stdio::piped())\n                .spawn()\n                .map_err(|e| format!(\"Failed to start daemon: {}\", e))?,\n        );\n    }\n\n    for _ in 0..50 {\n        if daemon_ready(session) {\n            return Ok(DaemonResult {\n                already_running: false,\n            });\n        }\n\n        // Detect early daemon exit and surface the real error from stderr\n        if let Some(ref mut child) = daemon_child {\n            if let Ok(Some(_)) = child.try_wait() {\n                let mut stderr_output = String::new();\n                if let Some(mut stderr) = child.stderr.take() {\n                    let _ = stderr.read_to_string(&mut stderr_output);\n                }\n                let stderr_trimmed = stderr_output.trim();\n                if !stderr_trimmed.is_empty() {\n                    let msg = if stderr_trimmed.len() > 500 {\n                        let mut end = 500;\n                        while !stderr_trimmed.is_char_boundary(end) {\n                            end -= 1;\n                        }\n                        &stderr_trimmed[..end]\n                    } else {\n                        stderr_trimmed\n                    };\n                    return Err(format!(\"Daemon process exited during startup:\\n{}\", msg));\n                }\n                return Err(\n                    \"Daemon process exited during startup with no error output. \\\n                     Re-run with --debug for more details.\"\n                        .to_string(),\n                );\n            }\n        }\n\n        thread::sleep(Duration::from_millis(100));\n    }\n\n    #[cfg(unix)]\n    let endpoint_info = format!(\n        \"socket: {}\",\n        get_socket_dir().join(format!(\"{}.sock\", session)).display()\n    );\n    #[cfg(windows)]\n    let endpoint_info = format!(\"port: 127.0.0.1:{}\", get_port_for_session(session));\n\n    Err(format!(\"Daemon failed to start ({})\", endpoint_info))\n}\n\nfn connect(session: &str) -> Result<Connection, String> {\n    #[cfg(unix)]\n    {\n        let socket_path = get_socket_path(session);\n        UnixStream::connect(&socket_path)\n            .map(Connection::Unix)\n            .map_err(|e| format!(\"Failed to connect: {}\", e))\n    }\n    #[cfg(windows)]\n    {\n        let port = get_port_for_session(session);\n        TcpStream::connect(format!(\"127.0.0.1:{}\", port))\n            .map(Connection::Tcp)\n            .map_err(|e| format!(\"Failed to connect: {}\", e))\n    }\n}\n\npub fn send_command(cmd: Value, session: &str) -> Result<Response, String> {\n    // Retry logic for transient errors (EAGAIN/EWOULDBLOCK/connection issues)\n    const MAX_RETRIES: u32 = 5;\n    const RETRY_DELAY_MS: u64 = 200;\n\n    let mut last_error = String::new();\n\n    for attempt in 0..MAX_RETRIES {\n        if attempt > 0 {\n            thread::sleep(Duration::from_millis(RETRY_DELAY_MS * (attempt as u64)));\n        }\n\n        match send_command_once(&cmd, session) {\n            Ok(response) => return Ok(response),\n            Err(e) => {\n                if is_transient_error(&e) {\n                    last_error = e;\n                    continue;\n                }\n                // Non-transient error, fail immediately\n                return Err(e);\n            }\n        }\n    }\n\n    Err(format!(\n        \"{} (after {} retries - daemon may be busy or unresponsive)\",\n        last_error, MAX_RETRIES\n    ))\n}\n\n/// Check if an error is transient and worth retrying.\n/// Transient errors include:\n/// - EAGAIN/EWOULDBLOCK (os error 35 on macOS, 11 on Linux)\n/// - EOF errors (daemon closed connection before responding)\n/// - Connection reset/broken pipe (daemon crashed or restarting)\n/// - Connection refused/socket not found (daemon still starting)\nfn is_transient_error(error: &str) -> bool {\n    error.contains(\"os error 35\") // EAGAIN on macOS\n        || error.contains(\"os error 11\") // EAGAIN on Linux\n        || error.contains(\"WouldBlock\")\n        || error.contains(\"Resource temporarily unavailable\")\n        || error.contains(\"EOF\")\n        || error.contains(\"line 1 column 0\") // Empty JSON response\n        || error.contains(\"Connection reset\")\n        || error.contains(\"Broken pipe\")\n        || error.contains(\"os error 54\") // Connection reset by peer (macOS)\n        || error.contains(\"os error 104\") // Connection reset by peer (Linux)\n        || error.contains(\"os error 2\") // No such file or directory (socket gone)\n        || error.contains(\"os error 61\") // Connection refused (macOS)\n        || error.contains(\"os error 111\") // Connection refused (Linux)\n        || error.contains(\"os error 10061\") // Connection refused (Windows)\n        || error.contains(\"os error 10054\") // Connection reset by peer (Windows)\n}\n\nfn send_command_once(cmd: &Value, session: &str) -> Result<Response, String> {\n    let mut stream = connect(session)?;\n\n    stream.set_read_timeout(Some(Duration::from_secs(30))).ok();\n    stream.set_write_timeout(Some(Duration::from_secs(5))).ok();\n\n    let mut json_str = serde_json::to_string(cmd).map_err(|e| e.to_string())?;\n    json_str.push('\\n');\n\n    stream\n        .write_all(json_str.as_bytes())\n        .map_err(|e| format!(\"Failed to send: {}\", e))?;\n\n    let mut reader = BufReader::new(stream);\n    let mut response_line = String::new();\n    reader\n        .read_line(&mut response_line)\n        .map_err(|e| format!(\"Failed to read: {}\", e))?;\n\n    serde_json::from_str(&response_line).map_err(|e| format!(\"Invalid response: {}\", e))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_utils::EnvGuard;\n\n    #[test]\n    fn test_get_socket_dir_explicit_override() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_SOCKET_DIR\", \"XDG_RUNTIME_DIR\"]);\n\n        _guard.set(\"AGENT_BROWSER_SOCKET_DIR\", \"/custom/socket/path\");\n        _guard.remove(\"XDG_RUNTIME_DIR\");\n\n        assert_eq!(get_socket_dir(), PathBuf::from(\"/custom/socket/path\"));\n    }\n\n    #[test]\n    fn test_get_socket_dir_ignores_empty_socket_dir() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_SOCKET_DIR\", \"XDG_RUNTIME_DIR\"]);\n\n        _guard.set(\"AGENT_BROWSER_SOCKET_DIR\", \"\");\n        _guard.remove(\"XDG_RUNTIME_DIR\");\n\n        assert!(get_socket_dir()\n            .to_string_lossy()\n            .ends_with(\".agent-browser\"));\n    }\n\n    #[test]\n    fn test_get_socket_dir_xdg_runtime() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_SOCKET_DIR\", \"XDG_RUNTIME_DIR\"]);\n\n        _guard.remove(\"AGENT_BROWSER_SOCKET_DIR\");\n        _guard.set(\"XDG_RUNTIME_DIR\", \"/run/user/1000\");\n\n        assert_eq!(\n            get_socket_dir(),\n            PathBuf::from(\"/run/user/1000/agent-browser\")\n        );\n    }\n\n    #[test]\n    fn test_get_socket_dir_ignores_empty_xdg_runtime() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_SOCKET_DIR\", \"XDG_RUNTIME_DIR\"]);\n\n        _guard.set(\"AGENT_BROWSER_SOCKET_DIR\", \"\");\n        _guard.set(\"XDG_RUNTIME_DIR\", \"\");\n\n        assert!(get_socket_dir()\n            .to_string_lossy()\n            .ends_with(\".agent-browser\"));\n    }\n\n    #[test]\n    fn test_get_socket_dir_home_fallback() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_SOCKET_DIR\", \"XDG_RUNTIME_DIR\"]);\n\n        _guard.remove(\"AGENT_BROWSER_SOCKET_DIR\");\n        _guard.remove(\"XDG_RUNTIME_DIR\");\n\n        let result = get_socket_dir();\n        assert!(result.to_string_lossy().ends_with(\".agent-browser\"));\n        assert!(\n            result.to_string_lossy().contains(\"home\") || result.to_string_lossy().contains(\"Users\")\n        );\n    }\n\n    // === Transient Error Detection Tests ===\n\n    #[test]\n    fn test_is_transient_error_eagain_macos() {\n        assert!(is_transient_error(\n            \"Failed to read: Resource temporarily unavailable (os error 35)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_eagain_linux() {\n        assert!(is_transient_error(\n            \"Failed to read: Resource temporarily unavailable (os error 11)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_would_block() {\n        assert!(is_transient_error(\"operation WouldBlock\"));\n    }\n\n    #[test]\n    fn test_is_transient_error_resource_unavailable() {\n        assert!(is_transient_error(\"Resource temporarily unavailable\"));\n    }\n\n    #[test]\n    fn test_is_transient_error_eof() {\n        assert!(is_transient_error(\n            \"Invalid response: EOF while parsing a value at line 1 column 0\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_empty_json() {\n        assert!(is_transient_error(\n            \"Invalid response: expected value at line 1 column 0\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_reset() {\n        assert!(is_transient_error(\"Connection reset by peer\"));\n    }\n\n    #[test]\n    fn test_is_transient_error_broken_pipe() {\n        assert!(is_transient_error(\"Broken pipe\"));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_reset_macos() {\n        assert!(is_transient_error(\n            \"Failed to send: Connection reset by peer (os error 54)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_reset_linux() {\n        assert!(is_transient_error(\n            \"Failed to send: Connection reset by peer (os error 104)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_socket_not_found() {\n        assert!(is_transient_error(\n            \"Failed to connect: No such file or directory (os error 2)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_refused_macos() {\n        assert!(is_transient_error(\n            \"Failed to connect: Connection refused (os error 61)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_refused_linux() {\n        assert!(is_transient_error(\n            \"Failed to connect: Connection refused (os error 111)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_refused_windows() {\n        assert!(is_transient_error(\n            \"Failed to connect: No connection could be made because the target machine actively refused it. (os error 10061)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_connection_reset_windows() {\n        assert!(is_transient_error(\n            \"Failed to send: An existing connection was forcibly closed by the remote host. (os error 10054)\"\n        ));\n    }\n\n    #[test]\n    fn test_is_transient_error_non_transient() {\n        // These should NOT be considered transient\n        assert!(!is_transient_error(\"Unknown command: foo\"));\n        assert!(!is_transient_error(\"Invalid JSON syntax\"));\n        assert!(!is_transient_error(\"Permission denied\"));\n        assert!(!is_transient_error(\"Daemon not found\"));\n    }\n\n    #[test]\n    #[cfg(windows)]\n    fn test_get_port_for_session() {\n        assert_eq!(get_port_for_session(\"default\"), 50838);\n        assert_eq!(get_port_for_session(\"my-session\"), 63105);\n        assert_eq!(get_port_for_session(\"work\"), 51184);\n        assert_eq!(get_port_for_session(\"\"), 49152);\n    }\n}\n"
  },
  {
    "path": "cli/src/flags.rs",
    "content": "use crate::color;\nuse serde::Deserialize;\nuse std::env;\nuse std::fs;\nuse std::path::{Path, PathBuf};\n\nconst CONFIG_DIR: &str = \".agent-browser\";\nconst CONFIG_FILENAME: &str = \"config.json\";\nconst PROJECT_CONFIG_FILENAME: &str = \"agent-browser.json\";\n\n/// Parse idle timeout from user-friendly format.\n/// Supports: \"10s\" (seconds), \"3m\" (minutes), \"1h\" (hours), or raw milliseconds.\nfn parse_idle_timeout(s: &str) -> Result<String, String> {\n    let s = s.trim();\n    if s.is_empty() {\n        return Err(\"Empty idle timeout\".to_string());\n    }\n\n    // If the value ends with a unit suffix, convert it to milliseconds.\n    if s.chars().last().is_some_and(|c| c.is_ascii_alphabetic()) {\n        let (num_str, unit) = s.split_at(s.len() - 1);\n        let num: u64 = num_str.parse().map_err(|_| \"Invalid number\")?;\n\n        let ms = match unit {\n            \"s\" => num * 1000,\n            \"m\" => num * 60 * 1000,\n            \"h\" => num * 60 * 60 * 1000,\n            _ => return Err(\"Invalid idle timeout unit (use s, m, h, or raw ms)\".to_string()),\n        };\n        return Ok(ms.to_string());\n    }\n\n    // Pure numbers are already expressed in milliseconds.\n    s.parse::<u64>().map_err(|_| \"Invalid idle timeout\")?;\n    Ok(s.to_string())\n}\n\nfn parse_idle_timeout_value(value: Option<String>, source: &str) -> Option<String> {\n    value.and_then(|raw| match parse_idle_timeout(&raw) {\n        Ok(ms) => Some(ms),\n        Err(e) => {\n            eprintln!(\n                \"{} invalid idle timeout from {}: {}\",\n                color::warning_indicator(),\n                source,\n                e\n            );\n            None\n        }\n    })\n}\n\n#[derive(Debug, Default, Deserialize)]\n#[serde(default, rename_all = \"camelCase\")]\npub struct Config {\n    pub headed: Option<bool>,\n    pub json: Option<bool>,\n    pub debug: Option<bool>,\n    pub session: Option<String>,\n    pub session_name: Option<String>,\n    pub executable_path: Option<String>,\n    pub extensions: Option<Vec<String>>,\n    pub profile: Option<String>,\n    pub state: Option<String>,\n    pub proxy: Option<String>,\n    pub proxy_bypass: Option<String>,\n    pub args: Option<String>,\n    pub user_agent: Option<String>,\n    pub provider: Option<String>,\n    pub device: Option<String>,\n    pub ignore_https_errors: Option<bool>,\n    pub allow_file_access: Option<bool>,\n    pub cdp: Option<String>,\n    pub auto_connect: Option<bool>,\n    pub headers: Option<String>,\n    pub annotate: Option<bool>,\n    pub color_scheme: Option<String>,\n    pub download_path: Option<String>,\n    pub content_boundaries: Option<bool>,\n    pub max_output: Option<usize>,\n    pub allowed_domains: Option<Vec<String>>,\n    pub action_policy: Option<String>,\n    pub confirm_actions: Option<String>,\n    pub confirm_interactive: Option<bool>,\n    pub engine: Option<String>,\n    pub screenshot_dir: Option<String>,\n    pub screenshot_quality: Option<u32>,\n    pub screenshot_format: Option<String>,\n    pub idle_timeout: Option<String>,\n}\n\nimpl Config {\n    fn merge(self, other: Config) -> Config {\n        Config {\n            headed: other.headed.or(self.headed),\n            json: other.json.or(self.json),\n            debug: other.debug.or(self.debug),\n            session: other.session.or(self.session),\n            session_name: other.session_name.or(self.session_name),\n            executable_path: other.executable_path.or(self.executable_path),\n            extensions: match (self.extensions, other.extensions) {\n                (Some(mut a), Some(b)) => {\n                    a.extend(b);\n                    Some(a)\n                }\n                (a, b) => b.or(a),\n            },\n            profile: other.profile.or(self.profile),\n            state: other.state.or(self.state),\n            proxy: other.proxy.or(self.proxy),\n            proxy_bypass: other.proxy_bypass.or(self.proxy_bypass),\n            args: other.args.or(self.args),\n            user_agent: other.user_agent.or(self.user_agent),\n            provider: other.provider.or(self.provider),\n            device: other.device.or(self.device),\n            ignore_https_errors: other.ignore_https_errors.or(self.ignore_https_errors),\n            allow_file_access: other.allow_file_access.or(self.allow_file_access),\n            cdp: other.cdp.or(self.cdp),\n            auto_connect: other.auto_connect.or(self.auto_connect),\n            headers: other.headers.or(self.headers),\n            annotate: other.annotate.or(self.annotate),\n            color_scheme: other.color_scheme.or(self.color_scheme),\n            download_path: other.download_path.or(self.download_path),\n            content_boundaries: other.content_boundaries.or(self.content_boundaries),\n            max_output: other.max_output.or(self.max_output),\n            allowed_domains: other.allowed_domains.or(self.allowed_domains),\n            action_policy: other.action_policy.or(self.action_policy),\n            confirm_actions: other.confirm_actions.or(self.confirm_actions),\n            confirm_interactive: other.confirm_interactive.or(self.confirm_interactive),\n            engine: other.engine.or(self.engine),\n            screenshot_dir: other.screenshot_dir.or(self.screenshot_dir),\n            screenshot_quality: other.screenshot_quality.or(self.screenshot_quality),\n            screenshot_format: other.screenshot_format.or(self.screenshot_format),\n            idle_timeout: other.idle_timeout.or(self.idle_timeout),\n        }\n    }\n}\n\nfn read_config_file(path: &Path) -> Option<Config> {\n    let content = fs::read_to_string(path).ok()?;\n    match serde_json::from_str::<Config>(&content) {\n        Ok(mut config) => {\n            config.idle_timeout = parse_idle_timeout_value(\n                config.idle_timeout.take(),\n                &format!(\"config file {}\", path.display()),\n            );\n            Some(config)\n        }\n        Err(e) => {\n            eprintln!(\n                \"{} invalid config file {}: {}\",\n                color::warning_indicator(),\n                path.display(),\n                e\n            );\n            None\n        }\n    }\n}\n\n/// Check if a boolean environment variable is set to a truthy value.\n/// Returns false when unset, empty, or set to \"0\", \"false\", or \"no\" (case-insensitive).\nfn env_var_is_truthy(name: &str) -> bool {\n    match env::var(name) {\n        Ok(val) => !matches!(val.to_lowercase().as_str(), \"0\" | \"false\" | \"no\" | \"\"),\n        Err(_) => false,\n    }\n}\n\n/// Parse an optional boolean value after a flag. Returns (value, consumed_next_arg).\n/// Recognizes \"true\" as true, \"false\" as false. Bare flag defaults to true.\nfn parse_bool_arg(args: &[String], i: usize) -> (bool, bool) {\n    if let Some(v) = args.get(i + 1) {\n        match v.as_str() {\n            \"true\" => (true, true),\n            \"false\" => (false, true),\n            _ => (true, false),\n        }\n    } else {\n        (true, false)\n    }\n}\n\n/// Extract --config <path> from args before full flag parsing.\n/// Returns `Some(Some(path))` if --config <path> found, `Some(None)` if --config\n/// was the last arg with no value, `None` if --config not present.\n///\n/// Only flags that consume a following argument need to be listed here.\n/// Boolean flags (--content-boundaries, --confirm-interactive, etc.) are\n/// intentionally absent -- they don't take a value, so they can't cause\n/// the next argument to be mis-consumed.\nfn extract_config_path(args: &[String]) -> Option<Option<String>> {\n    const FLAGS_WITH_VALUE: &[&str] = &[\n        \"--session\",\n        \"--headers\",\n        \"--executable-path\",\n        \"--cdp\",\n        \"--extension\",\n        \"--profile\",\n        \"--state\",\n        \"--proxy\",\n        \"--proxy-bypass\",\n        \"--args\",\n        \"--user-agent\",\n        \"-p\",\n        \"--provider\",\n        \"--device\",\n        \"--session-name\",\n        \"--color-scheme\",\n        \"--download-path\",\n        \"--max-output\",\n        \"--allowed-domains\",\n        \"--action-policy\",\n        \"--confirm-actions\",\n        \"--engine\",\n        \"--screenshot-dir\",\n        \"--screenshot-quality\",\n        \"--screenshot-format\",\n        \"--idle-timeout\",\n    ];\n    let mut i = 0;\n    while i < args.len() {\n        if args[i] == \"--config\" {\n            return Some(args.get(i + 1).cloned());\n        }\n        if FLAGS_WITH_VALUE.contains(&args[i].as_str()) {\n            i += 1;\n        }\n        i += 1;\n    }\n    None\n}\n\npub fn load_config(args: &[String]) -> Result<Config, String> {\n    let explicit = extract_config_path(args)\n        .map(|p| (\"--config\", p))\n        .or_else(|| {\n            env::var(\"AGENT_BROWSER_CONFIG\")\n                .ok()\n                .map(|p| (\"AGENT_BROWSER_CONFIG\", Some(p)))\n        });\n\n    if let Some((source, maybe_path)) = explicit {\n        let path_str = maybe_path.ok_or_else(|| format!(\"{} requires a file path\", source))?;\n        let path = PathBuf::from(&path_str);\n        if !path.exists() {\n            return Err(format!(\"config file not found: {}\", path_str));\n        }\n        return read_config_file(&path)\n            .ok_or_else(|| format!(\"failed to load config from {}\", path_str));\n    }\n\n    let user_config = dirs::home_dir()\n        .map(|d| d.join(CONFIG_DIR).join(CONFIG_FILENAME))\n        .and_then(|p| read_config_file(&p))\n        .unwrap_or_default();\n\n    let project_config = read_config_file(&PathBuf::from(PROJECT_CONFIG_FILENAME));\n\n    Ok(match project_config {\n        Some(project) => user_config.merge(project),\n        None => user_config,\n    })\n}\n\npub struct Flags {\n    pub json: bool,\n    pub headed: bool,\n    pub debug: bool,\n    pub session: String,\n    pub headers: Option<String>,\n    pub executable_path: Option<String>,\n    pub cdp: Option<String>,\n    pub extensions: Vec<String>,\n    pub profile: Option<String>,\n    pub state: Option<String>,\n    pub proxy: Option<String>,\n    pub proxy_bypass: Option<String>,\n    pub args: Option<String>,\n    pub user_agent: Option<String>,\n    pub provider: Option<String>,\n    pub ignore_https_errors: bool,\n    pub allow_file_access: bool,\n    pub device: Option<String>,\n    pub auto_connect: bool,\n    pub session_name: Option<String>,\n    pub annotate: bool,\n    pub color_scheme: Option<String>,\n    pub download_path: Option<String>,\n    pub content_boundaries: bool,\n    pub max_output: Option<usize>,\n    pub allowed_domains: Option<Vec<String>>,\n    pub action_policy: Option<String>,\n    pub confirm_actions: Option<String>,\n    pub confirm_interactive: bool,\n    pub engine: Option<String>,\n    pub screenshot_dir: Option<String>,\n    pub screenshot_quality: Option<u32>,\n    pub screenshot_format: Option<String>,\n    pub idle_timeout: Option<String>, // Canonical milliseconds string for AGENT_BROWSER_IDLE_TIMEOUT_MS\n\n    // Track which launch-time options were explicitly passed via CLI\n    // (as opposed to being set only via environment variables)\n    pub cli_executable_path: bool,\n    pub cli_extensions: bool,\n    pub cli_profile: bool,\n    pub cli_state: bool,\n    pub cli_args: bool,\n    pub cli_user_agent: bool,\n    pub cli_proxy: bool,\n    pub cli_proxy_bypass: bool,\n    pub cli_allow_file_access: bool,\n    pub cli_annotate: bool,\n    pub cli_download_path: bool,\n    pub cli_headed: bool,\n}\n\npub fn parse_flags(args: &[String]) -> Flags {\n    let config = load_config(args).unwrap_or_else(|e| {\n        eprintln!(\"{} {}\", color::warning_indicator(), e);\n        std::process::exit(1);\n    });\n\n    let extensions_env = env::var(\"AGENT_BROWSER_EXTENSIONS\")\n        .ok()\n        .map(|s| {\n            s.split(',')\n                .map(|p| p.trim().to_string())\n                .filter(|p| !p.is_empty())\n                .collect::<Vec<_>>()\n        })\n        .unwrap_or_default();\n\n    let extensions = if !extensions_env.is_empty() {\n        extensions_env\n    } else {\n        config.extensions.unwrap_or_default()\n    };\n\n    let mut flags = Flags {\n        json: env_var_is_truthy(\"AGENT_BROWSER_JSON\") || config.json.unwrap_or(false),\n        headed: env_var_is_truthy(\"AGENT_BROWSER_HEADED\") || config.headed.unwrap_or(false),\n        debug: env_var_is_truthy(\"AGENT_BROWSER_DEBUG\") || config.debug.unwrap_or(false),\n        session: env::var(\"AGENT_BROWSER_SESSION\")\n            .ok()\n            .or(config.session)\n            .unwrap_or_else(|| \"default\".to_string()),\n        headers: config.headers,\n        executable_path: env::var(\"AGENT_BROWSER_EXECUTABLE_PATH\")\n            .ok()\n            .or(config.executable_path),\n        cdp: config.cdp,\n        extensions,\n        profile: env::var(\"AGENT_BROWSER_PROFILE\").ok().or(config.profile),\n        state: env::var(\"AGENT_BROWSER_STATE\").ok().or(config.state),\n        proxy: env::var(\"AGENT_BROWSER_PROXY\").ok().or(config.proxy),\n        proxy_bypass: env::var(\"AGENT_BROWSER_PROXY_BYPASS\")\n            .ok()\n            .or(config.proxy_bypass),\n        args: env::var(\"AGENT_BROWSER_ARGS\").ok().or(config.args),\n        user_agent: env::var(\"AGENT_BROWSER_USER_AGENT\")\n            .ok()\n            .or(config.user_agent),\n        provider: env::var(\"AGENT_BROWSER_PROVIDER\").ok().or(config.provider),\n        ignore_https_errors: env_var_is_truthy(\"AGENT_BROWSER_IGNORE_HTTPS_ERRORS\")\n            || config.ignore_https_errors.unwrap_or(false),\n        allow_file_access: env_var_is_truthy(\"AGENT_BROWSER_ALLOW_FILE_ACCESS\")\n            || config.allow_file_access.unwrap_or(false),\n        device: env::var(\"AGENT_BROWSER_IOS_DEVICE\").ok().or(config.device),\n        auto_connect: env_var_is_truthy(\"AGENT_BROWSER_AUTO_CONNECT\")\n            || config.auto_connect.unwrap_or(false),\n        session_name: env::var(\"AGENT_BROWSER_SESSION_NAME\")\n            .ok()\n            .or(config.session_name),\n        annotate: env_var_is_truthy(\"AGENT_BROWSER_ANNOTATE\") || config.annotate.unwrap_or(false),\n        color_scheme: env::var(\"AGENT_BROWSER_COLOR_SCHEME\")\n            .ok()\n            .or(config.color_scheme),\n        download_path: env::var(\"AGENT_BROWSER_DOWNLOAD_PATH\")\n            .ok()\n            .or(config.download_path),\n        content_boundaries: env_var_is_truthy(\"AGENT_BROWSER_CONTENT_BOUNDARIES\")\n            || config.content_boundaries.unwrap_or(false),\n        max_output: env::var(\"AGENT_BROWSER_MAX_OUTPUT\")\n            .ok()\n            .and_then(|s| s.parse().ok())\n            .or(config.max_output),\n        allowed_domains: env::var(\"AGENT_BROWSER_ALLOWED_DOMAINS\")\n            .ok()\n            .map(|s| {\n                s.split(',')\n                    .map(|d| d.trim().to_lowercase())\n                    .filter(|d| !d.is_empty())\n                    .collect()\n            })\n            .or(config.allowed_domains),\n        action_policy: env::var(\"AGENT_BROWSER_ACTION_POLICY\")\n            .ok()\n            .or(config.action_policy),\n        confirm_actions: env::var(\"AGENT_BROWSER_CONFIRM_ACTIONS\")\n            .ok()\n            .or(config.confirm_actions),\n        confirm_interactive: env_var_is_truthy(\"AGENT_BROWSER_CONFIRM_INTERACTIVE\")\n            || config.confirm_interactive.unwrap_or(false),\n        engine: env::var(\"AGENT_BROWSER_ENGINE\").ok().or(config.engine),\n        screenshot_dir: env::var(\"AGENT_BROWSER_SCREENSHOT_DIR\")\n            .ok()\n            .or(config.screenshot_dir),\n        screenshot_quality: env::var(\"AGENT_BROWSER_SCREENSHOT_QUALITY\")\n            .ok()\n            .and_then(|s| s.parse().ok())\n            .or(config.screenshot_quality),\n        screenshot_format: env::var(\"AGENT_BROWSER_SCREENSHOT_FORMAT\")\n            .ok()\n            .or(config.screenshot_format)\n            .filter(|s| s == \"png\" || s == \"jpeg\"),\n        idle_timeout: parse_idle_timeout_value(\n            env::var(\"AGENT_BROWSER_IDLE_TIMEOUT_MS\").ok(),\n            \"AGENT_BROWSER_IDLE_TIMEOUT_MS\",\n        )\n        .or(config.idle_timeout),\n        cli_executable_path: false,\n        cli_extensions: false,\n        cli_profile: false,\n        cli_state: false,\n        cli_args: false,\n        cli_user_agent: false,\n        cli_proxy: false,\n        cli_proxy_bypass: false,\n        cli_allow_file_access: false,\n        cli_annotate: false,\n        cli_download_path: false,\n        cli_headed: false,\n    };\n\n    let mut i = 0;\n    while i < args.len() {\n        match args[i].as_str() {\n            \"--json\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.json = val;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--headed\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.headed = val;\n                flags.cli_headed = true;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--debug\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.debug = val;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--session\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.session = s.clone();\n                    i += 1;\n                }\n            }\n            \"--idle-timeout\" => {\n                if let Some(s) = args.get(i + 1) {\n                    match parse_idle_timeout(s) {\n                        Ok(ms) => flags.idle_timeout = Some(ms),\n                        Err(e) => eprintln!(\n                            \"{} Invalid --idle-timeout: {}\",\n                            color::warning_indicator(),\n                            e\n                        ),\n                    }\n                    i += 1;\n                }\n            }\n            \"--headers\" => {\n                if let Some(h) = args.get(i + 1) {\n                    flags.headers = Some(h.clone());\n                    i += 1;\n                }\n            }\n            \"--executable-path\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.executable_path = Some(s.clone());\n                    flags.cli_executable_path = true;\n                    i += 1;\n                }\n            }\n            \"--extension\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.extensions.push(s.clone());\n                    flags.cli_extensions = true;\n                    i += 1;\n                }\n            }\n            \"--cdp\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.cdp = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--profile\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.profile = Some(s.clone());\n                    flags.cli_profile = true;\n                    i += 1;\n                }\n            }\n            \"--state\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.state = Some(s.clone());\n                    flags.cli_state = true;\n                    i += 1;\n                }\n            }\n            \"--proxy\" => {\n                if let Some(p) = args.get(i + 1) {\n                    flags.proxy = Some(p.clone());\n                    flags.cli_proxy = true;\n                    i += 1;\n                }\n            }\n            \"--proxy-bypass\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.proxy_bypass = Some(s.clone());\n                    flags.cli_proxy_bypass = true;\n                    i += 1;\n                }\n            }\n            \"--args\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.args = Some(s.clone());\n                    flags.cli_args = true;\n                    i += 1;\n                }\n            }\n            \"--user-agent\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.user_agent = Some(s.clone());\n                    flags.cli_user_agent = true;\n                    i += 1;\n                }\n            }\n            \"-p\" | \"--provider\" => {\n                if let Some(p) = args.get(i + 1) {\n                    flags.provider = Some(p.clone());\n                    i += 1;\n                }\n            }\n            \"--ignore-https-errors\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.ignore_https_errors = val;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--allow-file-access\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.allow_file_access = val;\n                flags.cli_allow_file_access = true;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--device\" => {\n                if let Some(d) = args.get(i + 1) {\n                    flags.device = Some(d.clone());\n                    i += 1;\n                }\n            }\n            \"--auto-connect\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.auto_connect = val;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--session-name\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.session_name = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--annotate\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.annotate = val;\n                flags.cli_annotate = true;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--color-scheme\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.color_scheme = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--download-path\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.download_path = Some(s.clone());\n                    flags.cli_download_path = true;\n                    i += 1;\n                }\n            }\n            \"--content-boundaries\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.content_boundaries = val;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--max-output\" => {\n                if let Some(s) = args.get(i + 1) {\n                    if let Ok(n) = s.parse::<usize>() {\n                        flags.max_output = Some(n);\n                    }\n                    i += 1;\n                }\n            }\n            \"--allowed-domains\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.allowed_domains = Some(\n                        s.split(',')\n                            .map(|d| d.trim().to_lowercase())\n                            .filter(|d| !d.is_empty())\n                            .collect(),\n                    );\n                    i += 1;\n                }\n            }\n            \"--action-policy\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.action_policy = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--confirm-actions\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.confirm_actions = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--confirm-interactive\" => {\n                let (val, consumed) = parse_bool_arg(args, i);\n                flags.confirm_interactive = val;\n                if consumed {\n                    i += 1;\n                }\n            }\n            \"--engine\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.engine = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--screenshot-dir\" => {\n                if let Some(s) = args.get(i + 1) {\n                    flags.screenshot_dir = Some(s.clone());\n                    i += 1;\n                }\n            }\n            \"--screenshot-quality\" => {\n                if let Some(s) = args.get(i + 1) {\n                    if let Ok(n) = s.parse::<u32>() {\n                        if n <= 100 {\n                            flags.screenshot_quality = Some(n);\n                        } else {\n                            eprintln!(\n                                \"{} --screenshot-quality must be 0-100, got {}\",\n                                color::warning_indicator(),\n                                n\n                            );\n                        }\n                    }\n                    i += 1;\n                }\n            }\n            \"--screenshot-format\" => {\n                if let Some(s) = args.get(i + 1) {\n                    if s == \"png\" || s == \"jpeg\" {\n                        flags.screenshot_format = Some(s.clone());\n                    } else {\n                        eprintln!(\n                            \"{} --screenshot-format must be png or jpeg, got '{}'\",\n                            color::warning_indicator(),\n                            s\n                        );\n                    }\n                    i += 1;\n                }\n            }\n            \"--config\" => {\n                // Already handled by load_config(); skip the value\n                i += 1;\n            }\n            _ => {}\n        }\n        i += 1;\n    }\n    flags\n}\n\npub fn clean_args(args: &[String]) -> Vec<String> {\n    let mut result = Vec::new();\n    let mut skip_next = false;\n\n    // Boolean flags that optionally take true/false\n    const GLOBAL_BOOL_FLAGS: &[&str] = &[\n        \"--json\",\n        \"--headed\",\n        \"--debug\",\n        \"--ignore-https-errors\",\n        \"--allow-file-access\",\n        \"--auto-connect\",\n        \"--annotate\",\n        \"--content-boundaries\",\n        \"--confirm-interactive\",\n    ];\n    // Global flags that always take a value (need to skip the next arg too)\n    const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[\n        \"--session\",\n        \"--headers\",\n        \"--executable-path\",\n        \"--cdp\",\n        \"--extension\",\n        \"--profile\",\n        \"--state\",\n        \"--proxy\",\n        \"--proxy-bypass\",\n        \"--args\",\n        \"--user-agent\",\n        \"-p\",\n        \"--provider\",\n        \"--device\",\n        \"--session-name\",\n        \"--color-scheme\",\n        \"--download-path\",\n        \"--max-output\",\n        \"--allowed-domains\",\n        \"--action-policy\",\n        \"--confirm-actions\",\n        \"--config\",\n        \"--engine\",\n        \"--screenshot-dir\",\n        \"--screenshot-quality\",\n        \"--screenshot-format\",\n        \"--idle-timeout\",\n    ];\n\n    let mut i = 0;\n    while i < args.len() {\n        let arg = &args[i];\n        if skip_next {\n            skip_next = false;\n            i += 1;\n            continue;\n        }\n        if GLOBAL_FLAGS_WITH_VALUE.contains(&arg.as_str()) {\n            skip_next = true;\n            i += 1;\n            continue;\n        }\n        if GLOBAL_BOOL_FLAGS.contains(&arg.as_str()) {\n            if let Some(v) = args.get(i + 1) {\n                if matches!(v.as_str(), \"true\" | \"false\") {\n                    i += 1;\n                }\n            }\n            i += 1;\n            continue;\n        }\n        result.push(arg.clone());\n        i += 1;\n    }\n    result\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn args(s: &str) -> Vec<String> {\n        s.split_whitespace().map(String::from).collect()\n    }\n\n    #[test]\n    fn test_parse_headers_flag() {\n        let flags = parse_flags(&args(r#\"open example.com --headers {\"Auth\":\"token\"}\"#));\n        assert_eq!(flags.headers, Some(r#\"{\"Auth\":\"token\"}\"#.to_string()));\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_raw_ms() {\n        assert_eq!(parse_idle_timeout(\"10\").unwrap(), \"10\");\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_seconds() {\n        assert_eq!(parse_idle_timeout(\"10s\").unwrap(), \"10000\");\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_minutes() {\n        assert_eq!(parse_idle_timeout(\"3m\").unwrap(), \"180000\");\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_hours() {\n        assert_eq!(parse_idle_timeout(\"1h\").unwrap(), \"3600000\");\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_rejects_capital_m() {\n        assert!(parse_idle_timeout(\"10M\").is_err());\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_rejects_unknown_unit() {\n        assert!(parse_idle_timeout(\"10x\").is_err());\n    }\n\n    #[test]\n    fn test_parse_headers_flag_with_spaces() {\n        // Headers JSON is passed as a single quoted argument in shell\n        let input: Vec<String> = vec![\n            \"open\".to_string(),\n            \"example.com\".to_string(),\n            \"--headers\".to_string(),\n            r#\"{\"Authorization\": \"Bearer token\"}\"#.to_string(),\n        ];\n        let flags = parse_flags(&input);\n        assert_eq!(\n            flags.headers,\n            Some(r#\"{\"Authorization\": \"Bearer token\"}\"#.to_string())\n        );\n    }\n\n    #[test]\n    fn test_parse_no_headers_flag() {\n        let flags = parse_flags(&args(\"open example.com\"));\n        assert!(flags.headers.is_none());\n    }\n\n    #[test]\n    fn test_clean_args_removes_headers() {\n        let input: Vec<String> = vec![\n            \"open\".to_string(),\n            \"example.com\".to_string(),\n            \"--headers\".to_string(),\n            r#\"{\"Auth\":\"token\"}\"#.to_string(),\n        ];\n        let clean = clean_args(&input);\n        assert_eq!(clean, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_clean_args_removes_headers_at_start() {\n        let input: Vec<String> = vec![\n            \"--headers\".to_string(),\n            r#\"{\"Auth\":\"token\"}\"#.to_string(),\n            \"open\".to_string(),\n            \"example.com\".to_string(),\n        ];\n        let clean = clean_args(&input);\n        assert_eq!(clean, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_headers_with_other_flags() {\n        let input: Vec<String> = vec![\n            \"open\".to_string(),\n            \"example.com\".to_string(),\n            \"--headers\".to_string(),\n            r#\"{\"Auth\":\"token\"}\"#.to_string(),\n            \"--json\".to_string(),\n            \"--headed\".to_string(),\n        ];\n        let flags = parse_flags(&input);\n        assert_eq!(flags.headers, Some(r#\"{\"Auth\":\"token\"}\"#.to_string()));\n        assert!(flags.json);\n        assert!(flags.headed);\n\n        let clean = clean_args(&input);\n        assert_eq!(clean, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_parse_executable_path_flag() {\n        let flags = parse_flags(&args(\n            \"--executable-path /path/to/chromium open example.com\",\n        ));\n        assert_eq!(flags.executable_path, Some(\"/path/to/chromium\".to_string()));\n    }\n\n    #[test]\n    fn test_parse_executable_path_flag_no_value() {\n        let flags = parse_flags(&args(\"--executable-path\"));\n        assert_eq!(flags.executable_path, None);\n    }\n\n    #[test]\n    fn test_clean_args_removes_executable_path() {\n        let cleaned = clean_args(&args(\n            \"--executable-path /path/to/chromium open example.com\",\n        ));\n        assert_eq!(cleaned, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_clean_args_removes_executable_path_with_other_flags() {\n        let cleaned = clean_args(&args(\n            \"--json --executable-path /path/to/chromium --headed open example.com\",\n        ));\n        assert_eq!(cleaned, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_clean_args_removes_idle_timeout_before_command() {\n        let cleaned = clean_args(&args(\"--idle-timeout 10s open example.com\"));\n        assert_eq!(cleaned, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_parse_idle_timeout_flag_converts_to_ms() {\n        let flags = parse_flags(&args(\"--idle-timeout 10s open example.com\"));\n        assert_eq!(flags.idle_timeout.as_deref(), Some(\"10000\"));\n    }\n\n    #[test]\n    fn test_parse_flags_with_session_and_executable_path() {\n        let flags = parse_flags(&args(\n            \"--session test --executable-path /custom/chrome open example.com\",\n        ));\n        assert_eq!(flags.session, \"test\");\n        assert_eq!(flags.executable_path, Some(\"/custom/chrome\".to_string()));\n    }\n\n    #[test]\n    fn test_cli_executable_path_tracking() {\n        // When --executable-path is passed via CLI, cli_executable_path should be true\n        let flags = parse_flags(&args(\"--executable-path /path/to/chrome snapshot\"));\n        assert!(flags.cli_executable_path);\n        assert_eq!(flags.executable_path, Some(\"/path/to/chrome\".to_string()));\n    }\n\n    #[test]\n    fn test_cli_executable_path_not_set_without_flag() {\n        // When no --executable-path is passed, cli_executable_path should be false\n        // (even if env var sets executable_path to Some value, which we can't test here)\n        let flags = parse_flags(&args(\"snapshot\"));\n        assert!(!flags.cli_executable_path);\n    }\n\n    #[test]\n    fn test_cli_extension_tracking() {\n        let flags = parse_flags(&args(\"--extension /path/to/ext snapshot\"));\n        assert!(flags.cli_extensions);\n    }\n\n    #[test]\n    fn test_cli_profile_tracking() {\n        let flags = parse_flags(&args(\"--profile /path/to/profile snapshot\"));\n        assert!(flags.cli_profile);\n    }\n\n    #[test]\n    fn test_cli_annotate_tracking() {\n        let flags = parse_flags(&args(\"--annotate screenshot\"));\n        assert!(flags.cli_annotate);\n        assert!(flags.annotate);\n    }\n\n    #[test]\n    fn test_cli_annotate_not_set_without_flag() {\n        let flags = parse_flags(&args(\"screenshot\"));\n        assert!(!flags.cli_annotate);\n    }\n\n    #[test]\n    fn test_cli_download_path_tracking() {\n        let flags = parse_flags(&args(\"--download-path /tmp/dl snapshot\"));\n        assert!(flags.cli_download_path);\n        assert_eq!(flags.download_path, Some(\"/tmp/dl\".to_string()));\n    }\n\n    #[test]\n    fn test_cli_download_path_not_set_without_flag() {\n        let flags = parse_flags(&args(\"snapshot\"));\n        assert!(!flags.cli_download_path);\n    }\n\n    #[test]\n    fn test_cli_multiple_flags_tracking() {\n        let flags = parse_flags(&args(\n            \"--executable-path /chrome --profile /profile --proxy http://proxy snapshot\",\n        ));\n        assert!(flags.cli_executable_path);\n        assert!(flags.cli_profile);\n        assert!(flags.cli_proxy);\n        assert!(!flags.cli_extensions);\n        assert!(!flags.cli_state);\n    }\n\n    // === Config file tests ===\n\n    #[test]\n    fn test_config_deserialize_full() {\n        let json = r#\"{\n            \"headed\": true,\n            \"json\": true,\n            \"debug\": true,\n            \"session\": \"test-session\",\n            \"sessionName\": \"my-app\",\n            \"executablePath\": \"/usr/bin/chromium\",\n            \"extensions\": [\"/ext1\", \"/ext2\"],\n            \"profile\": \"/tmp/profile\",\n            \"state\": \"/tmp/state.json\",\n            \"proxy\": \"http://proxy:8080\",\n            \"proxyBypass\": \"localhost\",\n            \"args\": \"--no-sandbox\",\n            \"userAgent\": \"test-agent\",\n            \"provider\": \"ios\",\n            \"device\": \"iPhone 15\",\n            \"ignoreHttpsErrors\": true,\n            \"allowFileAccess\": true,\n            \"cdp\": \"9222\",\n            \"autoConnect\": true,\n            \"headers\": \"{\\\"Auth\\\":\\\"token\\\"}\"\n        }\"#;\n        let config: Config = serde_json::from_str(json).unwrap();\n        assert_eq!(config.headed, Some(true));\n        assert_eq!(config.json, Some(true));\n        assert_eq!(config.debug, Some(true));\n        assert_eq!(config.session.as_deref(), Some(\"test-session\"));\n        assert_eq!(config.session_name.as_deref(), Some(\"my-app\"));\n        assert_eq!(config.executable_path.as_deref(), Some(\"/usr/bin/chromium\"));\n        assert_eq!(\n            config.extensions,\n            Some(vec![\"/ext1\".to_string(), \"/ext2\".to_string()])\n        );\n        assert_eq!(config.profile.as_deref(), Some(\"/tmp/profile\"));\n        assert_eq!(config.state.as_deref(), Some(\"/tmp/state.json\"));\n        assert_eq!(config.proxy.as_deref(), Some(\"http://proxy:8080\"));\n        assert_eq!(config.proxy_bypass.as_deref(), Some(\"localhost\"));\n        assert_eq!(config.args.as_deref(), Some(\"--no-sandbox\"));\n        assert_eq!(config.user_agent.as_deref(), Some(\"test-agent\"));\n        assert_eq!(config.provider.as_deref(), Some(\"ios\"));\n        assert_eq!(config.device.as_deref(), Some(\"iPhone 15\"));\n        assert_eq!(config.ignore_https_errors, Some(true));\n        assert_eq!(config.allow_file_access, Some(true));\n        assert_eq!(config.cdp.as_deref(), Some(\"9222\"));\n        assert_eq!(config.auto_connect, Some(true));\n        assert_eq!(config.headers.as_deref(), Some(\"{\\\"Auth\\\":\\\"token\\\"}\"));\n    }\n\n    #[test]\n    fn test_config_deserialize_partial() {\n        let json = r#\"{\"headed\": true, \"proxy\": \"http://localhost:8080\"}\"#;\n        let config: Config = serde_json::from_str(json).unwrap();\n        assert_eq!(config.headed, Some(true));\n        assert_eq!(config.proxy.as_deref(), Some(\"http://localhost:8080\"));\n        assert_eq!(config.session, None);\n        assert_eq!(config.extensions, None);\n        assert_eq!(config.debug, None);\n    }\n\n    #[test]\n    fn test_config_deserialize_empty() {\n        let config: Config = serde_json::from_str(\"{}\").unwrap();\n        assert_eq!(config.headed, None);\n        assert_eq!(config.session, None);\n        assert_eq!(config.proxy, None);\n    }\n\n    #[test]\n    fn test_config_ignores_unknown_keys() {\n        let json = r#\"{\"headed\": true, \"unknownFutureKey\": \"value\", \"anotherOne\": 42}\"#;\n        let config: Config = serde_json::from_str(json).unwrap();\n        assert_eq!(config.headed, Some(true));\n    }\n\n    #[test]\n    fn test_config_merge_project_overrides_user() {\n        let user = Config {\n            headed: Some(true),\n            proxy: Some(\"http://user-proxy:8080\".to_string()),\n            profile: Some(\"/user/profile\".to_string()),\n            ..Config::default()\n        };\n        let project = Config {\n            proxy: Some(\"http://project-proxy:9090\".to_string()),\n            debug: Some(true),\n            ..Config::default()\n        };\n        let merged = user.merge(project);\n        assert_eq!(merged.headed, Some(true)); // kept from user\n        assert_eq!(merged.proxy.as_deref(), Some(\"http://project-proxy:9090\")); // overridden by project\n        assert_eq!(merged.profile.as_deref(), Some(\"/user/profile\")); // kept from user\n        assert_eq!(merged.debug, Some(true)); // added by project\n    }\n\n    #[test]\n    fn test_config_merge_none_does_not_override() {\n        let user = Config {\n            headed: Some(true),\n            proxy: Some(\"http://proxy:8080\".to_string()),\n            ..Config::default()\n        };\n        let project = Config::default();\n        let merged = user.merge(project);\n        assert_eq!(merged.headed, Some(true));\n        assert_eq!(merged.proxy.as_deref(), Some(\"http://proxy:8080\"));\n    }\n\n    #[test]\n    fn test_load_config_from_file() {\n        use std::io::Write;\n        let dir = std::env::temp_dir().join(\"ab-test-config\");\n        let _ = fs::create_dir_all(&dir);\n        let config_path = dir.join(\"test-config.json\");\n        let mut f = fs::File::create(&config_path).unwrap();\n        writeln!(f, r#\"{{\"headed\": true, \"proxy\": \"http://test:1234\"}}\"#).unwrap();\n\n        let config = read_config_file(&config_path).unwrap();\n        assert_eq!(config.headed, Some(true));\n        assert_eq!(config.proxy.as_deref(), Some(\"http://test:1234\"));\n\n        let _ = fs::remove_file(&config_path);\n        let _ = fs::remove_dir(&dir);\n    }\n\n    #[test]\n    fn test_load_config_from_file_parses_idle_timeout() {\n        use std::io::Write;\n        let dir = std::env::temp_dir().join(\"ab-test-idle-timeout-config\");\n        let _ = fs::create_dir_all(&dir);\n        let config_path = dir.join(\"test-config.json\");\n        let mut f = fs::File::create(&config_path).unwrap();\n        writeln!(f, r#\"{{\"idleTimeout\": \"10s\"}}\"#).unwrap();\n\n        let config = read_config_file(&config_path).unwrap();\n        assert_eq!(config.idle_timeout.as_deref(), Some(\"10000\"));\n\n        let _ = fs::remove_file(&config_path);\n        let _ = fs::remove_dir(&dir);\n    }\n\n    #[test]\n    fn test_load_config_missing_file_returns_none() {\n        let result = read_config_file(&PathBuf::from(\"/nonexistent/agent-browser.json\"));\n        assert!(result.is_none());\n    }\n\n    #[test]\n    fn test_load_config_malformed_json_returns_none() {\n        use std::io::Write;\n        let dir = std::env::temp_dir().join(\"ab-test-malformed\");\n        let _ = fs::create_dir_all(&dir);\n        let config_path = dir.join(\"bad-config.json\");\n        let mut f = fs::File::create(&config_path).unwrap();\n        writeln!(f, \"{{not valid json}}\").unwrap();\n\n        let result = read_config_file(&config_path);\n        assert!(result.is_none());\n\n        let _ = fs::remove_file(&config_path);\n        let _ = fs::remove_dir(&dir);\n    }\n\n    #[test]\n    fn test_extract_config_path() {\n        assert_eq!(\n            extract_config_path(&args(\"--config ./my-config.json open example.com\")),\n            Some(Some(\"./my-config.json\".to_string()))\n        );\n    }\n\n    #[test]\n    fn test_extract_config_path_missing() {\n        assert_eq!(extract_config_path(&args(\"open example.com\")), None);\n    }\n\n    #[test]\n    fn test_extract_config_path_no_value() {\n        assert_eq!(extract_config_path(&args(\"--config\")), Some(None));\n    }\n\n    #[test]\n    fn test_extract_config_path_skips_flag_values() {\n        assert_eq!(extract_config_path(&args(\"--args --config open\")), None);\n    }\n\n    #[test]\n    fn test_clean_args_removes_config() {\n        let cleaned = clean_args(&args(\"--config ./config.json open example.com\"));\n        assert_eq!(cleaned, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_load_config_with_config_flag() {\n        use std::io::Write;\n        let dir = std::env::temp_dir().join(\"ab-test-flag-config\");\n        let _ = fs::create_dir_all(&dir);\n        let config_path = dir.join(\"custom.json\");\n        let mut f = fs::File::create(&config_path).unwrap();\n        writeln!(f, r#\"{{\"headed\": true, \"session\": \"custom\"}}\"#).unwrap();\n\n        let flag_args = vec![\n            \"--config\".to_string(),\n            config_path.to_string_lossy().to_string(),\n            \"open\".to_string(),\n            \"example.com\".to_string(),\n        ];\n        let config = load_config(&flag_args).unwrap();\n        assert_eq!(config.headed, Some(true));\n        assert_eq!(config.session.as_deref(), Some(\"custom\"));\n\n        let _ = fs::remove_file(&config_path);\n        let _ = fs::remove_dir(&dir);\n    }\n\n    #[test]\n    fn test_load_config_error_missing_config_value() {\n        let result = load_config(&args(\"--config\"));\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"requires a file path\"));\n    }\n\n    #[test]\n    fn test_load_config_error_nonexistent_file() {\n        let result = load_config(&args(\"--config /nonexistent/config.json open\"));\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"config file not found\"));\n    }\n\n    #[test]\n    fn test_load_config_error_malformed_explicit() {\n        use std::io::Write;\n        let dir = std::env::temp_dir().join(\"ab-test-explicit-malformed\");\n        let _ = fs::create_dir_all(&dir);\n        let config_path = dir.join(\"bad.json\");\n        let mut f = fs::File::create(&config_path).unwrap();\n        writeln!(f, \"{{not valid}}\").unwrap();\n\n        let flag_args = vec![\n            \"--config\".to_string(),\n            config_path.to_string_lossy().to_string(),\n        ];\n        let result = load_config(&flag_args);\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"failed to load config\"));\n\n        let _ = fs::remove_file(&config_path);\n        let _ = fs::remove_dir(&dir);\n    }\n\n    // === Boolean flag value tests ===\n\n    #[test]\n    fn test_headed_false() {\n        let flags = parse_flags(&args(\"--headed false open example.com\"));\n        assert!(!flags.headed);\n    }\n\n    #[test]\n    fn test_headed_true_explicit() {\n        let flags = parse_flags(&args(\"--headed true open example.com\"));\n        assert!(flags.headed);\n    }\n\n    #[test]\n    fn test_headed_bare_defaults_true() {\n        let flags = parse_flags(&args(\"--headed open example.com\"));\n        assert!(flags.headed);\n    }\n\n    #[test]\n    fn test_debug_false() {\n        let flags = parse_flags(&args(\"--debug false open example.com\"));\n        assert!(!flags.debug);\n    }\n\n    #[test]\n    fn test_json_false() {\n        let flags = parse_flags(&args(\"--json false open example.com\"));\n        assert!(!flags.json);\n    }\n\n    #[test]\n    fn test_ignore_https_errors_false() {\n        let flags = parse_flags(&args(\"--ignore-https-errors false open\"));\n        assert!(!flags.ignore_https_errors);\n    }\n\n    #[test]\n    fn test_allow_file_access_false() {\n        let flags = parse_flags(&args(\"--allow-file-access false open\"));\n        assert!(!flags.allow_file_access);\n        assert!(flags.cli_allow_file_access);\n    }\n\n    #[test]\n    fn test_auto_connect_false() {\n        let flags = parse_flags(&args(\"--auto-connect false open\"));\n        assert!(!flags.auto_connect);\n    }\n\n    #[test]\n    fn test_clean_args_removes_bool_flag_with_value() {\n        let cleaned = clean_args(&args(\"--headed false --debug true open example.com\"));\n        assert_eq!(cleaned, vec![\"open\", \"example.com\"]);\n    }\n\n    #[test]\n    fn test_clean_args_removes_bare_bool_flag() {\n        let cleaned = clean_args(&args(\"--headed --debug open example.com\"));\n        assert_eq!(cleaned, vec![\"open\", \"example.com\"]);\n    }\n\n    // === Extensions merge tests ===\n\n    #[test]\n    fn test_config_merge_extensions_concatenated() {\n        let user = Config {\n            extensions: Some(vec![\"/ext1\".to_string()]),\n            ..Config::default()\n        };\n        let project = Config {\n            extensions: Some(vec![\"/ext2\".to_string(), \"/ext3\".to_string()]),\n            ..Config::default()\n        };\n        let merged = user.merge(project);\n        assert_eq!(\n            merged.extensions,\n            Some(vec![\n                \"/ext1\".to_string(),\n                \"/ext2\".to_string(),\n                \"/ext3\".to_string()\n            ])\n        );\n    }\n\n    #[test]\n    fn test_config_merge_extensions_user_only() {\n        let user = Config {\n            extensions: Some(vec![\"/ext1\".to_string()]),\n            ..Config::default()\n        };\n        let project = Config::default();\n        let merged = user.merge(project);\n        assert_eq!(merged.extensions, Some(vec![\"/ext1\".to_string()]));\n    }\n\n    #[test]\n    fn test_config_merge_extensions_project_only() {\n        let user = Config::default();\n        let project = Config {\n            extensions: Some(vec![\"/ext2\".to_string()]),\n            ..Config::default()\n        };\n        let merged = user.merge(project);\n        assert_eq!(merged.extensions, Some(vec![\"/ext2\".to_string()]));\n    }\n}\n"
  },
  {
    "path": "cli/src/install.rs",
    "content": "use crate::color;\nuse std::fs;\nuse std::io::{self, Write};\nuse std::path::{Path, PathBuf};\nuse std::process::{exit, Command, Stdio};\n\nconst LAST_KNOWN_GOOD_URL: &str =\n    \"https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json\";\n\npub fn get_browsers_dir() -> PathBuf {\n    dirs::home_dir()\n        .unwrap_or_else(|| PathBuf::from(\".\"))\n        .join(\".agent-browser\")\n        .join(\"browsers\")\n}\n\npub fn find_installed_chrome() -> Option<PathBuf> {\n    let browsers_dir = get_browsers_dir();\n    if !browsers_dir.exists() {\n        return None;\n    }\n\n    let mut versions: Vec<_> = fs::read_dir(&browsers_dir)\n        .ok()?\n        .filter_map(|e| e.ok())\n        .filter(|e| {\n            e.file_name()\n                .to_str()\n                .is_some_and(|n| n.starts_with(\"chrome-\"))\n        })\n        .collect();\n\n    versions.sort_by_key(|b| std::cmp::Reverse(b.file_name()));\n\n    for entry in versions {\n        if let Some(bin) = chrome_binary_in_dir(&entry.path()) {\n            if bin.exists() {\n                return Some(bin);\n            }\n        }\n    }\n\n    None\n}\n\nfn chrome_binary_in_dir(dir: &Path) -> Option<PathBuf> {\n    #[cfg(target_os = \"macos\")]\n    {\n        let app =\n            dir.join(\"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing\");\n        if app.exists() {\n            return Some(app);\n        }\n        let inner = dir.join(\"chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing\");\n        if inner.exists() {\n            return Some(inner);\n        }\n        let inner_x64 = dir.join(\n            \"chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing\",\n        );\n        if inner_x64.exists() {\n            return Some(inner_x64);\n        }\n        None\n    }\n\n    #[cfg(target_os = \"linux\")]\n    {\n        let bin = dir.join(\"chrome\");\n        if bin.exists() {\n            return Some(bin);\n        }\n        let inner = dir.join(\"chrome-linux64/chrome\");\n        if inner.exists() {\n            return Some(inner);\n        }\n        None\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        let bin = dir.join(\"chrome.exe\");\n        if bin.exists() {\n            return Some(bin);\n        }\n        let inner = dir.join(\"chrome-win64/chrome.exe\");\n        if inner.exists() {\n            return Some(inner);\n        }\n        None\n    }\n\n    #[cfg(not(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\")))]\n    {\n        None\n    }\n}\n\nfn platform_key() -> &'static str {\n    #[cfg(all(target_os = \"macos\", target_arch = \"aarch64\"))]\n    {\n        \"mac-arm64\"\n    }\n    #[cfg(all(target_os = \"macos\", target_arch = \"x86_64\"))]\n    {\n        \"mac-x64\"\n    }\n    #[cfg(all(target_os = \"linux\", target_arch = \"x86_64\"))]\n    {\n        \"linux64\"\n    }\n    #[cfg(all(target_os = \"windows\", target_arch = \"x86_64\"))]\n    {\n        \"win64\"\n    }\n    #[cfg(not(any(\n        all(target_os = \"macos\", target_arch = \"aarch64\"),\n        all(target_os = \"macos\", target_arch = \"x86_64\"),\n        all(target_os = \"linux\", target_arch = \"x86_64\"),\n        all(target_os = \"windows\", target_arch = \"x86_64\"),\n    )))]\n    {\n        // Compiles on unsupported platforms (e.g. linux aarch64) so the binary\n        // can still be used for other commands like `connect`. The install path\n        // guards against this at runtime before calling platform_key().\n        panic!(\"Unsupported platform for Chrome for Testing download\")\n    }\n}\n\nasync fn fetch_download_url() -> Result<(String, String), String> {\n    let resp = reqwest::get(LAST_KNOWN_GOOD_URL)\n        .await\n        .map_err(|e| format!(\"Failed to fetch version info: {}\", e))?;\n\n    let body: serde_json::Value = resp\n        .json()\n        .await\n        .map_err(|e| format!(\"Failed to parse version info: {}\", e))?;\n\n    let channel = body\n        .get(\"channels\")\n        .and_then(|c| c.get(\"Stable\"))\n        .ok_or(\"No Stable channel found in version info\")?;\n\n    let version = channel\n        .get(\"version\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"No version string found\")?\n        .to_string();\n\n    let platform = platform_key();\n\n    let url = channel\n        .get(\"downloads\")\n        .and_then(|d| d.get(\"chrome\"))\n        .and_then(|c| c.as_array())\n        .and_then(|arr| {\n            arr.iter().find_map(|entry| {\n                if entry.get(\"platform\")?.as_str()? == platform {\n                    Some(entry.get(\"url\")?.as_str()?.to_string())\n                } else {\n                    None\n                }\n            })\n        })\n        .ok_or_else(|| format!(\"No download URL found for platform: {}\", platform))?;\n\n    Ok((version, url))\n}\n\nasync fn download_bytes(url: &str) -> Result<Vec<u8>, String> {\n    let resp = reqwest::get(url)\n        .await\n        .map_err(|e| format!(\"Download failed: {}\", e))?;\n\n    let total = resp.content_length();\n    let mut bytes = Vec::new();\n    let mut stream = resp;\n    let mut downloaded: u64 = 0;\n    let mut last_pct: u64 = 0;\n\n    loop {\n        let chunk = stream\n            .chunk()\n            .await\n            .map_err(|e| format!(\"Download error: {}\", e))?;\n        match chunk {\n            Some(data) => {\n                downloaded += data.len() as u64;\n                bytes.extend_from_slice(&data);\n\n                if let Some(total) = total {\n                    let pct = (downloaded * 100) / total;\n                    if pct >= last_pct + 5 {\n                        last_pct = pct;\n                        let mb = downloaded as f64 / 1_048_576.0;\n                        let total_mb = total as f64 / 1_048_576.0;\n                        eprint!(\"\\r  {:.0}/{:.0} MB ({pct}%)\", mb, total_mb);\n                        let _ = io::stderr().flush();\n                    }\n                }\n            }\n            None => break,\n        }\n    }\n\n    eprintln!();\n    Ok(bytes)\n}\n\nfn extract_zip(bytes: Vec<u8>, dest: &Path) -> Result<(), String> {\n    fs::create_dir_all(dest).map_err(|e| format!(\"Failed to create directory: {}\", e))?;\n\n    let cursor = io::Cursor::new(bytes);\n    let mut archive =\n        zip::ZipArchive::new(cursor).map_err(|e| format!(\"Failed to read zip archive: {}\", e))?;\n\n    for i in 0..archive.len() {\n        let mut file = archive\n            .by_index(i)\n            .map_err(|e| format!(\"Failed to read zip entry: {}\", e))?;\n\n        let enclosed = match file.enclosed_name() {\n            Some(name) => name.to_owned(),\n            None => continue,\n        };\n        let raw_name = enclosed.to_string_lossy().to_string();\n        let rel_path = raw_name\n            .strip_prefix(\"chrome-\")\n            .and_then(|s| s.split_once('/'))\n            .map(|(_, rest)| rest.to_string())\n            .unwrap_or(raw_name.clone());\n\n        if rel_path.is_empty() {\n            continue;\n        }\n\n        let out_path = dest.join(&rel_path);\n\n        // Defense-in-depth: ensure the resolved path is inside dest\n        if !out_path.starts_with(dest) {\n            continue;\n        }\n\n        if file.is_dir() {\n            fs::create_dir_all(&out_path)\n                .map_err(|e| format!(\"Failed to create dir {}: {}\", out_path.display(), e))?;\n        } else {\n            if let Some(parent) = out_path.parent() {\n                fs::create_dir_all(parent).map_err(|e| {\n                    format!(\"Failed to create parent dir {}: {}\", parent.display(), e)\n                })?;\n            }\n            let mut out_file = fs::File::create(&out_path)\n                .map_err(|e| format!(\"Failed to create file {}: {}\", out_path.display(), e))?;\n            io::copy(&mut file, &mut out_file)\n                .map_err(|e| format!(\"Failed to write {}: {}\", out_path.display(), e))?;\n\n            #[cfg(unix)]\n            {\n                use std::os::unix::fs::PermissionsExt;\n                if let Some(mode) = file.unix_mode() {\n                    let _ = fs::set_permissions(&out_path, fs::Permissions::from_mode(mode));\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\npub fn run_install(with_deps: bool) {\n    if cfg!(all(target_os = \"linux\", target_arch = \"aarch64\")) {\n        eprintln!(\n            \"{} Chrome for Testing does not provide Linux ARM64 builds.\",\n            color::error_indicator()\n        );\n        eprintln!(\"  Install Chromium from your system package manager instead:\");\n        eprintln!(\"    sudo apt install chromium-browser   # Debian/Ubuntu\");\n        eprintln!(\"    sudo dnf install chromium            # Fedora\");\n        eprintln!(\"  Then use: agent-browser --executable-path /usr/bin/chromium\");\n        exit(1);\n    }\n\n    let is_linux = cfg!(target_os = \"linux\");\n\n    if is_linux {\n        if with_deps {\n            install_linux_deps();\n        } else {\n            println!(\n                \"{} Linux detected. If browser fails to launch, run:\",\n                color::warning_indicator()\n            );\n            println!(\"  agent-browser install --with-deps\");\n            println!();\n        }\n    }\n\n    println!(\"{}\", color::cyan(\"Installing Chrome...\"));\n\n    let rt = tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()\n        .unwrap_or_else(|e| {\n            eprintln!(\n                \"{} Failed to create runtime: {}\",\n                color::error_indicator(),\n                e\n            );\n            exit(1);\n        });\n\n    let (version, url) = match rt.block_on(fetch_download_url()) {\n        Ok(v) => v,\n        Err(e) => {\n            eprintln!(\"{} {}\", color::error_indicator(), e);\n            exit(1);\n        }\n    };\n\n    let dest = get_browsers_dir().join(format!(\"chrome-{}\", version));\n\n    if let Some(bin) = chrome_binary_in_dir(&dest) {\n        if bin.exists() {\n            println!(\n                \"{} Chrome {} is already installed\",\n                color::success_indicator(),\n                version\n            );\n            return;\n        }\n    }\n\n    println!(\"  Downloading Chrome {} for {}\", version, platform_key());\n    println!(\"  {}\", url);\n\n    let bytes = match rt.block_on(download_bytes(&url)) {\n        Ok(b) => b,\n        Err(e) => {\n            eprintln!(\"{} {}\", color::error_indicator(), e);\n            exit(1);\n        }\n    };\n\n    match extract_zip(bytes, &dest) {\n        Ok(()) => {\n            println!(\n                \"{} Chrome {} installed successfully\",\n                color::success_indicator(),\n                version\n            );\n            println!(\"  Location: {}\", dest.display());\n\n            if is_linux && !with_deps {\n                println!();\n                println!(\n                    \"{} If you see \\\"shared library\\\" errors when running, use:\",\n                    color::yellow(\"Note:\")\n                );\n                println!(\"  agent-browser install --with-deps\");\n            }\n        }\n        Err(e) => {\n            let _ = fs::remove_dir_all(&dest);\n            eprintln!(\"{} {}\", color::error_indicator(), e);\n            exit(1);\n        }\n    }\n}\n\nfn report_install_status(status: io::Result<std::process::ExitStatus>) {\n    match status {\n        Ok(s) if s.success() => {\n            println!(\n                \"{} System dependencies installed\",\n                color::success_indicator()\n            )\n        }\n        Ok(_) => eprintln!(\n            \"{} Failed to install some dependencies. You may need to run manually with sudo.\",\n            color::warning_indicator()\n        ),\n        Err(e) => eprintln!(\n            \"{} Could not run install command: {}\",\n            color::warning_indicator(),\n            e\n        ),\n    }\n}\n\nfn install_linux_deps() {\n    println!(\"{}\", color::cyan(\"Installing system dependencies...\"));\n\n    let (pkg_mgr, deps) = if which_exists(\"apt-get\") {\n        // On Ubuntu 24.04+, many libraries were renamed with a t64 suffix as\n        // part of the 64-bit time_t transition. Using the old names can cause\n        // apt to propose removing hundreds of system packages to resolve\n        // conflicts. We check for the t64 variant first to avoid this.\n        let apt_deps: Vec<&str> = vec![\n            (\"libxcb-shm0\", None),\n            (\"libx11-xcb1\", None),\n            (\"libx11-6\", None),\n            (\"libxcb1\", None),\n            (\"libxext6\", None),\n            (\"libxrandr2\", None),\n            (\"libxcomposite1\", None),\n            (\"libxcursor1\", None),\n            (\"libxdamage1\", None),\n            (\"libxfixes3\", None),\n            (\"libxi6\", None),\n            (\"libgtk-3-0\", Some(\"libgtk-3-0t64\")),\n            (\"libpangocairo-1.0-0\", Some(\"libpangocairo-1.0-0t64\")),\n            (\"libpango-1.0-0\", Some(\"libpango-1.0-0t64\")),\n            (\"libatk1.0-0\", Some(\"libatk1.0-0t64\")),\n            (\"libcairo-gobject2\", Some(\"libcairo-gobject2t64\")),\n            (\"libcairo2\", Some(\"libcairo2t64\")),\n            (\"libgdk-pixbuf-2.0-0\", Some(\"libgdk-pixbuf-2.0-0t64\")),\n            (\"libxrender1\", None),\n            (\"libasound2\", Some(\"libasound2t64\")),\n            (\"libfreetype6\", None),\n            (\"libfontconfig1\", None),\n            (\"libdbus-1-3\", Some(\"libdbus-1-3t64\")),\n            (\"libnss3\", None),\n            (\"libnspr4\", None),\n            (\"libatk-bridge2.0-0\", Some(\"libatk-bridge2.0-0t64\")),\n            (\"libdrm2\", None),\n            (\"libxkbcommon0\", None),\n            (\"libatspi2.0-0\", Some(\"libatspi2.0-0t64\")),\n            (\"libcups2\", Some(\"libcups2t64\")),\n            (\"libxshmfence1\", None),\n            (\"libgbm1\", None),\n        ]\n        .into_iter()\n        .map(|(base, t64_variant)| {\n            if let Some(t64) = t64_variant {\n                if package_exists_apt(t64) {\n                    return t64;\n                }\n            }\n            base\n        })\n        .collect();\n\n        (\"apt-get\", apt_deps)\n    } else if which_exists(\"dnf\") {\n        (\n            \"dnf\",\n            vec![\n                \"nss\",\n                \"nspr\",\n                \"atk\",\n                \"at-spi2-atk\",\n                \"cups-libs\",\n                \"libdrm\",\n                \"libXcomposite\",\n                \"libXdamage\",\n                \"libXrandr\",\n                \"mesa-libgbm\",\n                \"pango\",\n                \"alsa-lib\",\n                \"libxkbcommon\",\n                \"libxcb\",\n                \"libX11-xcb\",\n                \"libX11\",\n                \"libXext\",\n                \"libXcursor\",\n                \"libXfixes\",\n                \"libXi\",\n                \"gtk3\",\n                \"cairo-gobject\",\n            ],\n        )\n    } else if which_exists(\"yum\") {\n        (\n            \"yum\",\n            vec![\n                \"nss\",\n                \"nspr\",\n                \"atk\",\n                \"at-spi2-atk\",\n                \"cups-libs\",\n                \"libdrm\",\n                \"libXcomposite\",\n                \"libXdamage\",\n                \"libXrandr\",\n                \"mesa-libgbm\",\n                \"pango\",\n                \"alsa-lib\",\n                \"libxkbcommon\",\n            ],\n        )\n    } else {\n        eprintln!(\n            \"{} No supported package manager found (apt-get, dnf, or yum)\",\n            color::error_indicator()\n        );\n        exit(1);\n    };\n\n    if pkg_mgr == \"apt-get\" {\n        // Run apt-get update first\n        println!(\"Running: sudo apt-get update\");\n        let update_status = Command::new(\"sudo\").args([\"apt-get\", \"update\"]).status();\n\n        match update_status {\n            Ok(s) if !s.success() => {\n                eprintln!(\n                    \"{} apt-get update failed. Continuing with existing package lists.\",\n                    color::warning_indicator()\n                );\n            }\n            Err(e) => {\n                eprintln!(\n                    \"{} Could not run apt-get update: {}\",\n                    color::warning_indicator(),\n                    e\n                );\n            }\n            _ => {}\n        }\n\n        // Simulate the install first to detect if apt would remove any\n        // packages. This prevents the catastrophic scenario where installing\n        // these libraries triggers removal of hundreds of system packages\n        // due to dependency conflicts (e.g. on Ubuntu 24.04 with the\n        // t64 transition).\n        println!(\"Checking for conflicts...\");\n        let sim_output = Command::new(\"sudo\")\n            .args([\"apt-get\", \"install\", \"--simulate\"])\n            .args(&deps)\n            .output();\n\n        match sim_output {\n            Ok(output) => {\n                let stdout = String::from_utf8_lossy(&output.stdout);\n                let stderr = String::from_utf8_lossy(&output.stderr);\n                let combined = format!(\"{}\\n{}\", stdout, stderr);\n\n                // Count packages that would be removed\n                let removals: Vec<&str> = combined\n                    .lines()\n                    .filter(|line| line.starts_with(\"Remv \"))\n                    .collect();\n\n                if !removals.is_empty() {\n                    eprintln!(\n                        \"{} Aborting: apt would remove {} package(s) to install these dependencies.\",\n                        color::error_indicator(),\n                        removals.len()\n                    );\n                    eprintln!(\n                        \"  This usually means some package names have changed on your system\"\n                    );\n                    eprintln!(\"  (e.g. Ubuntu 24.04 renamed libraries with a t64 suffix).\");\n                    eprintln!();\n                    eprintln!(\"  Packages that would be removed:\");\n                    for line in removals.iter().take(20) {\n                        eprintln!(\"    {}\", line);\n                    }\n                    if removals.len() > 20 {\n                        eprintln!(\"    ... and {} more\", removals.len() - 20);\n                    }\n                    eprintln!();\n                    eprintln!(\"  To install dependencies manually, run:\");\n                    eprintln!(\"    sudo apt-get install {}\", deps.join(\" \"));\n                    eprintln!();\n                    eprintln!(\"  Review the apt output carefully before confirming.\");\n                    exit(1);\n                }\n            }\n            Err(e) => {\n                eprintln!(\n                    \"{} Could not simulate install ({}). Proceeding with caution.\",\n                    color::warning_indicator(),\n                    e\n                );\n            }\n        }\n\n        // Safe to proceed: no removals detected\n        let install_cmd = format!(\"sudo apt-get install -y {}\", deps.join(\" \"));\n        println!(\"Running: {}\", install_cmd);\n        let status = Command::new(\"sudo\")\n            .args([\"apt-get\", \"install\", \"-y\"])\n            .args(&deps)\n            .status();\n\n        report_install_status(status);\n    } else {\n        // dnf / yum path — these package managers do not remove packages\n        // during install, so the simulate-first guard is not needed.\n        let install_cmd = format!(\"sudo {} install -y {}\", pkg_mgr, deps.join(\" \"));\n        println!(\"Running: {}\", install_cmd);\n        let status = Command::new(\"sh\").arg(\"-c\").arg(&install_cmd).status();\n\n        report_install_status(status);\n    }\n}\n\nfn which_exists(cmd: &str) -> bool {\n    #[cfg(unix)]\n    {\n        Command::new(\"which\")\n            .arg(cmd)\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .status()\n            .map(|s| s.success())\n            .unwrap_or(false)\n    }\n    #[cfg(windows)]\n    {\n        Command::new(\"where\")\n            .arg(cmd)\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .status()\n            .map(|s| s.success())\n            .unwrap_or(false)\n    }\n}\n\nfn package_exists_apt(pkg: &str) -> bool {\n    Command::new(\"apt-cache\")\n        .arg(\"show\")\n        .arg(pkg)\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false)\n}\n"
  },
  {
    "path": "cli/src/main.rs",
    "content": "mod color;\nmod commands;\nmod connection;\nmod flags;\nmod install;\nmod native;\nmod output;\n#[cfg(test)]\nmod test_utils;\nmod upgrade;\nmod validation;\n\nuse serde_json::json;\nuse std::env;\nuse std::fs;\nuse std::process::exit;\n\n#[cfg(windows)]\nuse windows_sys::Win32::Foundation::CloseHandle;\n#[cfg(windows)]\nuse windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};\n\nuse commands::{gen_id, parse_command, ParseError};\nuse connection::{ensure_daemon, get_socket_dir, send_command, DaemonOptions};\nuse flags::{clean_args, parse_flags, Flags};\nuse install::run_install;\nuse output::{\n    print_command_help, print_help, print_response_with_opts, print_version, OutputOptions,\n};\nuse upgrade::run_upgrade;\n\nfn serialize_json_value(value: &serde_json::Value) -> String {\n    serde_json::to_string(value).unwrap_or_else(|_| {\n        r#\"{\"success\":false,\"error\":\"Failed to serialize JSON response\"}\"#.to_string()\n    })\n}\n\nfn print_json_value(value: serde_json::Value) {\n    println!(\"{}\", serialize_json_value(&value));\n}\n\nfn print_json_error(message: impl AsRef<str>) {\n    print_json_value(json!({\n        \"success\": false,\n        \"error\": message.as_ref(),\n    }));\n}\n\nfn print_json_error_with_type(message: impl AsRef<str>, error_type: &str) {\n    print_json_value(json!({\n        \"success\": false,\n        \"error\": message.as_ref(),\n        \"type\": error_type,\n    }));\n}\n\nfn parse_proxy(proxy_str: &str) -> serde_json::Value {\n    let Some(protocol_end) = proxy_str.find(\"://\") else {\n        return json!({ \"server\": proxy_str });\n    };\n    let protocol = &proxy_str[..protocol_end + 3];\n    let rest = &proxy_str[protocol_end + 3..];\n\n    let Some(at_pos) = rest.rfind('@') else {\n        return json!({ \"server\": proxy_str });\n    };\n\n    let creds = &rest[..at_pos];\n    let server_part = &rest[at_pos + 1..];\n    let server = format!(\"{}{}\", protocol, server_part);\n\n    let Some(colon_pos) = creds.find(':') else {\n        return json!({\n            \"server\": server,\n            \"username\": creds,\n            \"password\": \"\"\n        });\n    };\n\n    json!({\n        \"server\": server,\n        \"username\": &creds[..colon_pos],\n        \"password\": &creds[colon_pos + 1..]\n    })\n}\n\nfn run_session(args: &[String], session: &str, json_mode: bool) {\n    let subcommand = args.get(1).map(|s| s.as_str());\n\n    match subcommand {\n        Some(\"list\") => {\n            let socket_dir = get_socket_dir();\n            let mut sessions: Vec<String> = Vec::new();\n\n            if let Ok(entries) = fs::read_dir(&socket_dir) {\n                for entry in entries.flatten() {\n                    let name = entry.file_name().to_string_lossy().to_string();\n                    // Look for pid files in socket directory\n                    if name.ends_with(\".pid\") {\n                        let session_name = name.strip_suffix(\".pid\").unwrap_or(\"\");\n                        if !session_name.is_empty() {\n                            // Check if session is actually running\n                            let pid_path = socket_dir.join(&name);\n                            if let Ok(pid_str) = fs::read_to_string(&pid_path) {\n                                if let Ok(pid) = pid_str.trim().parse::<u32>() {\n                                    #[cfg(unix)]\n                                    let running = unsafe {\n                                        libc::kill(pid as i32, 0) == 0\n                                            || std::io::Error::last_os_error().raw_os_error()\n                                                != Some(libc::ESRCH)\n                                    };\n                                    #[cfg(windows)]\n                                    let running = unsafe {\n                                        let handle =\n                                            OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);\n                                        if handle != 0 {\n                                            CloseHandle(handle);\n                                            true\n                                        } else {\n                                            false\n                                        }\n                                    };\n                                    if running {\n                                        sessions.push(session_name.to_string());\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            if json_mode {\n                println!(\n                    r#\"{{\"success\":true,\"data\":{{\"sessions\":{}}}}}\"#,\n                    serde_json::to_string(&sessions).unwrap_or_default()\n                );\n            } else if sessions.is_empty() {\n                println!(\"No active sessions\");\n            } else {\n                println!(\"Active sessions:\");\n                for s in &sessions {\n                    let marker = if s == session {\n                        color::cyan(\"→\")\n                    } else {\n                        \" \".to_string()\n                    };\n                    println!(\"{} {}\", marker, s);\n                }\n            }\n        }\n        None | Some(_) => {\n            // Just show current session\n            if json_mode {\n                print_json_value(json!({\n                    \"success\": true,\n                    \"data\": {\n                        \"session\": session,\n                    },\n                }));\n            } else {\n                println!(\"{}\", session);\n            }\n        }\n    }\n}\n\nfn main() {\n    // Rust ignores SIGPIPE by default, causing println! to panic on broken pipes.\n    // Reset to SIG_DFL so the OS terminates the process cleanly instead.\n    #[cfg(unix)]\n    unsafe {\n        libc::signal(libc::SIGPIPE, libc::SIG_DFL);\n    }\n\n    // Prevent MSYS/Git Bash path translation from mangling arguments\n    #[cfg(windows)]\n    {\n        env::set_var(\"MSYS_NO_PATHCONV\", \"1\");\n        env::set_var(\"MSYS2_ARG_CONV_EXCL\", \"*\");\n    }\n\n    // Native daemon mode: when AGENT_BROWSER_DAEMON is set, run as the daemon process\n    if env::var(\"AGENT_BROWSER_DAEMON\").is_ok() {\n        // Ignore SIGPIPE so the daemon isn't killed when the parent drops\n        // the piped stderr handle after confirming the daemon is ready.\n        #[cfg(unix)]\n        unsafe {\n            libc::signal(libc::SIGPIPE, libc::SIG_IGN);\n        }\n        let session = env::var(\"AGENT_BROWSER_SESSION\").unwrap_or_else(|_| \"default\".to_string());\n        let rt = tokio::runtime::Runtime::new().expect(\"Failed to create tokio runtime\");\n        rt.block_on(native::daemon::run_daemon(&session));\n        return;\n    }\n\n    let args: Vec<String> = env::args().skip(1).collect();\n    let flags = parse_flags(&args);\n    let clean = clean_args(&args);\n\n    let has_help = args.iter().any(|a| a == \"--help\" || a == \"-h\");\n    let has_version = args.iter().any(|a| a == \"--version\" || a == \"-V\");\n\n    if has_help {\n        if let Some(cmd) = clean.first() {\n            if print_command_help(cmd) {\n                return;\n            }\n        }\n        print_help();\n        return;\n    }\n\n    if has_version {\n        print_version();\n        return;\n    }\n\n    if clean.is_empty() {\n        print_help();\n        return;\n    }\n\n    // Handle install separately\n    if clean.first().map(|s| s.as_str()) == Some(\"install\") {\n        let with_deps = args.iter().any(|a| a == \"--with-deps\" || a == \"-d\");\n        run_install(with_deps);\n        return;\n    }\n\n    // Handle upgrade separately\n    if clean.first().map(|s| s.as_str()) == Some(\"upgrade\") {\n        run_upgrade();\n        return;\n    }\n\n    // Handle session separately (doesn't need daemon)\n    if clean.first().map(|s| s.as_str()) == Some(\"session\") {\n        run_session(&clean, &flags.session, flags.json);\n        return;\n    }\n\n    let mut cmd = match parse_command(&clean, &flags) {\n        Ok(c) => c,\n        Err(e) => {\n            if flags.json {\n                let error_type = match &e {\n                    ParseError::UnknownCommand { .. } => \"unknown_command\",\n                    ParseError::UnknownSubcommand { .. } => \"unknown_subcommand\",\n                    ParseError::MissingArguments { .. } => \"missing_arguments\",\n                    ParseError::InvalidValue { .. } => \"invalid_value\",\n                    ParseError::InvalidSessionName { .. } => \"invalid_session_name\",\n                };\n                print_json_error_with_type(e.format(), error_type);\n            } else {\n                eprintln!(\"{}\", color::red(&e.format()));\n            }\n            exit(1);\n        }\n    };\n\n    // Handle --password-stdin for auth save\n    if cmd.get(\"action\").and_then(|v| v.as_str()) == Some(\"auth_save\") {\n        if cmd.get(\"password\").is_some() {\n            eprintln!(\n                \"{} Passwords on the command line may be visible in process listings and shell history. Use --password-stdin instead.\",\n                color::warning_indicator()\n            );\n        }\n        if cmd\n            .get(\"passwordStdin\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            let mut pass = String::new();\n            if std::io::stdin().read_line(&mut pass).is_err() || pass.is_empty() {\n                eprintln!(\n                    \"{} Failed to read password from stdin\",\n                    color::error_indicator()\n                );\n                exit(1);\n            }\n            let pass = pass.trim_end_matches('\\n').trim_end_matches('\\r');\n            if pass.is_empty() {\n                eprintln!(\"{} Password from stdin is empty\", color::error_indicator());\n                exit(1);\n            }\n            cmd[\"password\"] = json!(pass);\n            cmd.as_object_mut().unwrap().remove(\"passwordStdin\");\n        }\n    }\n\n    // Validate session name before starting daemon\n    if let Some(ref name) = flags.session_name {\n        if !validation::is_valid_session_name(name) {\n            let msg = validation::session_name_error(name);\n            if flags.json {\n                print_json_error_with_type(msg, \"invalid_session_name\");\n            } else {\n                eprintln!(\"{} {}\", color::error_indicator(), msg);\n            }\n            exit(1);\n        }\n    }\n\n    let daemon_opts = DaemonOptions {\n        headed: flags.headed,\n        debug: flags.debug,\n        executable_path: flags.executable_path.as_deref(),\n        extensions: &flags.extensions,\n        args: flags.args.as_deref(),\n        user_agent: flags.user_agent.as_deref(),\n        proxy: flags.proxy.as_deref(),\n        proxy_bypass: flags.proxy_bypass.as_deref(),\n        ignore_https_errors: flags.ignore_https_errors,\n        allow_file_access: flags.allow_file_access,\n        profile: flags.profile.as_deref(),\n        state: flags.state.as_deref(),\n        provider: flags.provider.as_deref(),\n        device: flags.device.as_deref(),\n        session_name: flags.session_name.as_deref(),\n        download_path: flags.download_path.as_deref(),\n        allowed_domains: flags.allowed_domains.as_deref(),\n        action_policy: flags.action_policy.as_deref(),\n        confirm_actions: flags.confirm_actions.as_deref(),\n        engine: flags.engine.as_deref(),\n        auto_connect: flags.auto_connect,\n        idle_timeout: flags.idle_timeout.as_deref(),\n        cdp: flags.cdp.as_deref(),\n    };\n    let daemon_result = match ensure_daemon(&flags.session, &daemon_opts) {\n        Ok(result) => result,\n        Err(e) => {\n            if flags.json {\n                print_json_error(e);\n            } else {\n                eprintln!(\"{} {}\", color::error_indicator(), e);\n            }\n            exit(1);\n        }\n    };\n\n    // Warn if launch-time options were explicitly passed via CLI but daemon was already running\n    // Only warn about flags that were passed on the command line, not those set via environment\n    // variables (since the daemon already uses the env vars when it starts).\n    if daemon_result.already_running {\n        let ignored_flags: Vec<&str> = [\n            if flags.cli_executable_path {\n                Some(\"--executable-path\")\n            } else {\n                None\n            },\n            if flags.cli_extensions {\n                Some(\"--extension\")\n            } else {\n                None\n            },\n            if flags.cli_profile {\n                Some(\"--profile\")\n            } else {\n                None\n            },\n            if flags.cli_state {\n                Some(\"--state\")\n            } else {\n                None\n            },\n            if flags.cli_args { Some(\"--args\") } else { None },\n            if flags.cli_user_agent {\n                Some(\"--user-agent\")\n            } else {\n                None\n            },\n            if flags.cli_proxy {\n                Some(\"--proxy\")\n            } else {\n                None\n            },\n            if flags.cli_proxy_bypass {\n                Some(\"--proxy-bypass\")\n            } else {\n                None\n            },\n            flags.ignore_https_errors.then_some(\"--ignore-https-errors\"),\n            flags.cli_allow_file_access.then_some(\"--allow-file-access\"),\n            flags.cli_download_path.then_some(\"--download-path\"),\n            flags.cli_headed.then_some(\"--headed\"),\n        ]\n        .into_iter()\n        .flatten()\n        .collect();\n\n        if !ignored_flags.is_empty() && !flags.json {\n            eprintln!(\n                \"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.\",\n                color::warning_indicator(),\n                ignored_flags.join(\", \")\n            );\n        }\n    }\n\n    // Validate mutually exclusive options\n    if flags.cdp.is_some() && flags.provider.is_some() {\n        let msg = \"Cannot use --cdp and -p/--provider together\";\n        if flags.json {\n            print_json_error(msg);\n        } else {\n            eprintln!(\"{} {}\", color::error_indicator(), msg);\n        }\n        exit(1);\n    }\n\n    if flags.auto_connect && flags.cdp.is_some() {\n        let msg = \"Cannot use --auto-connect and --cdp together\";\n        if flags.json {\n            print_json_error(msg);\n        } else {\n            eprintln!(\"{} {}\", color::error_indicator(), msg);\n        }\n        exit(1);\n    }\n\n    if flags.auto_connect && flags.provider.is_some() {\n        let msg = \"Cannot use --auto-connect and -p/--provider together\";\n        if flags.json {\n            print_json_error(msg);\n        } else {\n            eprintln!(\"{} {}\", color::error_indicator(), msg);\n        }\n        exit(1);\n    }\n\n    if flags.provider.is_some() && !flags.extensions.is_empty() {\n        let msg = \"Cannot use --extension with -p/--provider (extensions require local browser)\";\n        if flags.json {\n            print_json_error(msg);\n        } else {\n            eprintln!(\"{} {}\", color::error_indicator(), msg);\n        }\n        exit(1);\n    }\n\n    if flags.cdp.is_some() && !flags.extensions.is_empty() {\n        let msg = \"Cannot use --extension with --cdp (extensions require local browser)\";\n        if flags.json {\n            print_json_error(msg);\n        } else {\n            eprintln!(\"{} {}\", color::error_indicator(), msg);\n        }\n        exit(1);\n    }\n\n    // Auto-connect to existing browser\n    if flags.auto_connect {\n        let mut launch_cmd = json!({\n            \"id\": gen_id(),\n            \"action\": \"launch\",\n            \"autoConnect\": true\n        });\n\n        if flags.ignore_https_errors {\n            launch_cmd[\"ignoreHTTPSErrors\"] = json!(true);\n        }\n\n        if let Some(ref cs) = flags.color_scheme {\n            launch_cmd[\"colorScheme\"] = json!(cs);\n        }\n\n        if let Some(ref dp) = flags.download_path {\n            launch_cmd[\"downloadPath\"] = json!(dp);\n        }\n\n        let err = match send_command(launch_cmd, &flags.session) {\n            Ok(resp) if resp.success => None,\n            Ok(resp) => Some(\n                resp.error\n                    .unwrap_or_else(|| \"Auto-connect failed\".to_string()),\n            ),\n            Err(e) => Some(e.to_string()),\n        };\n\n        if let Some(msg) = err {\n            if flags.json {\n                print_json_error(msg);\n            } else {\n                eprintln!(\"{} {}\", color::error_indicator(), msg);\n            }\n            exit(1);\n        }\n    }\n\n    // Connect via CDP if --cdp flag is set\n    // Accepts either a port number (e.g., \"9222\") or a full URL (e.g., \"ws://...\" or \"wss://...\")\n    if let Some(ref cdp_value) = flags.cdp {\n        let mut launch_cmd = if cdp_value.starts_with(\"ws://\")\n            || cdp_value.starts_with(\"wss://\")\n            || cdp_value.starts_with(\"http://\")\n            || cdp_value.starts_with(\"https://\")\n        {\n            // It's a URL - use cdpUrl field\n            json!({\n                \"id\": gen_id(),\n                \"action\": \"launch\",\n                \"cdpUrl\": cdp_value\n            })\n        } else {\n            // It's a port number - validate and use cdpPort field\n            let cdp_port: u16 = match cdp_value.parse::<u32>() {\n                Ok(0) => {\n                    let msg = \"Invalid CDP port: port must be greater than 0\".to_string();\n                    if flags.json {\n                        print_json_error(&msg);\n                    } else {\n                        eprintln!(\"{} {}\", color::error_indicator(), msg);\n                    }\n                    exit(1);\n                }\n                Ok(p) if p > 65535 => {\n                    let msg = format!(\n                        \"Invalid CDP port: {} is out of range (valid range: 1-65535)\",\n                        p\n                    );\n                    if flags.json {\n                        print_json_error(&msg);\n                    } else {\n                        eprintln!(\"{} {}\", color::error_indicator(), msg);\n                    }\n                    exit(1);\n                }\n                Ok(p) => p as u16,\n                Err(_) => {\n                    let msg = format!(\n                        \"Invalid CDP value: '{}' is not a valid port number or URL\",\n                        cdp_value\n                    );\n                    if flags.json {\n                        print_json_error(&msg);\n                    } else {\n                        eprintln!(\"{} {}\", color::error_indicator(), msg);\n                    }\n                    exit(1);\n                }\n            };\n            json!({\n                \"id\": gen_id(),\n                \"action\": \"launch\",\n                \"cdpPort\": cdp_port\n            })\n        };\n\n        if flags.ignore_https_errors {\n            launch_cmd[\"ignoreHTTPSErrors\"] = json!(true);\n        }\n\n        if let Some(ref cs) = flags.color_scheme {\n            launch_cmd[\"colorScheme\"] = json!(cs);\n        }\n\n        if let Some(ref dp) = flags.download_path {\n            launch_cmd[\"downloadPath\"] = json!(dp);\n        }\n\n        let err = match send_command(launch_cmd, &flags.session) {\n            Ok(resp) if resp.success => None,\n            Ok(resp) => Some(\n                resp.error\n                    .unwrap_or_else(|| \"CDP connection failed\".to_string()),\n            ),\n            Err(e) => Some(e.to_string()),\n        };\n\n        if let Some(msg) = err {\n            if flags.json {\n                print_json_error(msg);\n            } else {\n                eprintln!(\"{} {}\", color::error_indicator(), msg);\n            }\n            exit(1);\n        }\n    }\n\n    // Launch with cloud provider if -p flag is set\n    if let Some(ref provider) = flags.provider {\n        let mut launch_cmd = json!({\n            \"id\": gen_id(),\n            \"action\": \"launch\",\n            \"provider\": provider\n        });\n\n        if let Some(ref cs) = flags.color_scheme {\n            launch_cmd[\"colorScheme\"] = json!(cs);\n        }\n\n        let err = match send_command(launch_cmd, &flags.session) {\n            Ok(resp) if resp.success => None,\n            Ok(resp) => Some(\n                resp.error\n                    .unwrap_or_else(|| \"Provider connection failed\".to_string()),\n            ),\n            Err(e) => Some(e.to_string()),\n        };\n\n        if let Some(msg) = err {\n            if flags.json {\n                print_json_error(msg);\n            } else {\n                eprintln!(\"{} {}\", color::error_indicator(), msg);\n            }\n            exit(1);\n        }\n    }\n\n    // Launch headed browser or configure browser options (without CDP or provider)\n    if (flags.headed\n        || flags.cli_headed  // User explicitly set --headed (even if false)\n        || flags.executable_path.is_some()\n        || flags.profile.is_some()\n        || flags.state.is_some()\n        || flags.proxy.is_some()\n        || flags.args.is_some()\n        || flags.user_agent.is_some()\n        || flags.allow_file_access\n        || flags.color_scheme.is_some()\n        || flags.download_path.is_some()\n        || flags.engine.is_some()\n        || !flags.extensions.is_empty())\n        && flags.cdp.is_none()\n        && flags.provider.is_none()\n        && !flags.auto_connect\n    {\n        let mut launch_cmd = json!({\n            \"id\": gen_id(),\n            \"action\": \"launch\",\n            \"headless\": !flags.headed\n        });\n\n        let cmd_obj = launch_cmd\n            .as_object_mut()\n            .expect(\"json! macro guarantees object type\");\n\n        // Add executable path if specified\n        if let Some(ref exec_path) = flags.executable_path {\n            cmd_obj.insert(\"executablePath\".to_string(), json!(exec_path));\n        }\n\n        // Add profile path if specified\n        if let Some(ref profile_path) = flags.profile {\n            cmd_obj.insert(\"profile\".to_string(), json!(profile_path));\n        }\n\n        // Add state path if specified\n        if let Some(ref state_path) = flags.state {\n            cmd_obj.insert(\"storageState\".to_string(), json!(state_path));\n        }\n\n        if let Some(ref proxy_str) = flags.proxy {\n            let mut proxy_obj = parse_proxy(proxy_str);\n            // Add bypass if specified\n            if let Some(ref bypass) = flags.proxy_bypass {\n                if let Some(obj) = proxy_obj.as_object_mut() {\n                    obj.insert(\"bypass\".to_string(), json!(bypass));\n                }\n            }\n            cmd_obj.insert(\"proxy\".to_string(), proxy_obj);\n        }\n\n        if let Some(ref ua) = flags.user_agent {\n            cmd_obj.insert(\"userAgent\".to_string(), json!(ua));\n        }\n\n        if let Some(ref a) = flags.args {\n            // Parse args (comma or newline separated)\n            let args_vec: Vec<String> = a\n                .split(&[',', '\\n'][..])\n                .map(|s| s.trim().to_string())\n                .filter(|s| !s.is_empty())\n                .collect();\n            cmd_obj.insert(\"args\".to_string(), json!(args_vec));\n        }\n\n        if !flags.extensions.is_empty() {\n            cmd_obj.insert(\"extensions\".to_string(), json!(&flags.extensions));\n        }\n\n        if flags.ignore_https_errors {\n            launch_cmd[\"ignoreHTTPSErrors\"] = json!(true);\n        }\n\n        if flags.allow_file_access {\n            launch_cmd[\"allowFileAccess\"] = json!(true);\n        }\n\n        if let Some(ref cs) = flags.color_scheme {\n            launch_cmd[\"colorScheme\"] = json!(cs);\n        }\n\n        if let Some(ref dp) = flags.download_path {\n            launch_cmd[\"downloadPath\"] = json!(dp);\n        }\n\n        if let Some(ref domains) = flags.allowed_domains {\n            launch_cmd[\"allowedDomains\"] = json!(domains);\n        }\n\n        if let Some(ref engine) = flags.engine {\n            launch_cmd[\"engine\"] = json!(engine);\n        }\n\n        match send_command(launch_cmd, &flags.session) {\n            Ok(resp) if !resp.success => {\n                // Launch command failed (e.g., invalid state file, profile error)\n                let error_msg = resp\n                    .error\n                    .unwrap_or_else(|| \"Browser launch failed\".to_string());\n                if flags.json {\n                    print_json_error(error_msg);\n                } else {\n                    eprintln!(\"{} {}\", color::error_indicator(), error_msg);\n                }\n                exit(1);\n            }\n            Err(e) => {\n                if flags.json {\n                    print_json_error(e);\n                } else {\n                    eprintln!(\n                        \"{} Could not configure browser: {}\",\n                        color::error_indicator(),\n                        e\n                    );\n                }\n                exit(1);\n            }\n            Ok(_) => {\n                // Launch succeeded\n            }\n        }\n    }\n\n    // Handle batch command: read commands from stdin, execute sequentially\n    if cmd.get(\"action\").and_then(|v| v.as_str()) == Some(\"batch\") {\n        let bail = cmd.get(\"bail\").and_then(|v| v.as_bool()).unwrap_or(false);\n        run_batch(&flags, bail);\n        return;\n    }\n\n    let output_opts = OutputOptions {\n        json: flags.json,\n        content_boundaries: flags.content_boundaries,\n        max_output: flags.max_output,\n    };\n\n    match send_command(cmd.clone(), &flags.session) {\n        Ok(resp) => {\n            let success = resp.success;\n            // Handle interactive confirmation\n            if flags.confirm_interactive {\n                if let Some(data) = &resp.data {\n                    if data\n                        .get(\"confirmation_required\")\n                        .and_then(|v| v.as_bool())\n                        .unwrap_or(false)\n                    {\n                        let desc = data\n                            .get(\"description\")\n                            .and_then(|v| v.as_str())\n                            .unwrap_or(\"unknown action\");\n                        let category = data.get(\"category\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                        let cid = data\n                            .get(\"confirmation_id\")\n                            .and_then(|v| v.as_str())\n                            .unwrap_or(\"\");\n\n                        eprintln!(\"[agent-browser] Action requires confirmation:\");\n                        eprintln!(\"  {}: {}\", category, desc);\n                        eprint!(\"  Allow? [y/N]: \");\n\n                        let mut input = String::new();\n                        let approved = if std::io::IsTerminal::is_terminal(&std::io::stdin()) {\n                            std::io::stdin().read_line(&mut input).is_ok()\n                                && matches!(input.trim().to_lowercase().as_str(), \"y\" | \"yes\")\n                        } else {\n                            false\n                        };\n\n                        let confirm_cmd = if approved {\n                            json!({ \"id\": gen_id(), \"action\": \"confirm\", \"confirmationId\": cid })\n                        } else {\n                            json!({ \"id\": gen_id(), \"action\": \"deny\", \"confirmationId\": cid })\n                        };\n\n                        match send_command(confirm_cmd, &flags.session) {\n                            Ok(r) => {\n                                if !approved {\n                                    eprintln!(\"{} Action denied\", color::error_indicator());\n                                    exit(1);\n                                }\n                                print_response_with_opts(&r, None, &output_opts);\n                            }\n                            Err(e) => {\n                                eprintln!(\"{} {}\", color::error_indicator(), e);\n                                exit(1);\n                            }\n                        }\n                        return;\n                    }\n                }\n            }\n            // Extract action for context-specific output handling\n            let action = cmd.get(\"action\").and_then(|v| v.as_str());\n            print_response_with_opts(&resp, action, &output_opts);\n            if !success {\n                exit(1);\n            }\n        }\n        Err(e) => {\n            if flags.json {\n                print_json_error(e);\n            } else {\n                eprintln!(\"{} {}\", color::error_indicator(), e);\n            }\n            exit(1);\n        }\n    }\n}\n\nfn run_batch(flags: &Flags, bail: bool) {\n    use std::io::Read as _;\n\n    let mut input = String::new();\n    if let Err(e) = std::io::stdin().read_to_string(&mut input) {\n        if flags.json {\n            print_json_error(format!(\"Failed to read stdin: {}\", e));\n        } else {\n            eprintln!(\"{} Failed to read stdin: {}\", color::error_indicator(), e);\n        }\n        exit(1);\n    }\n\n    let commands: Vec<Vec<String>> = match serde_json::from_str(&input) {\n        Ok(c) => c,\n        Err(e) => {\n            if flags.json {\n                print_json_error(format!(\n                    \"Invalid JSON input: {}. Expected an array of string arrays, e.g. [[\\\"open\\\", \\\"https://example.com\\\"], [\\\"snapshot\\\"]]\",\n                    e\n                ));\n            } else {\n                eprintln!(\n                    \"{} Invalid JSON input: {}. Expected an array of string arrays.\",\n                    color::error_indicator(),\n                    e\n                );\n            }\n            exit(1);\n        }\n    };\n\n    if commands.is_empty() {\n        if flags.json {\n            println!(\"[]\");\n        }\n        return;\n    }\n\n    let output_opts = OutputOptions {\n        json: flags.json,\n        content_boundaries: flags.content_boundaries,\n        max_output: flags.max_output,\n    };\n\n    let mut results: Vec<serde_json::Value> = Vec::new();\n    let mut had_error = false;\n\n    for (i, cmd_args) in commands.iter().enumerate() {\n        if cmd_args.is_empty() {\n            continue;\n        }\n\n        let parsed = match parse_command(cmd_args, flags) {\n            Ok(c) => c,\n            Err(e) => {\n                had_error = true;\n                if flags.json {\n                    results.push(json!({\n                        \"command\": cmd_args,\n                        \"success\": false,\n                        \"error\": e.format(),\n                    }));\n                    if bail {\n                        break;\n                    }\n                } else {\n                    eprintln!(\n                        \"{} Command {}: {}\",\n                        color::error_indicator(),\n                        i + 1,\n                        e.format()\n                    );\n                    if bail {\n                        exit(1);\n                    }\n                }\n                continue;\n            }\n        };\n\n        let action = parsed\n            .get(\"action\")\n            .and_then(|v| v.as_str())\n            .map(|s| s.to_string());\n\n        match send_command(parsed, &flags.session) {\n            Ok(resp) => {\n                if flags.json {\n                    results.push(json!({\n                        \"command\": cmd_args,\n                        \"success\": resp.success,\n                        \"result\": resp.data,\n                        \"error\": resp.error,\n                    }));\n                } else {\n                    if i > 0 {\n                        println!();\n                    }\n                    print_response_with_opts(&resp, action.as_deref(), &output_opts);\n                }\n                if !resp.success {\n                    had_error = true;\n                    if bail {\n                        if !flags.json {\n                            exit(1);\n                        }\n                        break;\n                    }\n                }\n            }\n            Err(e) => {\n                had_error = true;\n                if flags.json {\n                    results.push(json!({\n                        \"command\": cmd_args,\n                        \"success\": false,\n                        \"error\": e.to_string(),\n                    }));\n                    if bail {\n                        break;\n                    }\n                } else {\n                    eprintln!(\"{} Command {}: {}\", color::error_indicator(), i + 1, e);\n                    if bail {\n                        exit(1);\n                    }\n                }\n            }\n        }\n    }\n\n    if flags.json {\n        println!(\n            \"{}\",\n            serde_json::to_string(&results).unwrap_or_else(|_| \"[]\".to_string())\n        );\n    }\n\n    if had_error {\n        exit(1);\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse_proxy_simple() {\n        let result = parse_proxy(\"http://proxy.com:8080\");\n        assert_eq!(result[\"server\"], \"http://proxy.com:8080\");\n        assert!(result.get(\"username\").is_none());\n        assert!(result.get(\"password\").is_none());\n    }\n\n    #[test]\n    fn test_parse_proxy_with_auth() {\n        let result = parse_proxy(\"http://user:pass@proxy.com:8080\");\n        assert_eq!(result[\"server\"], \"http://proxy.com:8080\");\n        assert_eq!(result[\"username\"], \"user\");\n        assert_eq!(result[\"password\"], \"pass\");\n    }\n\n    #[test]\n    fn test_parse_proxy_username_only() {\n        let result = parse_proxy(\"http://user@proxy.com:8080\");\n        assert_eq!(result[\"server\"], \"http://proxy.com:8080\");\n        assert_eq!(result[\"username\"], \"user\");\n        assert_eq!(result[\"password\"], \"\");\n    }\n\n    #[test]\n    fn test_parse_proxy_no_protocol() {\n        let result = parse_proxy(\"proxy.com:8080\");\n        assert_eq!(result[\"server\"], \"proxy.com:8080\");\n        assert!(result.get(\"username\").is_none());\n    }\n\n    #[test]\n    fn test_parse_proxy_socks5() {\n        let result = parse_proxy(\"socks5://proxy.com:1080\");\n        assert_eq!(result[\"server\"], \"socks5://proxy.com:1080\");\n        assert!(result.get(\"username\").is_none());\n    }\n\n    #[test]\n    fn test_parse_proxy_socks5_with_auth() {\n        let result = parse_proxy(\"socks5://admin:secret@proxy.com:1080\");\n        assert_eq!(result[\"server\"], \"socks5://proxy.com:1080\");\n        assert_eq!(result[\"username\"], \"admin\");\n        assert_eq!(result[\"password\"], \"secret\");\n    }\n\n    #[test]\n    fn test_parse_proxy_complex_password() {\n        let result = parse_proxy(\"http://user:p@ss:w0rd@proxy.com:8080\");\n        assert_eq!(result[\"server\"], \"http://proxy.com:8080\");\n        assert_eq!(result[\"username\"], \"user\");\n        assert_eq!(result[\"password\"], \"p@ss:w0rd\");\n    }\n\n    #[test]\n    fn test_serialize_json_value_escapes_control_characters() {\n        let payload = serialize_json_value(&json!({\n            \"success\": false,\n            \"error\": \"Daemon process exited during startup:\\nline \\\"quoted\\\"\\u{001b}[2mansi\\u{001b}[22m\",\n        }));\n\n        let parsed: serde_json::Value = serde_json::from_str(&payload).unwrap();\n        assert_eq!(parsed[\"success\"], false);\n        assert_eq!(\n            parsed[\"error\"],\n            \"Daemon process exited during startup:\\nline \\\"quoted\\\"\\u{001b}[2mansi\\u{001b}[22m\"\n        );\n    }\n}\n"
  },
  {
    "path": "cli/src/native/actions.rs",
    "content": "use serde_json::{json, Value};\nuse std::collections::HashMap;\nuse std::env;\nuse std::io::Write;\nuse std::path::PathBuf;\nuse std::sync::atomic::AtomicU64;\nuse std::sync::Arc;\nuse time::{format_description::well_known::Rfc3339, OffsetDateTime};\nuse tokio::sync::{broadcast, oneshot, RwLock};\n\nuse super::auth;\nuse super::browser::{BrowserManager, WaitUntil};\nuse super::cdp::chrome::LaunchOptions;\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::{\n    AttachToTargetParams, AttachToTargetResult, CdpEvent, ConsoleApiCalledEvent,\n    CreateTargetResult, DispatchMouseEventParams, ExceptionThrownEvent, TargetCreatedEvent,\n    TargetDestroyedEvent,\n};\nuse super::cookies;\nuse super::diff;\nuse super::element::RefMap;\nuse super::inspect_server::InspectServer;\nuse super::interaction;\nuse super::network::{self, DomainFilter, EventTracker};\nuse super::policy::{ActionPolicy, ConfirmActions, PolicyResult};\nuse super::providers;\nuse super::recording::{self, RecordingState};\nuse super::screenshot::{self, ScreenshotOptions};\nuse super::snapshot::{self, SnapshotOptions};\nuse super::state;\nuse super::storage;\nuse super::stream::{self, StreamServer};\nuse super::tracing::{self as native_tracing, TracingState};\nuse super::webdriver::appium::AppiumManager;\nuse super::webdriver::backend::{BrowserBackend, WebDriverBackend, WEBDRIVER_UNSUPPORTED_ACTIONS};\nuse super::webdriver::ios;\nuse super::webdriver::safari;\n\npub struct PendingConfirmation {\n    pub action: String,\n    pub cmd: Value,\n}\n\n/// Captured request/response metadata used to export HAR 1.2 files.\npub struct HarEntry {\n    pub request_id: String,\n    /// Seconds since Unix epoch (CDP `wallTime`), with sub-second precision.\n    pub wall_time: f64,\n    // Request fields\n    pub method: String,\n    pub url: String,\n    pub request_headers: Vec<(String, String)>,\n    pub post_data: Option<String>,\n    pub request_body_size: i64,\n    pub resource_type: String,\n    // Response fields — populated by `Network.responseReceived`\n    pub status: Option<i64>,\n    pub status_text: String,\n    /// Normalised from CDP `response.protocol` (e.g. `\"h2\"` → `\"HTTP/2.0\"`).\n    pub http_version: String,\n    pub response_headers: Vec<(String, String)>,\n    pub mime_type: String,\n    pub redirect_url: String,\n    /// Updated by `Network.loadingFinished` for final accuracy.\n    pub response_body_size: i64,\n    /// Raw CDP `ResourceTiming` object from `Network.responseReceived`.\n    pub cdp_timing: Option<Value>,\n    /// Monotonic timestamp (seconds) from `Network.loadingFinished`; used to\n    /// compute the `receive` timing phase.\n    pub loading_finished_timestamp: Option<f64>,\n}\n\npub struct RouteEntry {\n    pub url_pattern: String,\n    pub response: Option<RouteResponse>,\n    pub abort: bool,\n}\n\npub struct RouteResponse {\n    pub status: Option<u16>,\n    pub body: Option<String>,\n    pub content_type: Option<String>,\n    pub headers: Option<HashMap<String, String>>,\n}\n\n#[derive(Clone, serde::Serialize)]\npub struct TrackedRequest {\n    pub url: String,\n    pub method: String,\n    pub headers: Value,\n    pub timestamp: u64,\n    #[serde(rename = \"resourceType\")]\n    pub resource_type: String,\n}\n\npub struct FetchPausedRequest {\n    pub request_id: String,\n    pub url: String,\n    pub resource_type: String,\n    pub session_id: String,\n    /// Original request headers from the Fetch.requestPaused event, needed\n    /// because Fetch.continueRequest replaces (not merges) headers.\n    pub request_headers: Option<serde_json::Map<String, Value>>,\n}\n\npub enum BackendType {\n    Cdp,\n    WebDriver,\n}\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct MouseState {\n    pub x: f64,\n    pub y: f64,\n    pub buttons: i32,\n}\n\npub struct DaemonState {\n    pub browser: Option<BrowserManager>,\n    pub appium: Option<AppiumManager>,\n    pub safari_driver: Option<safari::SafariDriverProcess>,\n    pub webdriver_backend: Option<super::webdriver::backend::WebDriverBackend>,\n    pub backend_type: BackendType,\n    pub ref_map: RefMap,\n    pub domain_filter: Arc<RwLock<Option<DomainFilter>>>,\n    pub event_tracker: EventTracker,\n    pub session_name: Option<String>,\n    pub session_id: String,\n    pub tracing_state: TracingState,\n    pub recording_state: RecordingState,\n    event_rx: Option<broadcast::Receiver<CdpEvent>>,\n    pub screencasting: bool,\n    pub policy: Option<ActionPolicy>,\n    pub pending_confirmation: Option<PendingConfirmation>,\n    pub har_recording: bool,\n    pub har_entries: Vec<HarEntry>,\n    pub confirm_actions: Option<ConfirmActions>,\n    pub inspect_server: Option<InspectServer>,\n    pub routes: Arc<RwLock<Vec<RouteEntry>>>,\n    pub tracked_requests: Vec<TrackedRequest>,\n    pub request_tracking: bool,\n    pub active_frame_id: Option<String>,\n    /// Origin-scoped extra HTTP headers set via `--headers` on navigate.\n    /// Key is the origin (scheme + host + port), value is the headers map.\n    /// Wrapped in Arc<RwLock<>> so the background Fetch handler can read it.\n    pub origin_headers: Arc<RwLock<HashMap<String, HashMap<String, String>>>>,\n    /// Background task that processes Fetch.requestPaused events in real-time,\n    /// handling domain filtering, route interception, and origin-scoped headers\n    /// without deadlocking navigation/evaluate.\n    fetch_handler_task: Option<tokio::task::JoinHandle<()>>,\n    pub mouse_state: MouseState,\n    /// Shared slot for stream server to receive CDP client when browser launches.\n    pub stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,\n    /// Stream server instance kept alive so the broadcast channel remains open.\n    pub stream_server: Option<Arc<StreamServer>>,\n}\n\nimpl DaemonState {\n    pub fn new() -> Self {\n        Self {\n            browser: None,\n            appium: None,\n            safari_driver: None,\n            webdriver_backend: None,\n            backend_type: BackendType::Cdp,\n            ref_map: RefMap::new(),\n            domain_filter: Arc::new(RwLock::new(\n                env::var(\"AGENT_BROWSER_ALLOWED_DOMAINS\")\n                    .ok()\n                    .filter(|s| !s.is_empty())\n                    .map(|s| DomainFilter::new(&s)),\n            )),\n            event_tracker: EventTracker::new(),\n            session_name: env::var(\"AGENT_BROWSER_SESSION_NAME\").ok(),\n            session_id: env::var(\"AGENT_BROWSER_SESSION\").unwrap_or_else(|_| \"default\".to_string()),\n            tracing_state: TracingState::new(),\n            recording_state: RecordingState::new(),\n            event_rx: None,\n            screencasting: false,\n            policy: ActionPolicy::load_if_exists(),\n            pending_confirmation: None,\n            har_recording: false,\n            har_entries: Vec::new(),\n            confirm_actions: ConfirmActions::from_env(),\n            inspect_server: None,\n            routes: Arc::new(RwLock::new(Vec::new())),\n            tracked_requests: Vec::new(),\n            request_tracking: false,\n            active_frame_id: None,\n            origin_headers: Arc::new(RwLock::new(HashMap::new())),\n            fetch_handler_task: None,\n            mouse_state: MouseState::default(),\n            stream_client: None,\n            stream_server: None,\n        }\n    }\n\n    fn reset_input_state(&mut self) {\n        self.mouse_state = MouseState::default();\n    }\n\n    /// Create state with an optional stream client slot and server instance\n    /// (for daemon startup with stream server).\n    pub fn new_with_stream(\n        stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,\n        stream_server: Option<Arc<StreamServer>>,\n    ) -> Self {\n        let mut s = Self::new();\n        s.stream_client = stream_client;\n        s.stream_server = stream_server;\n        s\n    }\n\n    fn subscribe_to_browser_events(&mut self) {\n        if let Some(ref browser) = self.browser {\n            self.event_rx = Some(browser.client.subscribe());\n        }\n    }\n\n    /// Start the background task that processes all Fetch.requestPaused events\n    /// in real-time (domain filtering, route interception, origin-scoped headers).\n    /// Must be called after the browser is set and events are subscribed.\n    fn start_fetch_handler(&mut self) {\n        // Abort any existing handler.\n        if let Some(task) = self.fetch_handler_task.take() {\n            task.abort();\n        }\n\n        let Some(ref browser) = self.browser else {\n            return;\n        };\n\n        let client = browser.client.clone();\n        let mut rx = browser.client.subscribe();\n        let domain_filter = self.domain_filter.clone();\n        let routes = self.routes.clone();\n        let origin_headers = self.origin_headers.clone();\n\n        self.fetch_handler_task = Some(tokio::spawn(async move {\n            loop {\n                match rx.recv().await {\n                    Ok(event) if event.method == \"Fetch.requestPaused\" => {\n                        let request_id = event\n                            .params\n                            .get(\"requestId\")\n                            .and_then(|v| v.as_str())\n                            .unwrap_or(\"\")\n                            .to_string();\n                        let request_url = event\n                            .params\n                            .get(\"request\")\n                            .and_then(|r| r.get(\"url\"))\n                            .and_then(|v| v.as_str())\n                            .unwrap_or(\"\")\n                            .to_string();\n                        let resource_type = event\n                            .params\n                            .get(\"resourceType\")\n                            .and_then(|v| v.as_str())\n                            .unwrap_or(\"\")\n                            .to_string();\n                        let request_headers = event\n                            .params\n                            .get(\"request\")\n                            .and_then(|r| r.get(\"headers\"))\n                            .and_then(|h| h.as_object())\n                            .cloned();\n                        let sid = event.session_id.clone().unwrap_or_default();\n\n                        let paused = FetchPausedRequest {\n                            request_id,\n                            url: request_url,\n                            resource_type,\n                            session_id: sid,\n                            request_headers,\n                        };\n\n                        let df = domain_filter.read().await;\n                        let rt = routes.read().await;\n                        let oh = origin_headers.read().await;\n\n                        resolve_fetch_paused(&client, df.as_ref(), &rt, &oh, &paused).await;\n                    }\n                    Ok(_) => continue,\n                    Err(broadcast::error::RecvError::Lagged(_)) => continue,\n                    Err(_) => break,\n                }\n            }\n        }));\n    }\n\n    /// Update the stream server's CDP client slot when browser is set or cleared.\n    pub async fn update_stream_client(&self) {\n        if let Some(ref slot) = self.stream_client {\n            let mut guard = slot.write().await;\n            *guard = self.browser.as_ref().map(|m| Arc::clone(&m.client));\n        }\n        if let Some(ref server) = self.stream_server {\n            // Update the CDP page session ID so screencast commands target the right page\n            let session_id = self\n                .browser\n                .as_ref()\n                .and_then(|m| m.active_session_id().ok().map(|s| s.to_string()));\n            server.set_cdp_session_id(session_id).await;\n\n            // Broadcast connection status change to WebSocket clients\n            let connected = self.browser.is_some();\n            let sc = server.is_screencasting().await;\n            server.broadcast_status(connected, sc, 1280, 720);\n            // Notify the background CDP event loop that the client changed\n            server.notify_client_changed();\n        }\n    }\n\n    /// Spawn a background task that polls screenshots and pipes them to ffmpeg.\n    async fn start_recording_task(\n        &mut self,\n        client: Arc<CdpClient>,\n        session_id: String,\n    ) -> Result<(), String> {\n        let shared_count = Arc::new(AtomicU64::new(0));\n        let (cancel_tx, cancel_rx) = oneshot::channel();\n        let handle = recording::spawn_recording_task(\n            client,\n            session_id,\n            self.recording_state.output_path.clone(),\n            shared_count.clone(),\n            cancel_rx,\n        );\n        self.recording_state.capture_task = Some(handle);\n        self.recording_state.shared_frame_count = Some(shared_count);\n        self.recording_state.cancel_tx = Some(cancel_tx);\n        Ok(())\n    }\n\n    async fn stop_recording_task(&mut self) -> Result<(), String> {\n        recording::stop_recording_task(&mut self.recording_state).await\n    }\n\n    fn drain_cdp_events(&mut self) -> (Vec<i64>, Vec<TargetCreatedEvent>, Vec<String>) {\n        let rx = match self.event_rx.as_mut() {\n            Some(rx) => rx,\n            None => return (Vec::new(), Vec::new(), Vec::new()),\n        };\n\n        let mut pending_acks: Vec<i64> = Vec::new();\n        let mut new_targets: Vec<TargetCreatedEvent> = Vec::new();\n        let mut destroyed_targets: Vec<String> = Vec::new();\n\n        loop {\n            match rx.try_recv() {\n                Ok(event) => {\n                    // Target events are not session-scoped; handle them first\n                    match event.method.as_str() {\n                        \"Target.targetCreated\" => {\n                            if let Ok(te) =\n                                serde_json::from_value::<TargetCreatedEvent>(event.params.clone())\n                            {\n                                if (te.target_info.target_type == \"page\"\n                                    || te.target_info.target_type == \"webview\")\n                                    && !te.target_info.url.is_empty()\n                                {\n                                    let already_tracked = self\n                                        .browser\n                                        .as_ref()\n                                        .is_none_or(|b| b.has_target(&te.target_info.target_id));\n                                    if !already_tracked {\n                                        new_targets.push(te);\n                                    }\n                                }\n                            }\n                            continue;\n                        }\n                        \"Target.targetDestroyed\" => {\n                            if let Ok(te) =\n                                serde_json::from_value::<TargetDestroyedEvent>(event.params.clone())\n                            {\n                                destroyed_targets.push(te.target_id);\n                            }\n                            continue;\n                        }\n                        _ => {}\n                    }\n\n                    let session_matches = if let Some(ref browser) = self.browser {\n                        event.session_id.as_deref() == browser.active_session_id().ok()\n                    } else {\n                        false\n                    };\n\n                    if !session_matches {\n                        continue;\n                    }\n\n                    match event.method.as_str() {\n                        \"Runtime.consoleAPICalled\" => {\n                            if let Ok(console_event) = serde_json::from_value::<ConsoleApiCalledEvent>(\n                                event.params.clone(),\n                            ) {\n                                let text: String = console_event\n                                    .args\n                                    .iter()\n                                    .filter_map(|arg| {\n                                        arg.value\n                                            .as_ref()\n                                            .map(|v| match v {\n                                                Value::String(s) => s.clone(),\n                                                other => other.to_string(),\n                                            })\n                                            .or_else(|| arg.description.clone())\n                                    })\n                                    .collect::<Vec<_>>()\n                                    .join(\" \");\n                                self.event_tracker\n                                    .add_console(&console_event.call_type, &text);\n                            }\n                        }\n                        \"Runtime.exceptionThrown\" => {\n                            if let Ok(ex_event) =\n                                serde_json::from_value::<ExceptionThrownEvent>(event.params.clone())\n                            {\n                                let details = &ex_event.exception_details;\n                                let text = details\n                                    .exception\n                                    .as_ref()\n                                    .and_then(|e| e.description.as_deref())\n                                    .unwrap_or(&details.text);\n                                self.event_tracker.add_error(\n                                    text,\n                                    None,\n                                    details.line_number,\n                                    details.column_number,\n                                );\n                            }\n                        }\n                        \"Network.requestWillBeSent\"\n                            if self.har_recording || self.request_tracking =>\n                        {\n                            if let Some(request) = event.params.get(\"request\") {\n                                let method = request\n                                    .get(\"method\")\n                                    .and_then(|v| v.as_str())\n                                    .unwrap_or(\"GET\")\n                                    .to_string();\n                                let url = request\n                                    .get(\"url\")\n                                    .and_then(|v| v.as_str())\n                                    .unwrap_or(\"\")\n                                    .to_string();\n                                let request_id = event\n                                    .params\n                                    .get(\"requestId\")\n                                    .and_then(|v| v.as_str())\n                                    .unwrap_or(\"\")\n                                    .to_string();\n                                if self.har_recording {\n                                    let wall_time = event\n                                        .params\n                                        .get(\"wallTime\")\n                                        .and_then(|v| v.as_f64())\n                                        .unwrap_or(0.0);\n                                    let request_headers =\n                                        har_extract_headers(request.get(\"headers\"));\n                                    let post_data = request\n                                        .get(\"postData\")\n                                        .and_then(|v| v.as_str())\n                                        .map(String::from);\n                                    let request_body_size =\n                                        post_data.as_ref().map(|s| s.len() as i64).unwrap_or(0);\n                                    let resource_type = event\n                                        .params\n                                        .get(\"type\")\n                                        .and_then(|v| v.as_str())\n                                        .unwrap_or(\"Other\")\n                                        .to_string();\n                                    self.har_entries.push(HarEntry {\n                                        request_id,\n                                        wall_time,\n                                        method: method.clone(),\n                                        url: url.clone(),\n                                        request_headers,\n                                        post_data,\n                                        request_body_size,\n                                        resource_type,\n                                        status: None,\n                                        status_text: String::new(),\n                                        http_version: \"HTTP/1.1\".to_string(),\n                                        response_headers: Vec::new(),\n                                        mime_type: String::new(),\n                                        redirect_url: String::new(),\n                                        response_body_size: -1,\n                                        cdp_timing: None,\n                                        loading_finished_timestamp: None,\n                                    });\n                                }\n                                if self.request_tracking {\n                                    let headers =\n                                        request.get(\"headers\").cloned().unwrap_or(json!({}));\n                                    let resource_type = event\n                                        .params\n                                        .get(\"type\")\n                                        .and_then(|v| v.as_str())\n                                        .unwrap_or(\"Other\")\n                                        .to_string();\n                                    let timestamp = std::time::SystemTime::now()\n                                        .duration_since(std::time::UNIX_EPOCH)\n                                        .map(|d| d.as_millis() as u64)\n                                        .unwrap_or(0);\n                                    self.tracked_requests.push(TrackedRequest {\n                                        url,\n                                        method,\n                                        headers,\n                                        timestamp,\n                                        resource_type,\n                                    });\n                                }\n                            }\n                        }\n                        \"Network.responseReceived\" if self.har_recording => {\n                            if let Some(response) = event.params.get(\"response\") {\n                                let request_id = event\n                                    .params\n                                    .get(\"requestId\")\n                                    .and_then(|v| v.as_str())\n                                    .unwrap_or(\"\");\n                                let status = response.get(\"status\").and_then(|v| v.as_i64());\n                                let status_text = response\n                                    .get(\"statusText\")\n                                    .and_then(|v| v.as_str())\n                                    .unwrap_or(\"\")\n                                    .to_string();\n                                let mime_type = response\n                                    .get(\"mimeType\")\n                                    .and_then(|v| v.as_str())\n                                    .unwrap_or(\"\")\n                                    .to_string();\n                                let http_version = response\n                                    .get(\"protocol\")\n                                    .and_then(|v| v.as_str())\n                                    .map(har_cdp_protocol_to_http_version)\n                                    .unwrap_or_else(|| \"HTTP/1.1\".to_string());\n                                let response_headers = har_extract_headers(response.get(\"headers\"));\n                                let redirect_url = response_headers\n                                    .iter()\n                                    .find(|(k, _)| k.eq_ignore_ascii_case(\"location\"))\n                                    .map(|(_, v)| v.clone())\n                                    .unwrap_or_default();\n                                let encoded_data_length = response\n                                    .get(\"encodedDataLength\")\n                                    .and_then(|v| v.as_i64())\n                                    .unwrap_or(-1);\n                                let cdp_timing = response.get(\"timing\").cloned();\n                                if let Some(entry) = self\n                                    .har_entries\n                                    .iter_mut()\n                                    .rev()\n                                    .find(|e| e.request_id == request_id)\n                                {\n                                    entry.status = status;\n                                    entry.status_text = status_text;\n                                    entry.mime_type = mime_type;\n                                    entry.http_version = http_version;\n                                    entry.response_headers = response_headers;\n                                    entry.redirect_url = redirect_url;\n                                    entry.response_body_size = encoded_data_length;\n                                    entry.cdp_timing = cdp_timing;\n                                }\n                            }\n                        }\n                        \"Network.loadingFinished\" if self.har_recording => {\n                            let request_id = event\n                                .params\n                                .get(\"requestId\")\n                                .and_then(|v| v.as_str())\n                                .unwrap_or(\"\");\n                            let timestamp = event.params.get(\"timestamp\").and_then(|v| v.as_f64());\n                            let encoded_data_length = event\n                                .params\n                                .get(\"encodedDataLength\")\n                                .and_then(|v| v.as_i64());\n                            if let Some(entry) = self\n                                .har_entries\n                                .iter_mut()\n                                .rev()\n                                .find(|e| e.request_id == request_id)\n                            {\n                                if let Some(ts) = timestamp {\n                                    entry.loading_finished_timestamp = Some(ts);\n                                }\n                                if let Some(len) = encoded_data_length {\n                                    entry.response_body_size = len;\n                                }\n                            }\n                        }\n                        \"Page.screencastFrame\" => {\n                            // Frame broadcasting and acks are handled in real-time by the\n                            // stream server's background CDP event loop. Here we just\n                            // collect acks as a fallback for non-streaming mode.\n                            if self.stream_server.is_none() {\n                                if let Some(sid) =\n                                    event.params.get(\"sessionId\").and_then(|v| v.as_i64())\n                                {\n                                    pending_acks.push(sid);\n                                }\n                            }\n                        }\n                        // Fetch.requestPaused is handled by the background\n                        // fetch_handler_task — no need to collect here.\n                        _ => {}\n                    }\n                }\n                Err(broadcast::error::TryRecvError::Empty) => break,\n                Err(broadcast::error::TryRecvError::Lagged(_)) => continue,\n                Err(broadcast::error::TryRecvError::Closed) => {\n                    self.event_rx = None;\n                    break;\n                }\n            }\n        }\n\n        (pending_acks, new_targets, destroyed_targets)\n    }\n}\n\nimpl Drop for DaemonState {\n    fn drop(&mut self) {\n        // The background fetch handler sits in rx.recv().await indefinitely.\n        // Without aborting it, the tokio runtime won't shut down (tests hang).\n        if let Some(task) = self.fetch_handler_task.take() {\n            task.abort();\n        }\n    }\n}\n\npub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {\n    let action = cmd.get(\"action\").and_then(|v| v.as_str()).unwrap_or(\"\");\n    let id = cmd\n        .get(\"id\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"\")\n        .to_string();\n\n    // Drain pending CDP events (console, errors, screencast frames, target lifecycle)\n    let (pending_acks, new_targets, destroyed_targets) = state.drain_cdp_events();\n    if !pending_acks.is_empty() {\n        if let Some(ref browser) = state.browser {\n            if let Ok(session_id) = browser.active_session_id() {\n                for ack_sid in pending_acks {\n                    let _ =\n                        stream::ack_screencast_frame(&browser.client, session_id, ack_sid).await;\n                }\n            }\n        }\n    }\n\n    for target_id in &destroyed_targets {\n        if let Some(ref mut mgr) = state.browser {\n            mgr.remove_page_by_target_id(target_id);\n        }\n    }\n\n    for te in &new_targets {\n        if let Some(ref mut mgr) = state.browser {\n            let attach_result: Result<AttachToTargetResult, String> = mgr\n                .client\n                .send_command_typed(\n                    \"Target.attachToTarget\",\n                    &AttachToTargetParams {\n                        target_id: te.target_info.target_id.clone(),\n                        flatten: true,\n                    },\n                    None,\n                )\n                .await;\n            if let Ok(attach) = attach_result {\n                let _ = mgr.enable_domains_pub(&attach.session_id).await;\n\n                // Install domain filter on new pages\n                let df = state.domain_filter.read().await;\n                if let Some(ref filter) = *df {\n                    let _ = network::install_domain_filter(\n                        &mgr.client,\n                        &attach.session_id,\n                        &filter.allowed_domains,\n                    )\n                    .await;\n                }\n\n                mgr.add_page(super::browser::PageInfo {\n                    target_id: te.target_info.target_id.clone(),\n                    session_id: attach.session_id,\n                    url: te.target_info.url.clone(),\n                    title: te.target_info.title.clone(),\n                    target_type: te.target_info.target_type.clone(),\n                });\n            }\n        }\n    }\n\n    // Hot-reload and check action policy\n    if let Some(ref mut policy) = state.policy {\n        let _ = policy.reload();\n        match policy.check(action) {\n            PolicyResult::Allow => {}\n            PolicyResult::Deny(reason) => {\n                return error_response(\n                    &id,\n                    &format!(\"Action '{}' denied by policy: {}\", action, reason),\n                );\n            }\n            PolicyResult::RequiresConfirmation => {\n                state.pending_confirmation = Some(PendingConfirmation {\n                    action: action.to_string(),\n                    cmd: cmd.clone(),\n                });\n                return json!({\n                    \"id\": id,\n                    \"success\": true,\n                    \"data\": { \"confirmation_required\": true, \"action\": action },\n                });\n            }\n        }\n    }\n\n    // Check AGENT_BROWSER_CONFIRM_ACTIONS (category-based, independent of policy file)\n    if action != \"confirm\" && action != \"deny\" {\n        if let Some(ref ca) = state.confirm_actions {\n            if ca.requires_confirmation(action) {\n                state.pending_confirmation = Some(PendingConfirmation {\n                    action: action.to_string(),\n                    cmd: cmd.clone(),\n                });\n                return json!({\n                    \"id\": id,\n                    \"success\": true,\n                    \"data\": {\n                        \"confirmation_required\": true,\n                        \"confirmation_id\": id,\n                        \"action\": action,\n                    },\n                });\n            }\n        }\n    }\n\n    let skip_launch = matches!(\n        action,\n        \"\" | \"launch\"\n            | \"close\"\n            | \"har_stop\"\n            | \"credentials_set\"\n            | \"credentials_get\"\n            | \"credentials_delete\"\n            | \"credentials_list\"\n            | \"auth_save\"\n            | \"auth_show\"\n            | \"auth_delete\"\n            | \"auth_list\"\n            | \"state_list\"\n            | \"state_show\"\n            | \"state_clear\"\n            | \"state_clean\"\n            | \"state_rename\"\n            | \"device_list\"\n    );\n    if !skip_launch {\n        // Check if existing connection is stale and needs re-launch\n        let needs_launch = if let Some(ref mgr) = state.browser {\n            !mgr.is_connection_alive().await\n        } else {\n            true\n        };\n\n        if needs_launch {\n            if state.browser.is_some() {\n                if let Some(ref mut mgr) = state.browser {\n                    let _ = mgr.close().await;\n                }\n                state.browser = None;\n                state.reset_input_state();\n                state.update_stream_client().await;\n            }\n            if let Err(e) = auto_launch(state).await {\n                return error_response(&id, &format!(\"Auto-launch failed: {}\", e));\n            }\n        }\n\n        if let Some(ref mut mgr) = state.browser {\n            if mgr.page_count() == 0 {\n                let _ = mgr.ensure_page().await;\n            }\n        }\n    }\n\n    // WebDriver backend: reject unsupported CDP-only actions\n    if matches!(state.backend_type, BackendType::WebDriver)\n        && WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&action)\n    {\n        return error_response(\n            &id,\n            &format!(\n                \"Action '{}' is not supported on the WebDriver backend\",\n                action\n            ),\n        );\n    }\n\n    let result = match action {\n        \"launch\" => handle_launch(cmd, state).await,\n        \"navigate\" => handle_navigate(cmd, state).await,\n        \"url\" => handle_url(state).await,\n        \"cdp_url\" => handle_cdp_url(state),\n        \"inspect\" => handle_inspect(state).await,\n        \"title\" => handle_title(state).await,\n        \"content\" => handle_content(state).await,\n        \"evaluate\" => handle_evaluate(cmd, state).await,\n        \"close\" => handle_close(state).await,\n        \"snapshot\" => handle_snapshot(cmd, state).await,\n        \"screenshot\" => handle_screenshot(cmd, state).await,\n        \"click\" => handle_click(cmd, state).await,\n        \"dblclick\" => handle_dblclick(cmd, state).await,\n        \"fill\" => handle_fill(cmd, state).await,\n        \"type\" => handle_type(cmd, state).await,\n        \"press\" => handle_press(cmd, state).await,\n        \"hover\" => handle_hover(cmd, state).await,\n        \"scroll\" => handle_scroll(cmd, state).await,\n        \"select\" => handle_select(cmd, state).await,\n        \"check\" => handle_check(cmd, state).await,\n        \"uncheck\" => handle_uncheck(cmd, state).await,\n        \"wait\" => handle_wait(cmd, state).await,\n        \"gettext\" => handle_gettext(cmd, state).await,\n        \"getattribute\" => handle_getattribute(cmd, state).await,\n        \"isvisible\" => handle_isvisible(cmd, state).await,\n        \"isenabled\" => handle_isenabled(cmd, state).await,\n        \"ischecked\" => handle_ischecked(cmd, state).await,\n        \"back\" => handle_back(state).await,\n        \"forward\" => handle_forward(state).await,\n        \"reload\" => handle_reload(state).await,\n        \"cookies_get\" => handle_cookies_get(cmd, state).await,\n        \"cookies_set\" => handle_cookies_set(cmd, state).await,\n        \"cookies_clear\" => handle_cookies_clear(state).await,\n        \"storage_get\" => handle_storage_get(cmd, state).await,\n        \"storage_set\" => handle_storage_set(cmd, state).await,\n        \"storage_clear\" => handle_storage_clear(cmd, state).await,\n        \"setcontent\" => handle_setcontent(cmd, state).await,\n        \"headers\" => handle_headers(cmd, state).await,\n        \"offline\" => handle_offline(cmd, state).await,\n        \"console\" => handle_console(state).await,\n        \"errors\" => handle_errors(state).await,\n        \"state_save\" => handle_state_save(cmd, state).await,\n        \"state_load\" => handle_state_load(cmd, state).await,\n        \"state_list\" => handle_state_list().await,\n        \"state_show\" => handle_state_show(cmd).await,\n        \"state_clear\" => handle_state_clear(cmd).await,\n        \"state_clean\" => handle_state_clean(cmd).await,\n        \"state_rename\" => handle_state_rename(cmd).await,\n        \"trace_start\" => handle_trace_start(state).await,\n        \"trace_stop\" => handle_trace_stop(cmd, state).await,\n        \"profiler_start\" => handle_profiler_start(cmd, state).await,\n        \"profiler_stop\" => handle_profiler_stop(cmd, state).await,\n        \"recording_start\" => handle_recording_start(cmd, state).await,\n        \"recording_stop\" => handle_recording_stop(state).await,\n        \"recording_restart\" => handle_recording_restart(cmd, state).await,\n        \"pdf\" => handle_pdf(cmd, state).await,\n        \"tab_list\" => handle_tab_list(state).await,\n        \"tab_new\" => handle_tab_new(cmd, state).await,\n        \"tab_switch\" => handle_tab_switch(cmd, state).await,\n        \"tab_close\" => handle_tab_close(cmd, state).await,\n        \"viewport\" => handle_viewport(cmd, state).await,\n        \"useragent\" | \"user_agent\" => handle_user_agent(cmd, state).await,\n        \"set_media\" => handle_set_media(cmd, state).await,\n        \"download\" => handle_download(cmd, state).await,\n        \"diff_snapshot\" => handle_diff_snapshot(cmd, state).await,\n        \"diff_url\" => handle_diff_url(cmd, state).await,\n        \"credentials_set\" => handle_credentials_set(cmd).await,\n        \"credentials_get\" => handle_credentials_get(cmd).await,\n        \"credentials_delete\" => handle_credentials_delete(cmd).await,\n        \"credentials_list\" => handle_credentials_list().await,\n        \"mouse\" => handle_mouse(cmd, state).await,\n        \"keyboard\" => handle_keyboard(cmd, state).await,\n        \"focus\" => handle_focus(cmd, state).await,\n        \"clear\" => handle_clear(cmd, state).await,\n        \"selectall\" => handle_selectall(cmd, state).await,\n        \"scrollintoview\" => handle_scrollintoview(cmd, state).await,\n        \"dispatch\" => handle_dispatch(cmd, state).await,\n        \"highlight\" => handle_highlight(cmd, state).await,\n        \"tap\" => handle_tap(cmd, state).await,\n        \"boundingbox\" => handle_boundingbox(cmd, state).await,\n        \"innertext\" => handle_innertext(cmd, state).await,\n        \"innerhtml\" => handle_innerhtml(cmd, state).await,\n        \"inputvalue\" => handle_inputvalue(cmd, state).await,\n        \"setvalue\" => handle_setvalue(cmd, state).await,\n        \"count\" => handle_count(cmd, state).await,\n        \"styles\" => handle_styles(cmd, state).await,\n        \"bringtofront\" => handle_bringtofront(state).await,\n        \"timezone\" => handle_timezone(cmd, state).await,\n        \"locale\" => handle_locale(cmd, state).await,\n        \"geolocation\" => handle_geolocation(cmd, state).await,\n        \"permissions\" => handle_permissions(cmd, state).await,\n        \"dialog\" => handle_dialog(cmd, state).await,\n        \"upload\" => handle_upload(cmd, state).await,\n        \"addscript\" => handle_addscript(cmd, state).await,\n        \"addinitscript\" => handle_addinitscript(cmd, state).await,\n        \"addstyle\" => handle_addstyle(cmd, state).await,\n        \"clipboard\" => handle_clipboard(cmd, state).await,\n        \"wheel\" => handle_wheel(cmd, state).await,\n        \"device\" => handle_device(cmd, state).await,\n        \"screencast_start\" => handle_screencast_start(cmd, state).await,\n        \"screencast_stop\" => handle_screencast_stop(state).await,\n        \"waitforurl\" => handle_waitforurl(cmd, state).await,\n        \"waitforloadstate\" => handle_waitforloadstate(cmd, state).await,\n        \"waitforfunction\" => handle_waitforfunction(cmd, state).await,\n        \"frame\" => handle_frame(cmd, state).await,\n        \"mainframe\" => handle_mainframe(state).await,\n        \"getbyrole\" => handle_getbyrole(cmd, state).await,\n        \"getbytext\" => handle_getbytext(cmd, state).await,\n        \"getbylabel\" => handle_getbylabel(cmd, state).await,\n        \"getbyplaceholder\" => handle_getbyplaceholder(cmd, state).await,\n        \"getbyalttext\" => handle_getbyalttext(cmd, state).await,\n        \"getbytitle\" => handle_getbytitle(cmd, state).await,\n        \"getbytestid\" => handle_getbytestid(cmd, state).await,\n        \"nth\" => handle_nth(cmd, state).await,\n        \"find\" => handle_find(cmd, state).await,\n        \"evalhandle\" => handle_evalhandle(cmd, state).await,\n        \"drag\" => handle_drag(cmd, state).await,\n        \"expose\" => handle_expose(cmd, state).await,\n        \"pause\" => handle_pause(state).await,\n        \"multiselect\" => handle_multiselect(cmd, state).await,\n        \"responsebody\" => handle_responsebody(cmd, state).await,\n        \"waitfordownload\" => handle_waitfordownload(cmd, state).await,\n        \"window_new\" => handle_window_new(cmd, state).await,\n        \"diff_screenshot\" => handle_diff_screenshot(cmd, state).await,\n        \"video_start\" => handle_video_start(cmd, state).await,\n        \"video_stop\" => handle_video_stop(state).await,\n        \"har_start\" => handle_har_start(state).await,\n        \"har_stop\" => handle_har_stop(cmd, state).await,\n        \"route\" => handle_route(cmd, state).await,\n        \"unroute\" => handle_unroute(cmd, state).await,\n        \"requests\" => handle_requests(cmd, state).await,\n        \"credentials\" => handle_http_credentials(cmd, state).await,\n        \"emulatemedia\" => handle_set_media(cmd, state).await,\n        \"auth_save\" => handle_auth_save(cmd).await,\n        \"auth_login\" => handle_auth_login(cmd, state).await,\n        \"auth_list\" => handle_credentials_list().await,\n        \"auth_delete\" => handle_credentials_delete(cmd).await,\n        \"auth_show\" => handle_auth_show(cmd).await,\n        \"confirm\" => handle_confirm(cmd, state).await,\n        \"deny\" => handle_deny(cmd, state).await,\n        \"swipe\" => handle_swipe(cmd, state).await,\n        \"device_list\" => handle_device_list().await,\n        \"input_mouse\" => handle_input_mouse(cmd, state).await,\n        \"input_keyboard\" => handle_input_keyboard(cmd, state).await,\n        \"input_touch\" => handle_input_touch(cmd, state).await,\n        \"keydown\" => handle_keydown(cmd, state).await,\n        \"keyup\" => handle_keyup(cmd, state).await,\n        \"inserttext\" => handle_inserttext(cmd, state).await,\n        \"mousemove\" => handle_mousemove(cmd, state).await,\n        \"mousedown\" => handle_mousedown(cmd, state).await,\n        \"mouseup\" => handle_mouseup(cmd, state).await,\n        _ => Err(format!(\"Not yet implemented: {}\", action)),\n    };\n\n    match result {\n        Ok(data) => success_response(&id, data),\n        Err(e) => error_response(&id, &super::browser::to_ai_friendly_error(&e)),\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Auto-launch\n// ---------------------------------------------------------------------------\n\n/// Connect to a running Chrome via auto-discovery and open a fresh tab so\n/// subsequent navigations don't hijack the user's existing tabs.\nasync fn connect_auto_with_fresh_tab() -> Result<BrowserManager, String> {\n    let mut mgr = BrowserManager::connect_auto().await?;\n    mgr.tab_new(None).await?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let _ = mgr\n        .client\n        .send_command(\"Page.bringToFront\", None, Some(&session_id))\n        .await;\n    Ok(mgr)\n}\n\nasync fn auto_launch(state: &mut DaemonState) -> Result<(), String> {\n    let options = launch_options_from_env();\n    let engine = env::var(\"AGENT_BROWSER_ENGINE\").ok();\n\n    if let Ok(cdp) = env::var(\"AGENT_BROWSER_CDP\") {\n        let mgr = BrowserManager::connect_cdp(&cdp).await?;\n        state.reset_input_state();\n        state.browser = Some(mgr);\n        state.subscribe_to_browser_events();\n        state.start_fetch_handler();\n        state.update_stream_client().await;\n        try_auto_restore_state(state).await;\n        return Ok(());\n    }\n\n    if env::var(\"AGENT_BROWSER_AUTO_CONNECT\").is_ok() {\n        state.reset_input_state();\n        state.browser = Some(connect_auto_with_fresh_tab().await?);\n        state.subscribe_to_browser_events();\n        state.start_fetch_handler();\n        state.update_stream_client().await;\n        try_auto_restore_state(state).await;\n        return Ok(());\n    }\n\n    let mgr = BrowserManager::launch(options, engine.as_deref()).await?;\n    state.reset_input_state();\n    state.browser = Some(mgr);\n    state.subscribe_to_browser_events();\n    state.start_fetch_handler();\n    state.update_stream_client().await;\n    try_auto_restore_state(state).await;\n    Ok(())\n}\n\nfn launch_options_from_env() -> LaunchOptions {\n    let headed = env::var(\"AGENT_BROWSER_HEADED\")\n        .map(|v| v == \"1\" || v == \"true\")\n        .unwrap_or(false);\n\n    let extensions: Option<Vec<String>> = env::var(\"AGENT_BROWSER_EXTENSIONS\").ok().map(|v| {\n        v.split([',', '\\n'])\n            .map(|s| s.trim().to_string())\n            .filter(|s| !s.is_empty())\n            .collect()\n    });\n\n    LaunchOptions {\n        headless: !headed,\n        executable_path: env::var(\"AGENT_BROWSER_EXECUTABLE_PATH\").ok(),\n        proxy: env::var(\"AGENT_BROWSER_PROXY\").ok(),\n        proxy_bypass: env::var(\"AGENT_BROWSER_PROXY_BYPASS\").ok(),\n        profile: env::var(\"AGENT_BROWSER_PROFILE\").ok(),\n        allow_file_access: env::var(\"AGENT_BROWSER_ALLOW_FILE_ACCESS\")\n            .map(|v| v == \"1\" || v == \"true\")\n            .unwrap_or(false),\n        args: env::var(\"AGENT_BROWSER_ARGS\")\n            .map(|v| {\n                v.split([',', '\\n'])\n                    .map(|s| s.trim().to_string())\n                    .filter(|s| !s.is_empty())\n                    .collect()\n            })\n            .unwrap_or_default(),\n        extensions,\n        storage_state: env::var(\"AGENT_BROWSER_STATE\").ok(),\n        user_agent: env::var(\"AGENT_BROWSER_USER_AGENT\").ok(),\n        ignore_https_errors: env::var(\"AGENT_BROWSER_IGNORE_HTTPS_ERRORS\")\n            .map(|v| v == \"1\" || v == \"true\")\n            .unwrap_or(false),\n        color_scheme: env::var(\"AGENT_BROWSER_COLOR_SCHEME\").ok(),\n        download_path: env::var(\"AGENT_BROWSER_DOWNLOAD_PATH\").ok(),\n    }\n}\n\nasync fn daemon_state_from_env(state: &mut DaemonState) {\n    if let Ok(name) = env::var(\"AGENT_BROWSER_SESSION_NAME\") {\n        if !name.is_empty() {\n            state.session_name = Some(name);\n        }\n    }\n    if let Ok(domains) = env::var(\"AGENT_BROWSER_ALLOWED_DOMAINS\") {\n        if !domains.is_empty() {\n            let mut df = state.domain_filter.write().await;\n            *df = Some(DomainFilter::new(&domains));\n        }\n    }\n    if state.policy.is_none() {\n        state.policy = ActionPolicy::load_if_exists();\n    }\n}\n\nasync fn try_auto_restore_state(state: &mut DaemonState) {\n    let session_name = match state.session_name.as_deref() {\n        Some(n) if !n.is_empty() => n.to_string(),\n        _ => return,\n    };\n    if let Some(path) = state::find_auto_state_file(&session_name) {\n        if let Some(ref mgr) = state.browser {\n            if let Ok(session_id) = mgr.active_session_id() {\n                let _ = state::load_state(&mgr.client, session_id, &path).await;\n            }\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let headless = cmd\n        .get(\"headless\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(true);\n    let cdp_url = cmd.get(\"cdpUrl\").and_then(|v| v.as_str());\n    let cdp_port = cmd.get(\"cdpPort\").and_then(|v| v.as_u64());\n    let auto_connect = cmd\n        .get(\"autoConnect\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false);\n\n    // Relaunch logic: check if we can reuse the existing connection\n    let needs_relaunch = if let Some(ref mgr) = state.browser {\n        let is_external = cdp_url.is_some() || cdp_port.is_some() || auto_connect;\n        let was_external = mgr.is_cdp_connection();\n        is_external != was_external || !mgr.is_connection_alive().await\n    } else {\n        true\n    };\n\n    if needs_relaunch {\n        if let Some(ref mut b) = state.browser {\n            b.close().await?;\n            state.browser = None;\n            state.reset_input_state();\n            state.update_stream_client().await;\n        }\n    } else {\n        return Ok(json!({ \"launched\": true, \"reused\": true }));\n    }\n    state.ref_map.clear();\n    let extensions: Option<Vec<String>> =\n        cmd.get(\"extensions\").and_then(|v| v.as_array()).map(|arr| {\n            arr.iter()\n                .filter_map(|v| v.as_str().map(String::from))\n                .collect()\n        });\n\n    let profile = cmd.get(\"profile\").and_then(|v| v.as_str());\n    let storage_state = cmd.get(\"storageState\").and_then(|v| v.as_str());\n    let allow_file_access = cmd\n        .get(\"allowFileAccess\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false);\n    let executable_path: Option<String> = cmd\n        .get(\"executablePath\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .or_else(|| std::env::var(\"AGENT_BROWSER_EXECUTABLE_PATH\").ok());\n\n    let has_cdp = cdp_url.is_some() || cdp_port.is_some();\n    super::browser::validate_launch_options(\n        extensions.as_deref(),\n        has_cdp,\n        profile,\n        storage_state,\n        allow_file_access,\n        executable_path.as_deref(),\n    )?;\n\n    if let Some(url) = cdp_url {\n        state.reset_input_state();\n        state.browser = Some(BrowserManager::connect_cdp(url).await?);\n        state.subscribe_to_browser_events();\n        state.start_fetch_handler();\n        state.update_stream_client().await;\n        return Ok(json!({ \"launched\": true }));\n    }\n\n    if let Some(port) = cdp_port {\n        state.reset_input_state();\n        state.browser = Some(BrowserManager::connect_cdp(&port.to_string()).await?);\n        state.subscribe_to_browser_events();\n        state.start_fetch_handler();\n        state.update_stream_client().await;\n        return Ok(json!({ \"launched\": true }));\n    }\n\n    if auto_connect {\n        state.reset_input_state();\n        state.browser = Some(connect_auto_with_fresh_tab().await?);\n        state.subscribe_to_browser_events();\n        state.start_fetch_handler();\n        state.update_stream_client().await;\n        return Ok(json!({ \"launched\": true }));\n    }\n\n    if let Some(provider) = cmd.get(\"provider\").and_then(|v| v.as_str()) {\n        match provider.to_lowercase().as_str() {\n            \"ios\" => {\n                return launch_ios(cmd, state).await;\n            }\n            \"safari\" => {\n                return launch_safari(cmd, state).await;\n            }\n            _ => {\n                let (ws_url, provider_session) = providers::connect_provider(provider).await?;\n                match BrowserManager::connect_cdp(&ws_url).await {\n                    Ok(mgr) => {\n                        state.reset_input_state();\n                        state.browser = Some(mgr);\n                        state.subscribe_to_browser_events();\n                        state.start_fetch_handler();\n                        state.update_stream_client().await;\n                        return Ok(json!({ \"launched\": true, \"provider\": provider }));\n                    }\n                    Err(e) => {\n                        if let Some(ref ps) = provider_session {\n                            providers::close_provider_session(ps).await;\n                        }\n                        return Err(e);\n                    }\n                }\n            }\n        }\n    }\n\n    let engine = cmd\n        .get(\"engine\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .or_else(|| env::var(\"AGENT_BROWSER_ENGINE\").ok());\n\n    let options = LaunchOptions {\n        headless,\n        executable_path: cmd\n            .get(\"executablePath\")\n            .and_then(|v| v.as_str())\n            .map(|s| s.to_string())\n            .or_else(|| env::var(\"AGENT_BROWSER_EXECUTABLE_PATH\").ok()),\n        proxy: cmd.get(\"proxy\").and_then(|v| {\n            v.as_str().map(|s| s.to_string()).or_else(|| {\n                v.get(\"server\")\n                    .and_then(|s| s.as_str())\n                    .map(|s| s.to_string())\n            })\n        }),\n        profile: cmd\n            .get(\"profile\")\n            .and_then(|v| v.as_str())\n            .map(|s| s.to_string()),\n        allow_file_access: cmd\n            .get(\"allowFileAccess\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false),\n        args: cmd\n            .get(\"args\")\n            .and_then(|v| v.as_array())\n            .map(|arr| {\n                arr.iter()\n                    .filter_map(|v| v.as_str().map(|s| s.to_string()))\n                    .collect()\n            })\n            .unwrap_or_default(),\n        extensions,\n        storage_state: storage_state.map(String::from),\n        proxy_bypass: cmd\n            .get(\"proxy\")\n            .and_then(|v| v.get(\"bypass\"))\n            .and_then(|v| v.as_str())\n            .map(String::from),\n        user_agent: cmd\n            .get(\"userAgent\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n        ignore_https_errors: cmd\n            .get(\"ignoreHTTPSErrors\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false),\n        color_scheme: cmd\n            .get(\"colorScheme\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n        download_path: cmd\n            .get(\"downloadPath\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n    };\n\n    if let Some(ref domains) = cmd\n        .get(\"allowedDomains\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n    {\n        let mut df = state.domain_filter.write().await;\n        *df = Some(DomainFilter::new(domains));\n    }\n\n    state.reset_input_state();\n    state.browser = Some(BrowserManager::launch(options, engine.as_deref()).await?);\n    state.subscribe_to_browser_events();\n    state.start_fetch_handler();\n    state.update_stream_client().await;\n\n    {\n        let df = state.domain_filter.read().await;\n        if let Some(ref filter) = *df {\n            if let Some(ref mgr) = state.browser {\n                if let Ok(session_id) = mgr.active_session_id() {\n                    let _ = network::install_domain_filter(\n                        &mgr.client,\n                        session_id,\n                        &filter.allowed_domains,\n                    )\n                    .await;\n                    network::sanitize_existing_pages(&mgr.client, &mgr.pages_list(), filter).await;\n                }\n            }\n        }\n    }\n\n    Ok(json!({ \"launched\": true }))\n}\n\nasync fn launch_ios(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let device_name = cmd.get(\"deviceName\").and_then(|v| v.as_str());\n    let device_udid = cmd.get(\"udid\").and_then(|v| v.as_str());\n    let platform_version = cmd.get(\"platformVersion\").and_then(|v| v.as_str());\n\n    // Select device (or use default)\n    let device = ios::select_device(device_name, device_udid)?;\n\n    // Boot simulator if it's not real and not already booted\n    if !device.is_real && device.state != \"Booted\" {\n        ios::boot_simulator(&device.udid)?;\n    }\n\n    // Start Appium\n    let mut appium = AppiumManager::connect_or_launch(Some(&device.udid)).await?;\n\n    // Create iOS Safari session\n    appium\n        .create_ios_session(Some(&device.name), platform_version)\n        .await?;\n\n    // Create a WebDriverBackend from the Appium session for common commands\n    if let Some(sid) = appium.client.session_id_pub().map(String::from) {\n        let wd_client = super::webdriver::client::WebDriverClient::new_with_session(4723, sid);\n        state.webdriver_backend = Some(WebDriverBackend::new(wd_client));\n    }\n\n    state.appium = Some(appium);\n    state.backend_type = BackendType::WebDriver;\n    state.reset_input_state();\n\n    Ok(json!({\n        \"launched\": true,\n        \"provider\": \"ios\",\n        \"device\": device.name,\n        \"udid\": device.udid,\n        \"backend\": \"webdriver\",\n    }))\n}\n\nasync fn launch_safari(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let port: u16 = cmd\n        .get(\"port\")\n        .and_then(|v| v.as_u64())\n        .map(|p| p as u16)\n        .unwrap_or(0);\n    let driver_port = if port > 0 { port } else { 0 };\n\n    // Find a free port if none specified\n    let actual_port = if driver_port > 0 {\n        driver_port\n    } else {\n        // Use any available high port\n        let listener = std::net::TcpListener::bind(\"127.0.0.1:0\")\n            .map_err(|e| format!(\"Failed to find free port: {}\", e))?;\n        listener\n            .local_addr()\n            .map_err(|e| format!(\"Failed to get local address: {}\", e))?\n            .port()\n    };\n\n    let driver = safari::launch_safaridriver(actual_port)?;\n    let mut client = super::webdriver::client::WebDriverClient::new(actual_port);\n\n    client\n        .create_session(serde_json::json!({\n            \"browserName\": \"safari\",\n        }))\n        .await?;\n\n    state.safari_driver = Some(driver);\n    state.webdriver_backend = Some(WebDriverBackend::new(client));\n    state.backend_type = BackendType::WebDriver;\n    state.reset_input_state();\n\n    Ok(json!({\n        \"launched\": true,\n        \"provider\": \"safari\",\n        \"port\": actual_port,\n        \"backend\": \"webdriver\",\n    }))\n}\n\nasync fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let url = cmd\n        .get(\"url\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url' parameter\")?;\n\n    {\n        let df = state.domain_filter.read().await;\n        if let Some(ref filter) = *df {\n            filter.check_url(url)?;\n        }\n    }\n\n    // WebDriver backend path\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            state.ref_map.clear();\n            wb.navigate(url).await?;\n            let new_url = wb.get_url().await.unwrap_or_else(|_| url.to_string());\n            let title = wb.get_title().await.unwrap_or_default();\n            return Ok(json!({ \"url\": new_url, \"title\": title }));\n        }\n    }\n\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n\n    let wait_until = cmd\n        .get(\"waitUntil\")\n        .and_then(|v| v.as_str())\n        .map(WaitUntil::from_str)\n        .unwrap_or(WaitUntil::Load);\n\n    // If --headers was passed, store them keyed by origin and enable Fetch\n    // interception. The background fetch_handler_task (started on launch)\n    // injects them into matching requests in real-time.\n    let scoped_headers = cmd\n        .get(\"headers\")\n        .and_then(|v| v.as_object())\n        .filter(|m| !m.is_empty());\n\n    if let Some(headers_map) = scoped_headers {\n        if let Some(origin) = url::Url::parse(url)\n            .ok()\n            .map(|u| u.origin().ascii_serialization())\n        {\n            let headers: HashMap<String, String> = headers_map\n                .iter()\n                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))\n                .collect();\n\n            let first_origin_header = {\n                let mut map = state.origin_headers.write().await;\n                let first = map.is_empty();\n                map.insert(origin, headers);\n                first\n            };\n\n            // Enable Fetch interception the first time --headers is used.\n            // Fetch.enable is idempotent — safe even if domain filter or\n            // routes already enabled it. Wildcard ensures we see all requests.\n            if first_origin_header {\n                let session_id = mgr.active_session_id()?.to_string();\n                mgr.client\n                    .send_command(\n                        \"Fetch.enable\",\n                        Some(json!({ \"patterns\": [{ \"urlPattern\": \"*\" }] })),\n                        Some(&session_id),\n                    )\n                    .await?;\n            }\n        }\n    }\n\n    state.ref_map.clear();\n    mgr.navigate(url, wait_until).await\n}\n\nasync fn handle_url(state: &DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            let url = wb.get_url().await?;\n            return Ok(json!({ \"url\": url }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let url = mgr.get_url().await?;\n    Ok(json!({ \"url\": url }))\n}\n\nfn handle_cdp_url(state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    Ok(json!({ \"cdpUrl\": mgr.get_cdp_url() }))\n}\n\nasync fn handle_inspect(state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n\n    // Shut down any existing inspect server so we always target the current page\n    if let Some(server) = state.inspect_server.take() {\n        server.shutdown();\n    }\n\n    let target_id = mgr.active_target_id()?.to_string();\n    let chrome_hp = mgr.chrome_host_port().to_string();\n    let proxy_handle = mgr.client.inspect_handle();\n\n    let server = InspectServer::start(proxy_handle, target_id, chrome_hp).await?;\n    let url = format!(\"http://127.0.0.1:{}\", server.port());\n    open_url_in_browser(&url);\n\n    state.inspect_server = Some(server);\n    Ok(json!({ \"opened\": true, \"url\": url }))\n}\n\nfn open_url_in_browser(url: &str) {\n    #[cfg(target_os = \"macos\")]\n    let result = std::process::Command::new(\"open\").arg(url).spawn();\n    #[cfg(target_os = \"linux\")]\n    let result = std::process::Command::new(\"xdg-open\").arg(url).spawn();\n    #[cfg(target_os = \"windows\")]\n    let result = std::process::Command::new(\"cmd\")\n        .args([\"/c\", \"start\", \"\", url])\n        .spawn();\n    #[cfg(not(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\")))]\n    let result: Result<std::process::Child, std::io::Error> = Err(std::io::Error::new(\n        std::io::ErrorKind::Unsupported,\n        \"unsupported platform\",\n    ));\n    if let Err(e) = result {\n        let _ = writeln!(std::io::stderr(), \"[inspect] Failed to open browser: {}\", e);\n    }\n}\n\nasync fn handle_title(state: &DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            let title = wb.get_title().await?;\n            return Ok(json!({ \"title\": title }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let title = mgr.get_title().await?;\n    Ok(json!({ \"title\": title }))\n}\n\nasync fn handle_content(state: &DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            let html = wb.get_content().await?;\n            let url = wb.get_url().await.unwrap_or_default();\n            return Ok(json!({ \"html\": html, \"origin\": url }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let html = mgr.get_content().await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"html\": html, \"origin\": url }))\n}\n\nasync fn handle_evaluate(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            let script = cmd\n                .get(\"script\")\n                .and_then(|v| v.as_str())\n                .ok_or(\"Missing 'script' parameter\")?;\n            let result = wb.evaluate(script).await?;\n            let url = wb.get_url().await.unwrap_or_default();\n            return Ok(json!({ \"result\": result, \"origin\": url }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let script = cmd\n        .get(\"script\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'script' parameter\")?;\n\n    let result = mgr.evaluate(script, None).await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"result\": result, \"origin\": url }))\n}\n\nasync fn handle_close(state: &mut DaemonState) -> Result<Value, String> {\n    if let Some(ref mgr) = state.browser {\n        if let Some(ref session_name) = state.session_name {\n            if let Ok(session_id) = mgr.active_session_id() {\n                let _ = state::save_state(\n                    &mgr.client,\n                    session_id,\n                    None,\n                    Some(session_name.as_str()),\n                    &state.session_id,\n                )\n                .await;\n            }\n        }\n    }\n    if let Some(ref mut mgr) = state.browser {\n        mgr.close().await?;\n    }\n    state.browser = None;\n    state.reset_input_state();\n    state.update_stream_client().await;\n\n    // Stop background Fetch handler\n    if let Some(task) = state.fetch_handler_task.take() {\n        task.abort();\n    }\n    {\n        let mut map = state.origin_headers.write().await;\n        map.clear();\n    }\n\n    // Close WebDriver sessions\n    if let Some(ref mut wb) = state.webdriver_backend {\n        let _ = wb.close().await;\n    }\n    state.webdriver_backend = None;\n    if let Some(ref mut appium) = state.appium {\n        let _ = appium.close().await;\n    }\n    state.appium = None;\n    if let Some(ref mut driver) = state.safari_driver {\n        driver.kill();\n    }\n    state.safari_driver = None;\n    state.backend_type = BackendType::Cdp;\n\n    if let Some(server) = state.inspect_server.take() {\n        server.shutdown();\n    }\n\n    state.ref_map.clear();\n    Ok(json!({ \"closed\": true }))\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_snapshot(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let options = SnapshotOptions {\n        selector: cmd\n            .get(\"selector\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n        interactive: cmd\n            .get(\"interactive\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false),\n        compact: cmd\n            .get(\"compact\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false),\n        depth: cmd\n            .get(\"maxDepth\")\n            .and_then(|v| v.as_u64())\n            .map(|d| d as usize),\n        cursor: cmd.get(\"cursor\").and_then(|v| v.as_bool()).unwrap_or(false),\n    };\n\n    state.ref_map.clear();\n    let tree = snapshot::take_snapshot(\n        &mgr.client,\n        &session_id,\n        &options,\n        &mut state.ref_map,\n        state.active_frame_id.as_deref(),\n    )\n    .await?;\n\n    let url = mgr.get_url().await.unwrap_or_default();\n\n    let refs: serde_json::Map<String, Value> = state\n        .ref_map\n        .entries_sorted()\n        .into_iter()\n        .map(|(ref_id, entry)| {\n            let mut obj = serde_json::Map::new();\n            obj.insert(\"role\".into(), Value::String(entry.role));\n            obj.insert(\"name\".into(), Value::String(entry.name));\n            (ref_id, Value::Object(obj))\n        })\n        .collect();\n\n    Ok(json!({ \"snapshot\": tree, \"origin\": url, \"refs\": refs }))\n}\n\nasync fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let annotate = cmd\n        .get(\"annotate\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false);\n\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            if annotate {\n                return Err(\n                    \"Annotated screenshots are not yet implemented on the WebDriver backend\"\n                        .to_string(),\n                );\n            }\n\n            let base64_data = wb.screenshot().await?;\n            let path = cmd.get(\"path\").and_then(|v| v.as_str());\n            if let Some(p) = path {\n                let bytes = base64::Engine::decode(\n                    &base64::engine::general_purpose::STANDARD,\n                    &base64_data,\n                )\n                .map_err(|e| format!(\"Base64 decode error: {}\", e))?;\n                std::fs::write(p, bytes)\n                    .map_err(|e| format!(\"Failed to write screenshot: {}\", e))?;\n                return Ok(json!({ \"path\": p }));\n            }\n            let tmp = format!(\n                \"/tmp/screenshot-{}.png\",\n                std::time::SystemTime::now()\n                    .duration_since(std::time::UNIX_EPOCH)\n                    .map(|d| d.as_millis())\n                    .unwrap_or(0)\n            );\n            let bytes =\n                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &base64_data)\n                    .map_err(|e| format!(\"Base64 decode error: {}\", e))?;\n            std::fs::write(&tmp, bytes)\n                .map_err(|e| format!(\"Failed to write screenshot: {}\", e))?;\n            return Ok(json!({ \"path\": tmp }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let format = cmd\n        .get(\"format\")\n        .or_else(|| cmd.get(\"type\"))\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"png\")\n        .to_string();\n\n    let options = ScreenshotOptions {\n        selector: cmd\n            .get(\"selector\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n        path: cmd.get(\"path\").and_then(|v| v.as_str()).map(String::from),\n        full_page: cmd\n            .get(\"fullPage\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false),\n        format,\n        quality: cmd\n            .get(\"quality\")\n            .and_then(|v| v.as_i64())\n            .map(|q| q as i32),\n        annotate,\n        output_dir: cmd\n            .get(\"screenshotDir\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n    };\n\n    if annotate {\n        state.ref_map.clear();\n        let _ = snapshot::take_snapshot(\n            &mgr.client,\n            &session_id,\n            &SnapshotOptions {\n                interactive: true,\n                cursor: true,\n                ..SnapshotOptions::default()\n            },\n            &mut state.ref_map,\n            state.active_frame_id.as_deref(),\n        )\n        .await?;\n    }\n\n    let result =\n        screenshot::take_screenshot(&mgr.client, &session_id, &state.ref_map, &options).await?;\n\n    let mut response = json!({ \"path\": result.path });\n    if !result.annotations.is_empty() {\n        response[\"annotations\"] = serde_json::to_value(&result.annotations)\n            .map_err(|e| format!(\"Failed to serialize annotations: {}\", e))?;\n    }\n\n    Ok(response)\n}\n\nasync fn handle_click(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            wb.click(selector).await?;\n            return Ok(json!({ \"clicked\": selector }));\n        }\n    }\n\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let new_tab = cmd.get(\"newTab\").and_then(|v| v.as_bool()).unwrap_or(false);\n\n    if new_tab {\n        use super::element::resolve_element_object_id;\n        let object_id =\n            resolve_element_object_id(&mgr.client, &session_id, &state.ref_map, selector).await?;\n        let call_params = json!({\n            \"objectId\": object_id,\n            \"functionDeclaration\": \"function() { var h = this.getAttribute('href'); if (!h) return null; try { return new URL(h, document.baseURI).toString(); } catch(e) { return null; } }\",\n            \"returnByValue\": true\n        });\n        let call_result = mgr\n            .client\n            .send_command(\n                \"Runtime.callFunctionOn\",\n                Some(call_params),\n                Some(&session_id),\n            )\n            .await?;\n        let href = call_result\n            .get(\"result\")\n            .and_then(|r| r.get(\"value\"))\n            .and_then(|v| v.as_str())\n            .ok_or_else(|| {\n                format!(\n                    \"Element '{}' does not have an href attribute. --new-tab only works on links.\",\n                    selector\n                )\n            })?\n            .to_string();\n\n        let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n        state.ref_map.clear();\n        mgr.tab_new(Some(&href)).await?;\n\n        return Ok(json!({ \"clicked\": selector, \"newTab\": true, \"url\": href }));\n    }\n\n    let button = cmd.get(\"button\").and_then(|v| v.as_str()).unwrap_or(\"left\");\n    let click_count = cmd.get(\"clickCount\").and_then(|v| v.as_i64()).unwrap_or(1) as i32;\n\n    interaction::click(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        selector,\n        button,\n        click_count,\n    )\n    .await?;\n\n    Ok(json!({ \"clicked\": selector }))\n}\n\nasync fn handle_dblclick(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::dblclick(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"clicked\": selector }))\n}\n\nasync fn handle_fill(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let value = cmd\n        .get(\"value\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'value' parameter\")?;\n\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            wb.fill(selector, value).await?;\n            return Ok(json!({ \"filled\": selector }));\n        }\n    }\n\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    interaction::fill(&mgr.client, &session_id, &state.ref_map, selector, value).await?;\n    Ok(json!({ \"filled\": selector }))\n}\n\nasync fn handle_type(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let text = cmd\n        .get(\"text\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'text' parameter\")?;\n    let clear = cmd.get(\"clear\").and_then(|v| v.as_bool()).unwrap_or(false);\n    let delay = cmd.get(\"delay\").and_then(|v| v.as_u64());\n\n    interaction::type_text(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        selector,\n        text,\n        clear,\n        delay,\n    )\n    .await?;\n    Ok(json!({ \"typed\": text }))\n}\n\nasync fn handle_press(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let key = cmd\n        .get(\"key\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'key' parameter\")?;\n\n    interaction::press_key(&mgr.client, &session_id, key).await?;\n    Ok(json!({ \"pressed\": key }))\n}\n\nasync fn handle_hover(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::hover(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"hovered\": selector }))\n}\n\nasync fn handle_scroll(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd.get(\"selector\").and_then(|v| v.as_str());\n\n    let (mut dx, mut dy) = (\n        cmd.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(0.0),\n        cmd.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(0.0),\n    );\n\n    if let Some(direction) = cmd.get(\"direction\").and_then(|v| v.as_str()) {\n        let amount = cmd.get(\"amount\").and_then(|v| v.as_f64()).unwrap_or(300.0);\n        match direction {\n            \"up\" => dy = -amount,\n            \"down\" => dy = amount,\n            \"left\" => dx = -amount,\n            \"right\" => dx = amount,\n            _ => {}\n        }\n    }\n\n    interaction::scroll(&mgr.client, &session_id, &state.ref_map, selector, dx, dy).await?;\n    Ok(json!({ \"scrolled\": true }))\n}\n\nasync fn handle_select(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let values: Vec<String> = match cmd.get(\"values\") {\n        Some(Value::Array(arr)) => arr\n            .iter()\n            .filter_map(|v| v.as_str().map(String::from))\n            .collect(),\n        Some(Value::String(s)) => vec![s.clone()],\n        _ => cmd\n            .get(\"value\")\n            .and_then(|v| v.as_str())\n            .map(|s| vec![s.to_string()])\n            .unwrap_or_default(),\n    };\n\n    interaction::select_option(&mgr.client, &session_id, &state.ref_map, selector, &values).await?;\n    Ok(json!({ \"selected\": values }))\n}\n\nasync fn handle_check(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::check(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"checked\": selector }))\n}\n\nasync fn handle_uncheck(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::uncheck(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"unchecked\": selector }))\n}\n\nasync fn handle_wait(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let timeout_ms = cmd.get(\"timeout\").and_then(|v| v.as_u64()).unwrap_or(30000);\n\n    if let Some(text) = cmd.get(\"text\").and_then(|v| v.as_str()) {\n        wait_for_text(&mgr.client, &session_id, text, timeout_ms).await?;\n        return Ok(json!({ \"waited\": \"text\", \"text\": text }));\n    }\n\n    if let Some(selector) = cmd.get(\"selector\").and_then(|v| v.as_str()) {\n        let state_str = cmd\n            .get(\"state\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"visible\");\n        wait_for_selector(&mgr.client, &session_id, selector, state_str, timeout_ms).await?;\n        return Ok(json!({ \"waited\": \"selector\", \"selector\": selector }));\n    }\n\n    if let Some(url_pattern) = cmd.get(\"url\").and_then(|v| v.as_str()) {\n        wait_for_url(&mgr.client, &session_id, url_pattern, timeout_ms).await?;\n        return Ok(json!({ \"waited\": \"url\", \"url\": url_pattern }));\n    }\n\n    if let Some(fn_str) = cmd.get(\"function\").and_then(|v| v.as_str()) {\n        wait_for_function(&mgr.client, &session_id, fn_str, timeout_ms).await?;\n        return Ok(json!({ \"waited\": \"function\" }));\n    }\n\n    if let Some(load_state) = cmd.get(\"loadState\").and_then(|v| v.as_str()) {\n        let wait_until = WaitUntil::from_str(load_state);\n        mgr.wait_for_lifecycle_external(wait_until, &session_id)\n            .await?;\n        return Ok(json!({ \"waited\": \"load\", \"state\": load_state }));\n    }\n\n    // Just a timeout wait\n    tokio::time::sleep(tokio::time::Duration::from_millis(timeout_ms)).await;\n    Ok(json!({ \"waited\": \"timeout\", \"ms\": timeout_ms }))\n}\n\nasync fn handle_gettext(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let text = super::element::get_element_text(&mgr.client, &session_id, &state.ref_map, selector)\n        .await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"text\": text, \"origin\": url }))\n}\n\nasync fn handle_getattribute(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let attribute = cmd\n        .get(\"attribute\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'attribute' parameter\")?;\n\n    let value = super::element::get_element_attribute(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        selector,\n        attribute,\n    )\n    .await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"value\": value, \"origin\": url }))\n}\n\nasync fn handle_isvisible(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let visible =\n        super::element::is_element_visible(&mgr.client, &session_id, &state.ref_map, selector)\n            .await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"visible\": visible, \"origin\": url }))\n}\n\nasync fn handle_isenabled(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let enabled =\n        super::element::is_element_enabled(&mgr.client, &session_id, &state.ref_map, selector)\n            .await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"enabled\": enabled, \"origin\": url }))\n}\n\nasync fn handle_ischecked(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let checked =\n        super::element::is_element_checked(&mgr.client, &session_id, &state.ref_map, selector)\n            .await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"checked\": checked, \"origin\": url }))\n}\n\nasync fn handle_back(state: &mut DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            wb.back().await?;\n            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n            let url = wb.get_url().await.unwrap_or_default();\n            state.ref_map.clear();\n            return Ok(json!({ \"url\": url }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    mgr.evaluate(\"history.back()\", None).await?;\n    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n    let url = mgr.get_url().await.unwrap_or_default();\n    state.ref_map.clear();\n    Ok(json!({ \"url\": url }))\n}\n\nasync fn handle_forward(state: &mut DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            wb.forward().await?;\n            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n            let url = wb.get_url().await.unwrap_or_default();\n            state.ref_map.clear();\n            return Ok(json!({ \"url\": url }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    mgr.evaluate(\"history.forward()\", None).await?;\n    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n    let url = mgr.get_url().await.unwrap_or_default();\n    state.ref_map.clear();\n    Ok(json!({ \"url\": url }))\n}\n\nasync fn handle_reload(state: &mut DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            wb.reload().await?;\n            tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;\n            let url = wb.get_url().await.unwrap_or_default();\n            state.ref_map.clear();\n            return Ok(json!({ \"url\": url }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    mgr.client\n        .send_command_no_params(\"Page.reload\", Some(&session_id))\n        .await?;\n\n    let mut rx = mgr.client.subscribe();\n    let _ = tokio::time::timeout(tokio::time::Duration::from_secs(10), async {\n        loop {\n            match rx.recv().await {\n                Ok(event) => {\n                    if event.method == \"Page.loadEventFired\"\n                        && event.session_id.as_deref() == Some(&session_id)\n                    {\n                        return;\n                    }\n                }\n                Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,\n                Err(_) => break,\n            }\n        }\n    })\n    .await;\n\n    let url = mgr.get_url().await.unwrap_or_default();\n    state.ref_map.clear();\n    Ok(json!({ \"url\": url }))\n}\n\n// ---------------------------------------------------------------------------\n// Wait helpers\n// ---------------------------------------------------------------------------\n\nasync fn wait_for_selector(\n    client: &super::cdp::client::CdpClient,\n    session_id: &str,\n    selector: &str,\n    state: &str,\n    timeout_ms: u64,\n) -> Result<(), String> {\n    let check_fn = match state {\n        \"attached\" => format!(\n            \"!!document.querySelector({})\",\n            serde_json::to_string(selector).unwrap_or_default()\n        ),\n        \"detached\" => format!(\n            \"!document.querySelector({})\",\n            serde_json::to_string(selector).unwrap_or_default()\n        ),\n        \"hidden\" => format!(\n            r#\"(() => {{\n                const el = document.querySelector({sel});\n                if (!el) return true;\n                const s = window.getComputedStyle(el);\n                return s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0;\n            }})()\"#,\n            sel = serde_json::to_string(selector).unwrap_or_default()\n        ),\n        _ => format!(\n            r#\"(() => {{\n                const el = document.querySelector({sel});\n                if (!el) return false;\n                const r = el.getBoundingClientRect();\n                const s = window.getComputedStyle(el);\n                return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none';\n            }})()\"#,\n            sel = serde_json::to_string(selector).unwrap_or_default()\n        ),\n    };\n\n    poll_until_true(client, session_id, &check_fn, timeout_ms).await\n}\n\nasync fn wait_for_url(\n    client: &super::cdp::client::CdpClient,\n    session_id: &str,\n    pattern: &str,\n    timeout_ms: u64,\n) -> Result<(), String> {\n    let check_fn = format!(\n        \"location.href.includes({})\",\n        serde_json::to_string(pattern).unwrap_or_default()\n    );\n    poll_until_true(client, session_id, &check_fn, timeout_ms).await\n}\n\nasync fn wait_for_text(\n    client: &super::cdp::client::CdpClient,\n    session_id: &str,\n    text: &str,\n    timeout_ms: u64,\n) -> Result<(), String> {\n    let check_fn = format!(\n        \"(document.body.innerText || '').includes({})\",\n        serde_json::to_string(text).unwrap_or_default()\n    );\n    poll_until_true(client, session_id, &check_fn, timeout_ms).await\n}\n\nasync fn wait_for_function(\n    client: &super::cdp::client::CdpClient,\n    session_id: &str,\n    fn_str: &str,\n    timeout_ms: u64,\n) -> Result<(), String> {\n    let check_fn = format!(\"!!({})\", fn_str);\n    poll_until_true(client, session_id, &check_fn, timeout_ms).await\n}\n\nasync fn poll_until_true(\n    client: &super::cdp::client::CdpClient,\n    session_id: &str,\n    expression: &str,\n    timeout_ms: u64,\n) -> Result<(), String> {\n    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);\n\n    loop {\n        let result: super::cdp::types::EvaluateResult = client\n            .send_command_typed(\n                \"Runtime.evaluate\",\n                &super::cdp::types::EvaluateParams {\n                    expression: expression.to_string(),\n                    return_by_value: Some(true),\n                    await_promise: Some(true),\n                },\n                Some(session_id),\n            )\n            .await?;\n\n        if result\n            .result\n            .value\n            .as_ref()\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            return Ok(());\n        }\n\n        if tokio::time::Instant::now() >= deadline {\n            return Err(format!(\"Wait timed out after {}ms\", timeout_ms));\n        }\n\n        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_cookies_get(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    if let Some(ref wb) = state.webdriver_backend {\n        if state.browser.is_none() {\n            let cookies_list = wb.get_cookies().await?;\n            return Ok(json!({ \"cookies\": cookies_list }));\n        }\n    }\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let urls = cmd.get(\"urls\").and_then(|v| v.as_array()).map(|arr| {\n        arr.iter()\n            .filter_map(|v| v.as_str().map(String::from))\n            .collect()\n    });\n\n    let cookies_list = cookies::get_cookies(&mgr.client, &session_id, urls).await?;\n    Ok(json!({ \"cookies\": cookies_list }))\n}\n\nasync fn handle_cookies_set(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let url = mgr.get_url().await.ok();\n\n    let cookie_values = if let Some(arr) = cmd.get(\"cookies\").and_then(|v| v.as_array()) {\n        arr.clone()\n    } else {\n        let mut cookie = serde_json::Map::new();\n        for key in &[\n            \"name\", \"value\", \"domain\", \"path\", \"expires\", \"httpOnly\", \"secure\", \"sameSite\", \"url\",\n        ] {\n            if let Some(v) = cmd.get(*key) {\n                if !v.is_null() {\n                    cookie.insert(key.to_string(), v.clone());\n                }\n            }\n        }\n        vec![Value::Object(cookie)]\n    };\n\n    cookies::set_cookies(&mgr.client, &session_id, cookie_values, url.as_deref()).await?;\n    Ok(json!({ \"set\": true }))\n}\n\nasync fn handle_cookies_clear(state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    cookies::clear_cookies(&mgr.client, &session_id).await?;\n    Ok(json!({ \"cleared\": true }))\n}\n\nasync fn handle_storage_get(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let storage_type = cmd.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"local\");\n    let key = cmd.get(\"key\").and_then(|v| v.as_str());\n    storage::storage_get(&mgr.client, &session_id, storage_type, key).await\n}\n\nasync fn handle_storage_set(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let storage_type = cmd.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"local\");\n    let key = cmd\n        .get(\"key\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'key' parameter\")?;\n    let value = cmd\n        .get(\"value\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'value' parameter\")?;\n    storage::storage_set(&mgr.client, &session_id, storage_type, key, value).await?;\n    Ok(json!({ \"set\": true }))\n}\n\nasync fn handle_storage_clear(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let storage_type = cmd.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"local\");\n    storage::storage_clear(&mgr.client, &session_id, storage_type).await?;\n    Ok(json!({ \"cleared\": true }))\n}\n\nasync fn handle_setcontent(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let html = cmd\n        .get(\"html\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'html' parameter\")?;\n    network::set_content(&mgr.client, &session_id, html).await?;\n    Ok(json!({ \"set\": true }))\n}\n\nasync fn handle_headers(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let headers_value = cmd.get(\"headers\").ok_or(\"Missing 'headers' parameter\")?;\n\n    let headers: HashMap<String, String> = headers_value\n        .as_object()\n        .map(|m| {\n            m.iter()\n                .map(|(k, v)| (k.clone(), v.as_str().unwrap_or(\"\").to_string()))\n                .collect()\n        })\n        .unwrap_or_default();\n\n    network::set_extra_headers(&mgr.client, &session_id, &headers).await?;\n    Ok(json!({ \"set\": true }))\n}\n\nasync fn handle_offline(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let offline = cmd.get(\"offline\").and_then(|v| v.as_bool()).unwrap_or(true);\n    network::set_offline(&mgr.client, &session_id, offline).await?;\n    Ok(json!({ \"offline\": offline }))\n}\n\nasync fn handle_console(state: &DaemonState) -> Result<Value, String> {\n    Ok(state.event_tracker.get_console_json())\n}\n\nasync fn handle_errors(state: &DaemonState) -> Result<Value, String> {\n    Ok(state.event_tracker.get_errors_json())\n}\n\nasync fn handle_state_save(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let path = cmd.get(\"path\").and_then(|v| v.as_str());\n\n    let saved_path = state::save_state(\n        &mgr.client,\n        &session_id,\n        path,\n        state.session_name.as_deref(),\n        &state.session_id,\n    )\n    .await?;\n\n    Ok(json!({ \"saved\": true, \"path\": saved_path }))\n}\n\nasync fn handle_state_load(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n\n    state::load_state(&mgr.client, &session_id, path).await?;\n    Ok(json!({ \"loaded\": true, \"path\": path }))\n}\n\nasync fn handle_state_list() -> Result<Value, String> {\n    state::state_list()\n}\n\nasync fn handle_state_show(cmd: &Value) -> Result<Value, String> {\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n    state::state_show(path)\n}\n\nasync fn handle_state_clear(cmd: &Value) -> Result<Value, String> {\n    let path = cmd.get(\"path\").and_then(|v| v.as_str());\n    state::state_clear(path)\n}\n\nasync fn handle_state_clean(cmd: &Value) -> Result<Value, String> {\n    let days = cmd.get(\"days\").and_then(|v| v.as_u64()).unwrap_or(30);\n    state::state_clean(days)\n}\n\nasync fn handle_state_rename(cmd: &Value) -> Result<Value, String> {\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name' parameter\")?;\n    state::state_rename(path, name)\n}\n\n// ---------------------------------------------------------------------------\n// Phase 6 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_diff_snapshot(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let compact = cmd\n        .get(\"compact\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false);\n    let max_depth = cmd\n        .get(\"maxDepth\")\n        .and_then(|v| v.as_u64())\n        .map(|d| d as usize);\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .map(String::from);\n\n    let options = SnapshotOptions {\n        compact,\n        depth: max_depth,\n        selector,\n        ..SnapshotOptions::default()\n    };\n    let current = snapshot::take_snapshot(\n        &mgr.client,\n        &session_id,\n        &options,\n        &mut state.ref_map,\n        state.active_frame_id.as_deref(),\n    )\n    .await?;\n\n    let baseline = cmd.get(\"baseline\").and_then(|v| v.as_str());\n\n    let baseline_text = match baseline {\n        Some(b) if std::path::Path::new(b).exists() => {\n            std::fs::read_to_string(b).map_err(|e| format!(\"Failed to read baseline: {}\", e))?\n        }\n        Some(b) => b.to_string(),\n        None => String::new(),\n    };\n\n    let result = diff::diff_snapshots(&baseline_text, &current);\n    Ok(json!({\n        \"diff\": result.diff,\n        \"additions\": result.additions,\n        \"removals\": result.removals,\n        \"unchanged\": result.unchanged,\n        \"changed\": result.changed,\n    }))\n}\n\nasync fn handle_diff_url(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n\n    let url1 = cmd\n        .get(\"url1\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url1' parameter\")?;\n    let url2 = cmd\n        .get(\"url2\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url2' parameter\")?;\n\n    let wait_until = cmd\n        .get(\"waitUntil\")\n        .and_then(|v| v.as_str())\n        .map(WaitUntil::from_str)\n        .unwrap_or(WaitUntil::Load);\n\n    // Navigate to URL1 and snapshot\n    mgr.navigate(url1, wait_until).await?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let options = SnapshotOptions::default();\n    let snap1 =\n        snapshot::take_snapshot(&mgr.client, &session_id, &options, &mut state.ref_map, None)\n            .await?;\n\n    // Navigate to URL2 and snapshot\n    mgr.navigate(url2, wait_until).await?;\n    state.ref_map.clear();\n    let snap2 =\n        snapshot::take_snapshot(&mgr.client, &session_id, &options, &mut state.ref_map, None)\n            .await?;\n\n    let result = diff::diff_text(&snap1, &snap2);\n    Ok(json!({\n        \"diff\": result,\n        \"url1\": url1,\n        \"url2\": url2,\n        \"snapshot1\": snap1,\n        \"snapshot2\": snap2,\n    }))\n}\n\nasync fn handle_credentials_set(cmd: &Value) -> Result<Value, String> {\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name'\")?;\n    let username = cmd\n        .get(\"username\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'username'\")?;\n    let password = cmd\n        .get(\"password\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'password'\")?;\n    let url = cmd.get(\"url\").and_then(|v| v.as_str());\n    auth::credentials_set(name, username, password, url)\n}\n\nasync fn handle_credentials_get(cmd: &Value) -> Result<Value, String> {\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name'\")?;\n    auth::credentials_get(name)\n}\n\nasync fn handle_credentials_delete(cmd: &Value) -> Result<Value, String> {\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name'\")?;\n    auth::credentials_delete(name)\n}\n\nasync fn handle_credentials_list() -> Result<Value, String> {\n    auth::credentials_list()\n}\n\nasync fn handle_auth_show(cmd: &Value) -> Result<Value, String> {\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name'\")?;\n    auth::auth_show(name)\n}\n\nasync fn handle_mouse(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let event_type = cmd\n        .get(\"eventType\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"mouseMoved\");\n    let x = cmd.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let y = cmd.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let button = cmd.get(\"button\").and_then(|v| v.as_str()).unwrap_or(\"none\");\n    let click_count = cmd.get(\"clickCount\").and_then(|v| v.as_i64()).unwrap_or(0);\n\n    mgr.client\n        .send_command(\n            \"Input.dispatchMouseEvent\",\n            Some(json!({\n                \"type\": event_type,\n                \"x\": x,\n                \"y\": y,\n                \"button\": button,\n                \"clickCount\": click_count,\n            })),\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"dispatched\": event_type }))\n}\n\nasync fn handle_keyboard(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let event_type = cmd\n        .get(\"eventType\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"keyDown\");\n    let key = cmd.get(\"key\").and_then(|v| v.as_str());\n    let code = cmd.get(\"code\").and_then(|v| v.as_str());\n    let text = cmd.get(\"text\").and_then(|v| v.as_str());\n\n    let mut params = json!({ \"type\": event_type });\n    if let Some(k) = key {\n        params[\"key\"] = Value::String(k.to_string());\n    }\n    if let Some(c) = code {\n        params[\"code\"] = Value::String(c.to_string());\n    }\n    if let Some(t) = text {\n        params[\"text\"] = Value::String(t.to_string());\n    }\n\n    mgr.client\n        .send_command(\"Input.dispatchKeyEvent\", Some(params), Some(&session_id))\n        .await?;\n\n    Ok(json!({ \"dispatched\": event_type }))\n}\n\n// ---------------------------------------------------------------------------\n// Phase 5 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_tab_list(state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let tabs = mgr.tab_list();\n    Ok(json!({ \"tabs\": tabs }))\n}\n\nasync fn handle_tab_new(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n    let url = cmd.get(\"url\").and_then(|v| v.as_str());\n    state.ref_map.clear();\n    mgr.tab_new(url).await\n}\n\nasync fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n    let index = cmd\n        .get(\"index\")\n        .and_then(|v| v.as_u64())\n        .ok_or(\"Missing 'index' parameter\")? as usize;\n    state.ref_map.clear();\n    mgr.tab_switch(index).await\n}\n\nasync fn handle_tab_close(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n    let index = cmd\n        .get(\"index\")\n        .and_then(|v| v.as_u64())\n        .map(|i| i as usize);\n    state.ref_map.clear();\n    mgr.tab_close(index).await\n}\n\nasync fn handle_viewport(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let width = cmd.get(\"width\").and_then(|v| v.as_i64()).unwrap_or(1280) as i32;\n    let height = cmd.get(\"height\").and_then(|v| v.as_i64()).unwrap_or(720) as i32;\n    let scale = cmd\n        .get(\"deviceScaleFactor\")\n        .and_then(|v| v.as_f64())\n        .unwrap_or(1.0);\n    let mobile = cmd.get(\"mobile\").and_then(|v| v.as_bool()).unwrap_or(false);\n\n    mgr.set_viewport(width, height, scale, mobile).await?;\n    Ok(json!({ \"width\": width, \"height\": height, \"deviceScaleFactor\": scale, \"mobile\": mobile }))\n}\n\nasync fn handle_user_agent(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let ua = cmd\n        .get(\"userAgent\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'userAgent' parameter\")?;\n    mgr.set_user_agent(ua).await?;\n    Ok(json!({ \"userAgent\": ua }))\n}\n\nasync fn handle_set_media(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let media = cmd.get(\"media\").and_then(|v| v.as_str());\n\n    let features = cmd.get(\"features\").and_then(|v| v.as_object()).map(|m| {\n        m.iter()\n            .map(|(k, v)| (k.clone(), v.as_str().unwrap_or(\"\").to_string()))\n            .collect::<Vec<(String, String)>>()\n    });\n\n    mgr.set_emulated_media(media, features).await?;\n    Ok(json!({ \"set\": true }))\n}\n\nasync fn handle_download(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n    mgr.set_download_behavior(path).await?;\n    Ok(json!({ \"downloadPath\": path }))\n}\n\n// ---------------------------------------------------------------------------\n// Phase 4 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_trace_start(state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    native_tracing::trace_start(&mgr.client, &session_id, &mut state.tracing_state).await\n}\n\nasync fn handle_trace_stop(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let path = cmd.get(\"path\").and_then(|v| v.as_str());\n    native_tracing::trace_stop(&mgr.client, &session_id, &mut state.tracing_state, path).await\n}\n\nasync fn handle_profiler_start(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let categories = cmd.get(\"categories\").and_then(|v| v.as_array()).map(|arr| {\n        arr.iter()\n            .filter_map(|v| v.as_str().map(String::from))\n            .collect()\n    });\n    native_tracing::profiler_start(\n        &mgr.client,\n        &session_id,\n        &mut state.tracing_state,\n        categories,\n    )\n    .await\n}\n\nasync fn handle_profiler_stop(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let path = cmd.get(\"path\").and_then(|v| v.as_str());\n    native_tracing::profiler_stop(&mgr.client, &session_id, &mut state.tracing_state, path).await\n}\n\nasync fn handle_recording_start(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n\n    let recording_url = cmd\n        .get(\"url\")\n        .and_then(|v| v.as_str())\n        .filter(|s| !s.is_empty());\n\n    let (client, new_session_id) = {\n        let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n        let old_session_id = mgr.active_session_id()?.to_string();\n\n        // Capture current URL if no URL specified\n        let nav_url = if let Some(u) = recording_url {\n            u.to_string()\n        } else {\n            mgr.get_url()\n                .await\n                .unwrap_or_else(|_| \"about:blank\".to_string())\n        };\n\n        // Capture current cookies\n        let cookies_result = mgr\n            .client\n            .send_command_no_params(\"Network.getAllCookies\", Some(&old_session_id))\n            .await\n            .ok();\n\n        // Create new browser context\n        let ctx_result = mgr\n            .client\n            .send_command_no_params(\"Target.createBrowserContext\", None)\n            .await?;\n        let context_id = ctx_result\n            .get(\"browserContextId\")\n            .and_then(|v| v.as_str())\n            .ok_or(\"Failed to get browserContextId\")?\n            .to_string();\n\n        // Create page in new context\n        let create_result: CreateTargetResult = mgr\n            .client\n            .send_command_typed(\n                \"Target.createTarget\",\n                &json!({ \"url\": \"about:blank\", \"browserContextId\": context_id }),\n                None,\n            )\n            .await?;\n\n        let attach_result: AttachToTargetResult = mgr\n            .client\n            .send_command_typed(\n                \"Target.attachToTarget\",\n                &AttachToTargetParams {\n                    target_id: create_result.target_id.clone(),\n                    flatten: true,\n                },\n                None,\n            )\n            .await?;\n\n        let new_session_id = attach_result.session_id.clone();\n        mgr.enable_domains_pub(&new_session_id).await?;\n\n        // Transfer cookies to new context\n        if let Some(ref cr) = cookies_result {\n            if let Some(cookie_arr) = cr.get(\"cookies\").and_then(|v| v.as_array()) {\n                if !cookie_arr.is_empty() {\n                    let _ = mgr\n                        .client\n                        .send_command(\n                            \"Network.setCookies\",\n                            Some(json!({ \"cookies\": cookie_arr })),\n                            Some(&new_session_id),\n                        )\n                        .await;\n                }\n            }\n        }\n\n        // Add page and switch to it\n        mgr.add_page(super::browser::PageInfo {\n            target_id: create_result.target_id,\n            session_id: new_session_id.clone(),\n            url: nav_url.clone(),\n            title: String::new(),\n            target_type: \"page\".to_string(),\n        });\n\n        // Navigate to URL\n        if nav_url != \"about:blank\" {\n            let _ = mgr\n                .client\n                .send_command(\n                    \"Page.navigate\",\n                    Some(json!({ \"url\": nav_url })),\n                    Some(&new_session_id),\n                )\n                .await;\n            tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;\n        }\n\n        (mgr.client.clone(), new_session_id)\n    };\n\n    let result = recording::recording_start(&mut state.recording_state, path)?;\n    state.start_recording_task(client, new_session_id).await?;\n\n    Ok(result)\n}\n\nasync fn handle_recording_stop(state: &mut DaemonState) -> Result<Value, String> {\n    state.stop_recording_task().await?;\n    recording::recording_stop(&mut state.recording_state)\n}\n\nasync fn handle_recording_restart(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n\n    let _ = state.stop_recording_task().await;\n    let result = recording::recording_restart(&mut state.recording_state, path)?;\n\n    if let Some(ref browser) = state.browser {\n        let session_id = browser.active_session_id()?.to_string();\n        state\n            .start_recording_task(browser.client.clone(), session_id)\n            .await?;\n    }\n\n    Ok(result)\n}\n\nasync fn handle_pdf(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let params = json!({\n        \"printBackground\": cmd.get(\"printBackground\").and_then(|v| v.as_bool()).unwrap_or(true),\n        \"landscape\": cmd.get(\"landscape\").and_then(|v| v.as_bool()).unwrap_or(false),\n        \"preferCSSPageSize\": cmd.get(\"preferCSSPageSize\").and_then(|v| v.as_bool()).unwrap_or(false),\n    });\n\n    let result = mgr\n        .client\n        .send_command(\"Page.printToPDF\", Some(params), Some(&session_id))\n        .await?;\n\n    let data = result\n        .get(\"data\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"No PDF data returned\")?;\n\n    let path = cmd.get(\"path\").and_then(|v| v.as_str());\n    let save_path = match path {\n        Some(p) => p.to_string(),\n        None => {\n            let dir = dirs::home_dir()\n                .unwrap_or_else(std::env::temp_dir)\n                .join(\".agent-browser\")\n                .join(\"tmp\")\n                .join(\"pdfs\");\n            let _ = std::fs::create_dir_all(&dir);\n            let timestamp = std::time::SystemTime::now()\n                .duration_since(std::time::UNIX_EPOCH)\n                .unwrap_or_default()\n                .as_millis();\n            dir.join(format!(\"page-{}.pdf\", timestamp))\n                .to_string_lossy()\n                .to_string()\n        }\n    };\n\n    let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)\n        .map_err(|e| format!(\"Failed to decode PDF: {}\", e))?;\n    std::fs::write(&save_path, &bytes).map_err(|e| format!(\"Failed to save PDF: {}\", e))?;\n\n    Ok(json!({ \"path\": save_path }))\n}\n\n// ---------------------------------------------------------------------------\n// Phase 8 handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_focus(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::focus(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"focused\": selector }))\n}\n\nasync fn handle_clear(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::clear(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"cleared\": selector }))\n}\n\nasync fn handle_selectall(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::select_all(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"selected\": selector }))\n}\n\nasync fn handle_scrollintoview(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::scroll_into_view(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"scrolled\": selector }))\n}\n\nasync fn handle_dispatch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let event_type = cmd\n        .get(\"event\")\n        .or_else(|| cmd.get(\"eventType\"))\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'event' parameter\")?;\n    let event_init = cmd.get(\"eventInit\");\n\n    interaction::dispatch_event(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        selector,\n        event_type,\n        event_init,\n    )\n    .await?;\n    Ok(json!({ \"dispatched\": event_type, \"selector\": selector }))\n}\n\nasync fn handle_highlight(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    interaction::highlight(&mgr.client, &session_id, &state.ref_map, selector).await?;\n    Ok(json!({ \"highlighted\": selector }))\n}\n\nasync fn handle_tap(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let selector = cmd.get(\"selector\").and_then(|v| v.as_str());\n\n    // Route through Appium for iOS/WebDriver using coordinate-based tap\n    if let Some(ref appium) = state.appium {\n        if state.browser.is_none() {\n            let x = cmd.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(200.0);\n            let y = cmd.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(200.0);\n            appium.tap(x, y).await?;\n            return Ok(json!({ \"tapped\": true, \"x\": x, \"y\": y }));\n        }\n    }\n\n    let sel = selector.ok_or(\"Missing 'selector' parameter\")?;\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    interaction::tap_touch(&mgr.client, &session_id, &state.ref_map, sel).await?;\n    Ok(json!({ \"tapped\": sel }))\n}\n\nasync fn handle_boundingbox(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let bbox = super::element::get_element_bounding_box(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        selector,\n    )\n    .await?;\n    Ok(bbox)\n}\n\nasync fn handle_innertext(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let text =\n        super::element::get_element_inner_text(&mgr.client, &session_id, &state.ref_map, selector)\n            .await?;\n    Ok(json!({ \"text\": text }))\n}\n\nasync fn handle_innerhtml(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let html =\n        super::element::get_element_inner_html(&mgr.client, &session_id, &state.ref_map, selector)\n            .await?;\n    Ok(json!({ \"html\": html }))\n}\n\nasync fn handle_inputvalue(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let value =\n        super::element::get_element_input_value(&mgr.client, &session_id, &state.ref_map, selector)\n            .await?;\n    Ok(json!({ \"value\": value }))\n}\n\nasync fn handle_setvalue(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let value = cmd\n        .get(\"value\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'value' parameter\")?;\n\n    super::element::set_element_value(&mgr.client, &session_id, &state.ref_map, selector, value)\n        .await?;\n    Ok(json!({ \"set\": selector, \"value\": value }))\n}\n\nasync fn handle_count(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let count = super::element::get_element_count(&mgr.client, &session_id, selector).await?;\n    Ok(json!({ \"count\": count, \"selector\": selector }))\n}\n\nasync fn handle_styles(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let properties = cmd.get(\"properties\").and_then(|v| v.as_array()).map(|arr| {\n        arr.iter()\n            .filter_map(|v| v.as_str().map(String::from))\n            .collect()\n    });\n\n    let styles = super::element::get_element_styles(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        selector,\n        properties,\n    )\n    .await?;\n    Ok(json!({ \"styles\": styles }))\n}\n\nasync fn handle_bringtofront(state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    mgr.bring_to_front().await?;\n    Ok(json!({ \"broughtToFront\": true }))\n}\n\nasync fn handle_timezone(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let timezone = cmd\n        .get(\"timezoneId\")\n        .or_else(|| cmd.get(\"timezone\"))\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'timezoneId' parameter\")?;\n    mgr.set_timezone(timezone).await?;\n    Ok(json!({ \"timezoneId\": timezone }))\n}\n\nasync fn handle_locale(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let locale = cmd\n        .get(\"locale\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'locale' parameter\")?;\n    mgr.set_locale(locale).await?;\n    Ok(json!({ \"locale\": locale }))\n}\n\nasync fn handle_geolocation(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let latitude = cmd\n        .get(\"latitude\")\n        .and_then(|v| v.as_f64())\n        .ok_or(\"Missing 'latitude' parameter\")?;\n    let longitude = cmd\n        .get(\"longitude\")\n        .and_then(|v| v.as_f64())\n        .ok_or(\"Missing 'longitude' parameter\")?;\n    let accuracy = cmd.get(\"accuracy\").and_then(|v| v.as_f64());\n\n    mgr.set_geolocation(latitude, longitude, accuracy).await?;\n    Ok(json!({ \"latitude\": latitude, \"longitude\": longitude }))\n}\n\nasync fn handle_permissions(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let permissions: Vec<String> = cmd\n        .get(\"permissions\")\n        .and_then(|v| v.as_array())\n        .map(|arr| {\n            arr.iter()\n                .filter_map(|v| v.as_str().map(String::from))\n                .collect()\n        })\n        .unwrap_or_default();\n\n    mgr.grant_permissions(&permissions).await?;\n    Ok(json!({ \"granted\": permissions }))\n}\n\nasync fn handle_dialog(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let accept = cmd\n        .get(\"response\")\n        .and_then(|v| v.as_str())\n        .map(|r| r == \"accept\")\n        .or_else(|| cmd.get(\"accept\").and_then(|v| v.as_bool()))\n        .unwrap_or(true);\n    let prompt_text = cmd.get(\"promptText\").and_then(|v| v.as_str());\n\n    mgr.handle_dialog(accept, prompt_text).await?;\n    Ok(json!({ \"handled\": true, \"accepted\": accept }))\n}\n\nasync fn handle_upload(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let files: Vec<String> = cmd\n        .get(\"files\")\n        .and_then(|v| v.as_array())\n        .map(|arr| {\n            arr.iter()\n                .filter_map(|v| v.as_str().map(String::from))\n                .collect()\n        })\n        .or_else(|| {\n            cmd.get(\"file\")\n                .and_then(|v| v.as_str())\n                .map(|s| vec![s.to_string()])\n        })\n        .unwrap_or_default();\n\n    mgr.upload_files(selector, &files).await?;\n    Ok(json!({ \"uploaded\": files.len(), \"selector\": selector }))\n}\n\nasync fn handle_addscript(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let content = cmd\n        .get(\"content\")\n        .or_else(|| cmd.get(\"source\"))\n        .or_else(|| cmd.get(\"script\"))\n        .and_then(|v| v.as_str());\n    let url = cmd.get(\"url\").and_then(|v| v.as_str());\n\n    if content.is_none() && url.is_none() {\n        return Err(\"At least one of 'content' or 'url' is required\".to_string());\n    }\n\n    if let Some(src_url) = url {\n        let js = format!(\n            r#\"new Promise((resolve, reject) => {{\n                const s = document.createElement('script');\n                s.src = {};\n                s.onload = () => resolve(true);\n                s.onerror = () => reject(new Error('Failed to load script'));\n                document.head.appendChild(s);\n            }})\"#,\n            serde_json::to_string(src_url).unwrap_or_default()\n        );\n        mgr.evaluate(&js, None).await?;\n    } else if let Some(source) = content {\n        let js = format!(\n            r#\"(() => {{\n                const s = document.createElement('script');\n                s.textContent = {};\n                document.head.appendChild(s);\n            }})()\"#,\n            serde_json::to_string(source).unwrap_or_default()\n        );\n        mgr.evaluate(&js, None).await?;\n    }\n\n    Ok(json!({ \"added\": true }))\n}\n\nasync fn handle_addinitscript(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let source = cmd\n        .get(\"script\")\n        .or_else(|| cmd.get(\"source\"))\n        .or_else(|| cmd.get(\"content\"))\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'script' parameter\")?;\n\n    let identifier = mgr.add_script_to_evaluate(source).await?;\n    Ok(json!({ \"added\": true, \"identifier\": identifier }))\n}\n\nasync fn handle_addstyle(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let content = cmd\n        .get(\"content\")\n        .or_else(|| cmd.get(\"css\"))\n        .and_then(|v| v.as_str());\n    let url = cmd.get(\"url\").and_then(|v| v.as_str());\n\n    if content.is_none() && url.is_none() {\n        return Err(\"At least one of 'content' or 'url' is required\".to_string());\n    }\n\n    if let Some(href) = url {\n        let js = format!(\n            r#\"new Promise((resolve, reject) => {{\n                const link = document.createElement('link');\n                link.rel = 'stylesheet';\n                link.href = {};\n                link.onload = () => resolve(true);\n                link.onerror = () => reject(new Error('Failed to load stylesheet'));\n                document.head.appendChild(link);\n            }})\"#,\n            serde_json::to_string(href).unwrap_or_default()\n        );\n        mgr.evaluate(&js, None).await?;\n    } else if let Some(css) = content {\n        let js = format!(\n            r#\"(() => {{\n                const style = document.createElement('style');\n                style.textContent = {};\n                document.head.appendChild(style);\n            }})()\"#,\n            serde_json::to_string(css).unwrap_or_default()\n        );\n        mgr.evaluate(&js, None).await?;\n    }\n\n    Ok(json!({ \"added\": true }))\n}\n\nasync fn handle_clipboard(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let action = cmd\n        .get(\"subAction\")\n        .or_else(|| cmd.get(\"operation\"))\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"read\");\n\n    let session_id = mgr.active_session_id()?.to_string();\n\n    // cfg! is compile-time; assumes the browser runs on the same OS as the CLI binary.\n    let modifier: i32 = if cfg!(target_os = \"macos\") { 4 } else { 2 };\n\n    match action {\n        \"write\" => {\n            let text = cmd\n                .get(\"text\")\n                .or_else(|| cmd.get(\"value\"))\n                .and_then(|v| v.as_str())\n                .ok_or(\"Missing 'text' parameter\")?;\n            let js = format!(\n                \"navigator.clipboard.writeText({})\",\n                serde_json::to_string(text).unwrap_or_default()\n            );\n            mgr.evaluate(&js, None).await?;\n            Ok(json!({ \"written\": text }))\n        }\n        \"copy\" => {\n            interaction::press_key_with_modifiers(&mgr.client, &session_id, \"c\", Some(modifier))\n                .await?;\n            Ok(json!({ \"copied\": true }))\n        }\n        \"paste\" => {\n            interaction::press_key_with_modifiers(&mgr.client, &session_id, \"v\", Some(modifier))\n                .await?;\n            Ok(json!({ \"pasted\": true }))\n        }\n        _ => {\n            let result = mgr.evaluate(\"navigator.clipboard.readText()\", None).await?;\n            Ok(json!({ \"text\": result }))\n        }\n    }\n}\n\nasync fn handle_wheel(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let x = cmd.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let y = cmd.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let delta_x = cmd.get(\"deltaX\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let delta_y = cmd.get(\"deltaY\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n\n    mgr.client\n        .send_command(\n            \"Input.dispatchMouseEvent\",\n            Some(json!({\n                \"type\": \"mouseWheel\",\n                \"x\": x,\n                \"y\": y,\n                \"deltaX\": delta_x,\n                \"deltaY\": delta_y,\n            })),\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"scrolled\": true, \"deltaX\": delta_x, \"deltaY\": delta_y }))\n}\n\nasync fn handle_device(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let name = cmd\n        .get(\"name\")\n        .or_else(|| cmd.get(\"device\"))\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name' parameter\")?;\n\n    let (width, height, scale, mobile, ua) = match name.to_lowercase().as_str() {\n        \"iphone 12\" | \"iphone12\" => (390, 844, 3.0, true, \"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1\"),\n        \"iphone 14\" | \"iphone14\" => (390, 844, 3.0, true, \"Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1\"),\n        \"iphone 15\" | \"iphone15\" => (393, 852, 3.0, true, \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1\"),\n        \"ipad\" | \"ipad air\" => (820, 1180, 2.0, true, \"Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/604.1\"),\n        \"ipad pro\" => (1024, 1366, 2.0, true, \"Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/604.1\"),\n        \"pixel 5\" | \"pixel5\" => (393, 851, 2.75, true, \"Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36\"),\n        \"pixel 7\" | \"pixel7\" => (412, 915, 2.625, true, \"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36\"),\n        \"galaxy s21\" | \"galaxys21\" => (360, 800, 3.0, true, \"Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36\"),\n        _ => return Err(format!(\"Unknown device: {}. Supported: iPhone 12, iPhone 14, iPhone 15, iPad, iPad Pro, Pixel 5, Pixel 7, Galaxy S21\", name)),\n    };\n\n    mgr.set_viewport(width, height, scale, mobile).await?;\n    mgr.set_user_agent(ua).await?;\n\n    Ok(json!({\n        \"device\": name,\n        \"width\": width,\n        \"height\": height,\n        \"deviceScaleFactor\": scale,\n        \"mobile\": mobile,\n    }))\n}\n\n// ---------------------------------------------------------------------------\n// Screencast handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_screencast_start(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    if state.screencasting {\n        return Err(\"Screencast already active\".to_string());\n    }\n\n    let format = cmd.get(\"format\").and_then(|v| v.as_str()).unwrap_or(\"jpeg\");\n    let quality = cmd.get(\"quality\").and_then(|v| v.as_i64()).unwrap_or(80) as i32;\n    let max_width = cmd.get(\"maxWidth\").and_then(|v| v.as_i64()).unwrap_or(1280) as i32;\n    let max_height = cmd.get(\"maxHeight\").and_then(|v| v.as_i64()).unwrap_or(720) as i32;\n\n    stream::start_screencast(\n        &mgr.client,\n        &session_id,\n        format,\n        quality,\n        max_width,\n        max_height,\n    )\n    .await?;\n    state.screencasting = true;\n\n    if let Some(ref server) = state.stream_server {\n        server.broadcast_status(true, true, max_width as u32, max_height as u32);\n    }\n\n    Ok(json!({ \"started\": true }))\n}\n\nasync fn handle_screencast_stop(state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?;\n\n    if !state.screencasting {\n        return Err(\"No screencast active\".to_string());\n    }\n\n    stream::stop_screencast(&mgr.client, session_id).await?;\n    state.screencasting = false;\n\n    if let Some(ref server) = state.stream_server {\n        server.broadcast_status(true, false, 0, 0);\n    }\n\n    Ok(json!({ \"stopped\": true }))\n}\n\n// ---------------------------------------------------------------------------\n// Wait variant handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_waitforurl(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let url_pattern = cmd\n        .get(\"url\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url' parameter\")?;\n    let timeout_ms = cmd.get(\"timeout\").and_then(|v| v.as_u64()).unwrap_or(30000);\n\n    wait_for_url(&mgr.client, &session_id, url_pattern, timeout_ms).await?;\n    let url = mgr.get_url().await.unwrap_or_default();\n    Ok(json!({ \"url\": url }))\n}\n\nasync fn handle_waitforloadstate(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let load_state = cmd.get(\"state\").and_then(|v| v.as_str()).unwrap_or(\"load\");\n    let timeout_ms = cmd.get(\"timeout\").and_then(|v| v.as_u64()).unwrap_or(30000);\n\n    let wait_until = WaitUntil::from_str(load_state);\n    let _ = tokio::time::timeout(\n        tokio::time::Duration::from_millis(timeout_ms),\n        mgr.wait_for_lifecycle_external(wait_until, &session_id),\n    )\n    .await\n    .map_err(|_| format!(\"Timeout waiting for load state: {}\", load_state))?;\n\n    Ok(json!({ \"state\": load_state }))\n}\n\nasync fn handle_waitforfunction(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let expression = cmd\n        .get(\"expression\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'expression' parameter\")?;\n    let timeout_ms = cmd.get(\"timeout\").and_then(|v| v.as_u64()).unwrap_or(30000);\n\n    wait_for_function(&mgr.client, &session_id, expression, timeout_ms).await?;\n\n    let result: super::cdp::types::EvaluateResult = mgr\n        .client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &super::cdp::types::EvaluateParams {\n                expression: format!(\"({})\", expression),\n                return_by_value: Some(true),\n                await_promise: Some(true),\n            },\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"result\": result.result.value.unwrap_or(Value::Null) }))\n}\n\n// ---------------------------------------------------------------------------\n// Frame handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_frame(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let selector = cmd.get(\"selector\").and_then(|v| v.as_str());\n    let name = cmd.get(\"name\").and_then(|v| v.as_str());\n    let url = cmd.get(\"url\").and_then(|v| v.as_str());\n\n    if selector.is_none() && name.is_none() && url.is_none() {\n        return Err(\"At least one of 'selector', 'name', or 'url' is required\".to_string());\n    }\n\n    let tree_result = mgr\n        .client\n        .send_command_no_params(\"Page.getFrameTree\", Some(&session_id))\n        .await?;\n\n    fn find_frame(tree: &Value, name: Option<&str>, url: Option<&str>) -> Option<String> {\n        let frame = tree.get(\"frame\")?;\n        let frame_name = frame.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n        let frame_url = frame.get(\"url\").and_then(|v| v.as_str()).unwrap_or(\"\");\n        let frame_id = frame.get(\"id\").and_then(|v| v.as_str())?;\n\n        if let Some(n) = name {\n            if frame_name == n {\n                return Some(frame_id.to_string());\n            }\n        }\n        if let Some(u) = url {\n            if frame_url.contains(u) {\n                return Some(frame_id.to_string());\n            }\n        }\n\n        if let Some(children) = tree.get(\"childFrames\").and_then(|v| v.as_array()) {\n            for child in children {\n                if let Some(id) = find_frame(child, name, url) {\n                    return Some(id);\n                }\n            }\n        }\n        None\n    }\n\n    let frame_tree = &tree_result[\"frameTree\"];\n\n    // If selector is a ref (@e1), resolve the iframe element from the ref map\n    if let Some(sel) = selector {\n        if let Some(ref_id) = super::element::parse_ref(sel) {\n            let entry = state\n                .ref_map\n                .get(&ref_id)\n                .ok_or_else(|| format!(\"Unknown ref: {}\", ref_id))?;\n            let backend_node_id = entry\n                .backend_node_id\n                .ok_or_else(|| format!(\"Ref {} has no backend node id\", ref_id))?;\n\n            // Use DOM.describeNode to resolve the child frame ID directly.\n            // This works reliably for all iframes, including those without\n            // name, id, or src attributes.\n            let describe: Value = mgr\n                .client\n                .send_command(\n                    \"DOM.describeNode\",\n                    Some(json!({ \"backendNodeId\": backend_node_id, \"depth\": 1 })),\n                    Some(&session_id),\n                )\n                .await?;\n\n            // Verify this is an iframe/frame element\n            let node_name = describe\n                .get(\"node\")\n                .and_then(|n| n.get(\"nodeName\"))\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"\");\n            if node_name != \"IFRAME\" && node_name != \"FRAME\" {\n                return Err(\"Ref does not point to an iframe element\".to_string());\n            }\n\n            // Try contentDocument.frameId first (standard for iframes)\n            let frame_id = describe\n                .get(\"node\")\n                .and_then(|n| n.get(\"contentDocument\"))\n                .and_then(|cd| cd.get(\"frameId\"))\n                .and_then(|v| v.as_str())\n                // Fallback: the node itself may carry a frameId\n                .or_else(|| {\n                    describe\n                        .get(\"node\")\n                        .and_then(|n| n.get(\"frameId\"))\n                        .and_then(|v| v.as_str())\n                })\n                .ok_or(\"Could not resolve frame ID for iframe element\")?;\n\n            let label = describe\n                .get(\"node\")\n                .and_then(|n| n.get(\"attributes\"))\n                .and_then(|a| a.as_array())\n                .and_then(|attrs| {\n                    attrs\n                        .iter()\n                        .enumerate()\n                        .find(|(_, v)| v.as_str() == Some(\"name\"))\n                        .and_then(|(i, _)| attrs.get(i + 1))\n                        .and_then(|v| v.as_str())\n                })\n                .unwrap_or(&ref_id);\n\n            state.active_frame_id = Some(frame_id.to_string());\n            return Ok(json!({ \"frame\": label }));\n        }\n\n        // CSS selector path\n        let js = format!(\n            r#\"(() => {{\n                const el = document.querySelector({});\n                if (!el) return null;\n                if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {{\n                    return el.name || el.id || el.src || null;\n                }}\n                return null;\n            }})()\"#,\n            serde_json::to_string(sel).unwrap_or_default()\n        );\n        let result = mgr.evaluate(&js, None).await?;\n        let frame_name = result.as_str().ok_or(\"Could not find frame for selector\")?;\n        if let Some(frame_id) = find_frame(frame_tree, Some(frame_name), None) {\n            state.active_frame_id = Some(frame_id);\n            return Ok(json!({ \"frame\": frame_name }));\n        }\n    }\n\n    if let Some(frame_id) = find_frame(frame_tree, name, url) {\n        let label = name.or(url).unwrap_or(\"frame\");\n        state.active_frame_id = Some(frame_id);\n        return Ok(json!({ \"frame\": label }));\n    }\n\n    Err(\"Frame not found\".to_string())\n}\n\nasync fn handle_mainframe(state: &mut DaemonState) -> Result<Value, String> {\n    state.active_frame_id = None;\n    Ok(json!({ \"frame\": \"main\" }))\n}\n\n// ---------------------------------------------------------------------------\n// Semantic locator handlers\n// ---------------------------------------------------------------------------\n\nasync fn execute_subaction(\n    cmd: &Value,\n    state: &mut DaemonState,\n    selector: &str,\n) -> Result<Value, String> {\n    let subaction = cmd\n        .get(\"subaction\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"click\");\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    match subaction {\n        \"click\" => {\n            interaction::click(\n                &mgr.client,\n                &session_id,\n                &state.ref_map,\n                selector,\n                \"left\",\n                1,\n            )\n            .await?;\n            Ok(json!({ \"clicked\": selector }))\n        }\n        \"fill\" => {\n            let value = cmd\n                .get(\"value\")\n                .and_then(|v| v.as_str())\n                .ok_or(\"Missing 'value' for fill subaction\")?;\n            interaction::fill(&mgr.client, &session_id, &state.ref_map, selector, value).await?;\n            Ok(json!({ \"filled\": selector }))\n        }\n        \"check\" => {\n            interaction::check(&mgr.client, &session_id, &state.ref_map, selector).await?;\n            Ok(json!({ \"checked\": selector }))\n        }\n        \"hover\" => {\n            interaction::hover(&mgr.client, &session_id, &state.ref_map, selector).await?;\n            Ok(json!({ \"hovered\": selector }))\n        }\n        \"text\" => {\n            let text = super::element::get_element_text(\n                &mgr.client,\n                &session_id,\n                &state.ref_map,\n                selector,\n            )\n            .await?;\n            Ok(json!({ \"text\": text }))\n        }\n        _ => Err(format!(\"Unknown subaction: {}\", subaction)),\n    }\n}\n\nfn build_role_selector(role: &str, name: Option<&str>, exact: bool) -> String {\n    match name {\n        Some(n) => {\n            let exact_str = if exact { \", exact: true\" } else { \"\" };\n            format!(\"getByRole('{}', {{ name: '{}'{} }})\", role, n, exact_str)\n        }\n        None => format!(\"getByRole('{}')\", role),\n    }\n}\n\nasync fn resolve_semantic_locator(\n    client: &super::cdp::client::CdpClient,\n    session_id: &str,\n    strategy: &str,\n    value: &str,\n    exact: bool,\n) -> Result<String, String> {\n    let js = match strategy {\n        \"role\" => {\n            format!(\n                r#\"(() => {{\n                    const els = document.querySelectorAll('[role=\"{}\"]');\n                    if (els.length === 0) return null;\n                    return 'found';\n                }})()\"#,\n                value\n            )\n        }\n        \"text\" => {\n            let match_fn = if exact {\n                format!(\n                    \"el.textContent.trim() === {}\",\n                    serde_json::to_string(value).unwrap_or_default()\n                )\n            } else {\n                format!(\n                    \"el.textContent.includes({})\",\n                    serde_json::to_string(value).unwrap_or_default()\n                )\n            };\n            format!(\n                r#\"(() => {{\n                    const all = document.querySelectorAll('*');\n                    for (const el of all) {{\n                        if (el.children.length === 0 && {}) return 'found';\n                    }}\n                    return null;\n                }})()\"#,\n                match_fn\n            )\n        }\n        _ => return Err(format!(\"Unknown semantic strategy: {}\", strategy)),\n    };\n\n    let result: super::cdp::types::EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &super::cdp::types::EvaluateParams {\n                expression: js,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    if result\n        .result\n        .value\n        .as_ref()\n        .map(|v| v.is_null())\n        .unwrap_or(true)\n    {\n        return Err(format!(\"No element found for {} '{}'\", strategy, value));\n    }\n\n    Ok(value.to_string())\n}\n\nasync fn handle_getbyrole(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let role = cmd\n        .get(\"role\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'role' parameter\")?;\n    let name = cmd.get(\"name\").and_then(|v| v.as_str());\n    let exact = cmd.get(\"exact\").and_then(|v| v.as_bool()).unwrap_or(false);\n\n    let name_match = name\n        .map(|n| {\n            if exact {\n                format!(\n                    \"el.getAttribute('aria-label') === {} || el.textContent.trim() === {}\",\n                    serde_json::to_string(n).unwrap_or_default(),\n                    serde_json::to_string(n).unwrap_or_default()\n                )\n            } else {\n                format!(\n                    \"(el.getAttribute('aria-label') || '').includes({n}) || el.textContent.includes({n})\",\n                    n = serde_json::to_string(n).unwrap_or_default()\n                )\n            }\n        })\n        .unwrap_or_else(|| \"true\".to_string());\n\n    let js = format!(\n        r#\"(() => {{\n            const els = document.querySelectorAll('[role=\"{role}\"], {role}');\n            for (const el of els) {{\n                if ({name_match}) {{\n                    el.setAttribute('data-agent-browser-located', 'true');\n                    return true;\n                }}\n            }}\n            return false;\n        }})()\"#,\n        role = role,\n        name_match = name_match,\n    );\n\n    let result: super::cdp::types::EvaluateResult = mgr\n        .client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &super::cdp::types::EvaluateParams {\n                expression: js,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(&session_id),\n        )\n        .await?;\n\n    if !result\n        .result\n        .value\n        .as_ref()\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false)\n    {\n        let desc = build_role_selector(role, name, exact);\n        return Err(format!(\"No element found: {}\", desc));\n    }\n\n    let selector = \"[data-agent-browser-located='true']\";\n    let result = execute_subaction(cmd, state, selector).await;\n\n    // Clean up the marker attribute\n    if let Some(ref browser) = state.browser {\n        if let Ok(sid) = browser.active_session_id() {\n            let _ = browser\n                .evaluate(\n                    \"document.querySelector('[data-agent-browser-located]')?.removeAttribute('data-agent-browser-located')\",\n                    None,\n                )\n                .await;\n            let _ = sid;\n        }\n    }\n\n    result\n}\n\nasync fn handle_semantic_locator(\n    cmd: &Value,\n    state: &mut DaemonState,\n    strategy: &str,\n    param_name: &str,\n) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let value = cmd\n        .get(param_name)\n        .and_then(|v| v.as_str())\n        .ok_or(format!(\"Missing '{}' parameter\", param_name))?;\n    let exact = cmd.get(\"exact\").and_then(|v| v.as_bool()).unwrap_or(false);\n\n    let match_fn = if exact {\n        format!(\n            \"el.textContent.trim() === {}\",\n            serde_json::to_string(value).unwrap_or_default()\n        )\n    } else {\n        format!(\n            \"el.textContent.includes({})\",\n            serde_json::to_string(value).unwrap_or_default()\n        )\n    };\n\n    let query = match strategy {\n        \"label\" => format!(\n            r#\"(() => {{\n                const label = Array.from(document.querySelectorAll('label')).find(el => {match_fn});\n                if (!label) return false;\n                const forId = label.getAttribute('for');\n                const target = forId ? document.getElementById(forId) : label.querySelector('input,select,textarea');\n                if (target) {{ target.setAttribute('data-agent-browser-located', 'true'); return true; }}\n                return false;\n            }})()\"#,\n            match_fn = match_fn,\n        ),\n        \"placeholder\" => format!(\n            r#\"(() => {{\n                const el = document.querySelector('input[placeholder={val}], textarea[placeholder={val}]');\n                if (el) {{ el.setAttribute('data-agent-browser-located', 'true'); return true; }}\n                return false;\n            }})()\"#,\n            val = serde_json::to_string(value).unwrap_or_default(),\n        ),\n        \"alttext\" => format!(\n            r#\"(() => {{\n                const el = document.querySelector('img[alt={val}], [alt={val}]');\n                if (el) {{ el.setAttribute('data-agent-browser-located', 'true'); return true; }}\n                return false;\n            }})()\"#,\n            val = serde_json::to_string(value).unwrap_or_default(),\n        ),\n        \"title\" => format!(\n            r#\"(() => {{\n                const el = document.querySelector('[title={val}]');\n                if (el) {{ el.setAttribute('data-agent-browser-located', 'true'); return true; }}\n                return false;\n            }})()\"#,\n            val = serde_json::to_string(value).unwrap_or_default(),\n        ),\n        \"testid\" => format!(\n            r#\"(() => {{\n                const el = document.querySelector('[data-testid={val}]');\n                if (el) {{ el.setAttribute('data-agent-browser-located', 'true'); return true; }}\n                return false;\n            }})()\"#,\n            val = serde_json::to_string(value).unwrap_or_default(),\n        ),\n        _ => {\n            // \"text\" strategy\n            format!(\n                r#\"(() => {{\n                    const all = document.querySelectorAll('*');\n                    for (const el of all) {{\n                        if (el.children.length === 0 && {match_fn}) {{\n                            el.setAttribute('data-agent-browser-located', 'true');\n                            return true;\n                        }}\n                    }}\n                    return false;\n                }})()\"#,\n                match_fn = match_fn,\n            )\n        }\n    };\n\n    let result: super::cdp::types::EvaluateResult = mgr\n        .client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &super::cdp::types::EvaluateParams {\n                expression: query,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(&session_id),\n        )\n        .await?;\n\n    if !result\n        .result\n        .value\n        .as_ref()\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false)\n    {\n        return Err(format!(\"No element found by {} '{}'\", strategy, value));\n    }\n\n    let selector = \"[data-agent-browser-located='true']\";\n    let action_result = execute_subaction(cmd, state, selector).await;\n\n    if let Some(ref browser) = state.browser {\n        let _ = browser\n            .evaluate(\n                \"document.querySelector('[data-agent-browser-located]')?.removeAttribute('data-agent-browser-located')\",\n                None,\n            )\n            .await;\n    }\n\n    action_result\n}\n\nasync fn handle_getbytext(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    handle_semantic_locator(cmd, state, \"text\", \"text\").await\n}\n\nasync fn handle_getbylabel(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    handle_semantic_locator(cmd, state, \"label\", \"label\").await\n}\n\nasync fn handle_getbyplaceholder(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    handle_semantic_locator(cmd, state, \"placeholder\", \"placeholder\").await\n}\n\nasync fn handle_getbyalttext(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    handle_semantic_locator(cmd, state, \"alttext\", \"text\").await\n}\n\nasync fn handle_getbytitle(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    handle_semantic_locator(cmd, state, \"title\", \"text\").await\n}\n\nasync fn handle_getbytestid(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    handle_semantic_locator(cmd, state, \"testid\", \"testId\").await\n}\n\nasync fn handle_nth(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let index = cmd\n        .get(\"index\")\n        .and_then(|v| v.as_i64())\n        .ok_or(\"Missing 'index' parameter\")?;\n\n    let js = format!(\n        r#\"(() => {{\n            const els = document.querySelectorAll({sel});\n            const idx = {idx} < 0 ? els.length + {idx} : {idx};\n            if (idx < 0 || idx >= els.length) return false;\n            els[idx].setAttribute('data-agent-browser-located', 'true');\n            return true;\n        }})()\"#,\n        sel = serde_json::to_string(selector).unwrap_or_default(),\n        idx = index,\n    );\n\n    let result: super::cdp::types::EvaluateResult = mgr\n        .client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &super::cdp::types::EvaluateParams {\n                expression: js,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(&session_id),\n        )\n        .await?;\n\n    if !result\n        .result\n        .value\n        .as_ref()\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false)\n    {\n        return Err(format!(\n            \"No element at index {} for selector '{}'\",\n            index, selector\n        ));\n    }\n\n    let located = \"[data-agent-browser-located='true']\";\n    let action_result = execute_subaction(cmd, state, located).await;\n\n    if let Some(ref browser) = state.browser {\n        let _ = browser\n            .evaluate(\n                \"document.querySelector('[data-agent-browser-located]')?.removeAttribute('data-agent-browser-located')\",\n                None,\n            )\n            .await;\n    }\n\n    action_result\n}\n\nasync fn handle_find(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let _session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n\n    let js = format!(\n        r#\"(() => {{\n            const els = document.querySelectorAll({});\n            return Array.from(els).map((el, i) => ({{\n                index: i,\n                tagName: el.tagName.toLowerCase(),\n                text: el.textContent?.trim().substring(0, 100) || '',\n                visible: el.offsetWidth > 0 && el.offsetHeight > 0,\n            }}));\n        }})()\"#,\n        serde_json::to_string(selector).unwrap_or_default()\n    );\n\n    let result = mgr.evaluate(&js, None).await?;\n    Ok(json!({ \"elements\": result, \"selector\": selector }))\n}\n\nasync fn handle_evalhandle(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let script = cmd\n        .get(\"script\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'script' parameter\")?;\n\n    let result: super::cdp::types::EvaluateResult = mgr\n        .client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &super::cdp::types::EvaluateParams {\n                expression: script.to_string(),\n                return_by_value: Some(false),\n                await_promise: Some(true),\n            },\n            Some(&session_id),\n        )\n        .await?;\n\n    let handle = result.result.object_id.unwrap_or_default();\n    Ok(json!({ \"handle\": handle }))\n}\n\n// ---------------------------------------------------------------------------\n// Advanced interaction handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_drag(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let source = cmd\n        .get(\"source\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'source' parameter\")?;\n    let target = cmd\n        .get(\"target\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'target' parameter\")?;\n\n    let (sx, sy) =\n        super::element::resolve_element_center(&mgr.client, &session_id, &state.ref_map, source)\n            .await?;\n    let (tx, ty) =\n        super::element::resolve_element_center(&mgr.client, &session_id, &state.ref_map, target)\n            .await?;\n\n    // Mouse down at source\n    mgr.client\n        .send_command(\n            \"Input.dispatchMouseEvent\",\n            Some(json!({ \"type\": \"mouseMoved\", \"x\": sx, \"y\": sy })),\n            Some(&session_id),\n        )\n        .await?;\n    mgr.client\n        .send_command(\n            \"Input.dispatchMouseEvent\",\n            Some(json!({ \"type\": \"mousePressed\", \"x\": sx, \"y\": sy, \"button\": \"left\", \"clickCount\": 1 })),\n            Some(&session_id),\n        )\n        .await?;\n\n    // Move in steps to target\n    let steps = 10;\n    for i in 1..=steps {\n        let cx = sx + (tx - sx) * (i as f64) / (steps as f64);\n        let cy = sy + (ty - sy) * (i as f64) / (steps as f64);\n        mgr.client\n            .send_command(\n                \"Input.dispatchMouseEvent\",\n                Some(json!({ \"type\": \"mouseMoved\", \"x\": cx, \"y\": cy })),\n                Some(&session_id),\n            )\n            .await?;\n        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;\n    }\n\n    // Mouse up at target\n    mgr.client\n        .send_command(\n            \"Input.dispatchMouseEvent\",\n            Some(json!({ \"type\": \"mouseReleased\", \"x\": tx, \"y\": ty, \"button\": \"left\", \"clickCount\": 1 })),\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"dragged\": true, \"source\": source, \"target\": target }))\n}\n\nasync fn handle_expose(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name' parameter\")?;\n\n    mgr.client\n        .send_command(\n            \"Runtime.addBinding\",\n            Some(json!({ \"name\": name })),\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"exposed\": name }))\n}\n\nasync fn handle_pause(_state: &DaemonState) -> Result<Value, String> {\n    Ok(json!({ \"paused\": true, \"note\": \"Use DevTools to inspect. The daemon remains running.\" }))\n}\n\nasync fn handle_multiselect(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let _session_id = mgr.active_session_id()?.to_string();\n    let selector = cmd\n        .get(\"selector\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'selector' parameter\")?;\n    let values: Vec<String> = cmd\n        .get(\"values\")\n        .and_then(|v| v.as_array())\n        .map(|arr| {\n            arr.iter()\n                .filter_map(|v| v.as_str().map(String::from))\n                .collect()\n        })\n        .unwrap_or_default();\n\n    let values_json = serde_json::to_string(&values).unwrap_or(\"[]\".to_string());\n    let js = format!(\n        r#\"(() => {{\n            const select = document.querySelector({sel});\n            if (!select) throw new Error('Select element not found');\n            const vals = {vals};\n            for (const opt of select.options) {{\n                opt.selected = vals.includes(opt.value);\n            }}\n            select.dispatchEvent(new Event('change', {{ bubbles: true }}));\n            return Array.from(select.selectedOptions).map(o => o.value);\n        }})()\"#,\n        sel = serde_json::to_string(selector).unwrap_or_default(),\n        vals = values_json,\n    );\n\n    let result = mgr.evaluate(&js, None).await?;\n    Ok(json!({ \"selected\": result }))\n}\n\nasync fn handle_responsebody(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let url_pattern = cmd\n        .get(\"url\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url' parameter\")?;\n    let timeout_ms = cmd.get(\"timeout\").and_then(|v| v.as_u64()).unwrap_or(30000);\n\n    let mut rx = mgr.client.subscribe();\n    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);\n\n    loop {\n        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());\n        if remaining.is_zero() {\n            return Err(format!(\n                \"Timeout waiting for response matching '{}'\",\n                url_pattern\n            ));\n        }\n\n        match tokio::time::timeout(remaining, rx.recv()).await {\n            Ok(Ok(event)) => {\n                if event.method == \"Network.responseReceived\"\n                    && event.session_id.as_deref() == Some(&session_id)\n                {\n                    if let Some(resp_url) = event\n                        .params\n                        .get(\"response\")\n                        .and_then(|r| r.get(\"url\"))\n                        .and_then(|u| u.as_str())\n                    {\n                        if resp_url.contains(url_pattern) {\n                            let request_id = event\n                                .params\n                                .get(\"requestId\")\n                                .and_then(|v| v.as_str())\n                                .ok_or(\"No requestId in response event\")?;\n                            let status = event\n                                .params\n                                .get(\"response\")\n                                .and_then(|r| r.get(\"status\"))\n                                .and_then(|v| v.as_i64())\n                                .unwrap_or(0);\n                            let headers = event\n                                .params\n                                .get(\"response\")\n                                .and_then(|r| r.get(\"headers\"))\n                                .cloned()\n                                .unwrap_or(json!({}));\n\n                            let body_result = mgr\n                                .client\n                                .send_command(\n                                    \"Network.getResponseBody\",\n                                    Some(json!({ \"requestId\": request_id })),\n                                    Some(&session_id),\n                                )\n                                .await?;\n                            let body = body_result\n                                .get(\"body\")\n                                .and_then(|v| v.as_str())\n                                .unwrap_or(\"\");\n\n                            return Ok(\n                                json!({ \"body\": body, \"status\": status, \"headers\": headers }),\n                            );\n                        }\n                    }\n                }\n            }\n            Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,\n            Ok(Err(_)) => return Err(\"Event stream closed\".to_string()),\n            Err(_) => {\n                return Err(format!(\n                    \"Timeout waiting for response matching '{}'\",\n                    url_pattern\n                ));\n            }\n        }\n    }\n}\n\nasync fn handle_waitfordownload(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let timeout_ms = cmd.get(\"timeout\").and_then(|v| v.as_u64()).unwrap_or(30000);\n\n    let mut rx = mgr.client.subscribe();\n    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);\n\n    loop {\n        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());\n        if remaining.is_zero() {\n            return Err(\"Timeout waiting for download\".to_string());\n        }\n\n        match tokio::time::timeout(remaining, rx.recv()).await {\n            Ok(Ok(event)) => {\n                if event.method == \"Page.downloadProgress\"\n                    && event.session_id.as_deref() == Some(&session_id)\n                    && event.params.get(\"state\").and_then(|v| v.as_str()) == Some(\"completed\")\n                {\n                    let path = cmd\n                        .get(\"path\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"download\");\n                    return Ok(json!({ \"path\": path }));\n                }\n            }\n            Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,\n            Ok(Err(_)) => return Err(\"Event stream closed\".to_string()),\n            Err(_) => return Err(\"Timeout waiting for download\".to_string()),\n        }\n    }\n}\n\nasync fn handle_window_new(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n\n    // Create a new browser context\n    let context_result = mgr\n        .client\n        .send_command_no_params(\"Target.createBrowserContext\", None)\n        .await?;\n    let context_id = context_result\n        .get(\"browserContextId\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Failed to create browser context\")?\n        .to_string();\n\n    let create_result: super::cdp::types::CreateTargetResult = mgr\n        .client\n        .send_command_typed(\n            \"Target.createTarget\",\n            &json!({ \"url\": \"about:blank\", \"browserContextId\": context_id }),\n            None,\n        )\n        .await?;\n\n    let attach: super::cdp::types::AttachToTargetResult = mgr\n        .client\n        .send_command_typed(\n            \"Target.attachToTarget\",\n            &super::cdp::types::AttachToTargetParams {\n                target_id: create_result.target_id.clone(),\n                flatten: true,\n            },\n            None,\n        )\n        .await?;\n\n    mgr.add_page(super::browser::PageInfo {\n        target_id: create_result.target_id,\n        session_id: attach.session_id,\n        url: \"about:blank\".to_string(),\n        title: String::new(),\n        target_type: \"page\".to_string(),\n    });\n\n    if let Some(viewport) = cmd.get(\"viewport\") {\n        let width = viewport\n            .get(\"width\")\n            .and_then(|v| v.as_i64())\n            .unwrap_or(1280) as i32;\n        let height = viewport\n            .get(\"height\")\n            .and_then(|v| v.as_i64())\n            .unwrap_or(720) as i32;\n        mgr.set_viewport(width, height, 1.0, false).await?;\n    }\n\n    let total = mgr.page_count();\n    state.ref_map.clear();\n\n    Ok(json!({ \"index\": total - 1, \"total\": total }))\n}\n\nasync fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let baseline_path = cmd\n        .get(\"baseline\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'baseline' parameter\")?;\n\n    let threshold = cmd.get(\"threshold\").and_then(|v| v.as_f64()).unwrap_or(0.1);\n\n    let options = ScreenshotOptions {\n        selector: cmd\n            .get(\"selector\")\n            .and_then(|v| v.as_str())\n            .map(String::from),\n        path: None,\n        full_page: cmd\n            .get(\"fullPage\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false),\n        format: \"png\".to_string(),\n        quality: None,\n        annotate: false,\n        output_dir: None,\n    };\n\n    let result =\n        screenshot::take_screenshot(&mgr.client, &session_id, &state.ref_map, &options).await?;\n\n    let current_bytes =\n        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &result.base64)\n            .map_err(|e| format!(\"Failed to decode screenshot: {}\", e))?;\n\n    let baseline_bytes =\n        std::fs::read(baseline_path).map_err(|e| format!(\"Failed to read baseline: {}\", e))?;\n\n    let result = diff::diff_screenshot(&baseline_bytes, &current_bytes, threshold)?;\n\n    let output_path = cmd.get(\"output\").and_then(|v| v.as_str());\n    if let (Some(out_path), Some(ref diff_data)) = (output_path, &result.diff_image) {\n        std::fs::write(out_path, diff_data)\n            .map_err(|e| format!(\"Failed to write diff image: {}\", e))?;\n    }\n\n    Ok(json!({\n        \"match\": result.matched,\n        \"mismatchPercentage\": result.mismatch_percentage,\n        \"totalPixels\": result.total_pixels,\n        \"differentPixels\": result.different_pixels,\n        \"diffPath\": output_path,\n        \"dimensionMismatch\": result.dimension_mismatch,\n    }))\n}\n\n// ---------------------------------------------------------------------------\n// Video and HAR handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_video_start(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let path = cmd\n        .get(\"path\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'path' parameter\")?;\n\n    if state.recording_state.active {\n        return Err(\"A recording is already in progress\".to_string());\n    }\n\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    recording::recording_start(&mut state.recording_state, path)?;\n    state\n        .start_recording_task(mgr.client.clone(), session_id)\n        .await?;\n\n    Ok(json!({\n        \"started\": true,\n        \"note\": \"Video recording started. Use video_stop to save the recording.\"\n    }))\n}\n\nasync fn handle_video_stop(state: &mut DaemonState) -> Result<Value, String> {\n    if !state.recording_state.active {\n        return Ok(json!({\n            \"stopped\": false,\n            \"note\": \"No video recording was started. Use recording_stop if you used recording_start.\"\n        }));\n    }\n\n    state.stop_recording_task().await?;\n    recording::recording_stop(&mut state.recording_state)\n}\n\n/// Begin capturing network traffic for a later HAR export.\nasync fn handle_har_start(state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    mgr.client\n        .send_command_no_params(\"Network.enable\", Some(&session_id))\n        .await?;\n    state.har_recording = true;\n    state.har_entries.clear();\n    Ok(json!({ \"started\": true }))\n}\n\n/// Stop HAR recording and write the captured requests to disk.\nasync fn handle_har_stop(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let path = har_output_path(cmd.get(\"path\").and_then(|v| v.as_str()));\n\n    state.har_recording = false;\n\n    let entries: Vec<Value> = state.har_entries.drain(..).map(har_entry_to_json).collect();\n    let request_count = entries.len();\n    let browser = har_browser_metadata(state).await;\n\n    let mut log = json!({\n        \"version\": \"1.2\",\n        \"creator\": {\n            \"name\": \"agent-browser\",\n            \"version\": env!(\"CARGO_PKG_VERSION\")\n        },\n        \"entries\": entries\n    });\n    if let Some(browser) = browser {\n        log[\"browser\"] = browser;\n    }\n    let har = json!({ \"log\": log });\n\n    let har_str = serde_json::to_string_pretty(&har)\n        .map_err(|e| format!(\"Failed to serialize HAR: {}\", e))?;\n    std::fs::write(&path, har_str).map_err(|e| format!(\"Failed to write HAR: {}\", e))?;\n\n    Ok(json!({ \"path\": path, \"requestCount\": request_count }))\n}\n\n// ---------------------------------------------------------------------------\n// HAR serialization helpers\n// ---------------------------------------------------------------------------\n\n/// Convert a `HarEntry` (collected from CDP events) into a HAR 1.2 entry object.\nfn har_entry_to_json(e: HarEntry) -> Value {\n    let started_date_time = har_wall_time_to_rfc3339(e.wall_time);\n\n    let request_cookies = e\n        .request_headers\n        .iter()\n        .find(|(k, _)| k.eq_ignore_ascii_case(\"cookie\"))\n        .map(|(_, v)| har_parse_request_cookies(v))\n        .unwrap_or_default();\n\n    let query_string = har_parse_query_string(&e.url);\n\n    let req_headers: Vec<Value> = e\n        .request_headers\n        .iter()\n        .map(|(k, v)| json!({ \"name\": k, \"value\": v }))\n        .collect();\n\n    let resp_cookies: Vec<Value> = e\n        .response_headers\n        .iter()\n        .filter(|(k, _)| k.eq_ignore_ascii_case(\"set-cookie\"))\n        .map(|(_, v)| {\n            // Split on ';' first to discard attributes (Path, HttpOnly, etc.),\n            // then split on '=' once to separate name from value.\n            let name_value = v.split(';').next().unwrap_or(\"\");\n            let (name, value) = name_value.split_once('=').unwrap_or((name_value, \"\"));\n            json!({ \"name\": name.trim(), \"value\": value.trim() })\n        })\n        .collect();\n\n    let resp_headers: Vec<Value> = e\n        .response_headers\n        .iter()\n        .map(|(k, v)| json!({ \"name\": k, \"value\": v }))\n        .collect();\n\n    let (timings, total_time) =\n        har_compute_timings(e.cdp_timing.as_ref(), e.loading_finished_timestamp);\n\n    let mime_type = if e.mime_type.is_empty() {\n        \"application/octet-stream\".to_string()\n    } else {\n        e.mime_type\n    };\n\n    let post_content_type = e\n        .request_headers\n        .iter()\n        .find(|(k, _)| k.eq_ignore_ascii_case(\"content-type\"))\n        .map(|(_, v)| v.as_str())\n        .unwrap_or(\"text/plain\")\n        .to_string();\n\n    let mut request = json!({\n        \"method\": e.method,\n        \"url\": e.url,\n        \"httpVersion\": e.http_version,\n        \"cookies\": request_cookies,\n        \"headers\": req_headers,\n        \"queryString\": query_string,\n        \"headersSize\": -1,\n        \"bodySize\": e.request_body_size,\n    });\n    if let Some(body) = e.post_data {\n        request[\"postData\"] = json!({ \"mimeType\": post_content_type, \"text\": body });\n    }\n\n    json!({\n        \"startedDateTime\": started_date_time,\n        \"time\": total_time,\n        \"request\": request,\n        \"response\": {\n            \"status\": e.status.unwrap_or(0),\n            \"statusText\": e.status_text,\n            \"httpVersion\": e.http_version,\n            \"cookies\": resp_cookies,\n            \"headers\": resp_headers,\n            \"content\": {\n                \"size\": e.response_body_size,\n                \"mimeType\": mime_type,\n            },\n            \"redirectURL\": e.redirect_url,\n            \"headersSize\": -1,\n            \"bodySize\": e.response_body_size,\n        },\n        \"cache\": {},\n        \"timings\": timings,\n        \"_resourceType\": e.resource_type,\n    })\n}\n\n/// Convert a CDP headers object (`{ \"Name\": \"value\", ... }`) into a flat\n/// `Vec<(name, value)>` preserving insertion order.\nfn har_extract_headers(headers_val: Option<&Value>) -> Vec<(String, String)> {\n    headers_val\n        .and_then(|v| v.as_object())\n        .map(|obj| {\n            obj.iter()\n                .map(|(k, v)| (k.clone(), v.as_str().unwrap_or(\"\").to_string()))\n                .collect()\n        })\n        .unwrap_or_default()\n}\n\n/// Map a CDP `response.protocol` value to an HTTP-version string as required\n/// by the HAR spec (e.g. `\"h2\"` → `\"HTTP/2.0\"`).\nfn har_cdp_protocol_to_http_version(protocol: &str) -> String {\n    match protocol.to_ascii_lowercase().as_str() {\n        \"h2\" => \"HTTP/2.0\".to_string(),\n        \"h3\" => \"HTTP/3.0\".to_string(),\n        \"http/1.0\" => \"HTTP/1.0\".to_string(),\n        _ => \"HTTP/1.1\".to_string(),\n    }\n}\n\n/// Parse query-string parameters from a URL into a HAR `queryString` array.\nfn har_parse_query_string(url_str: &str) -> Vec<Value> {\n    url::Url::parse(url_str)\n        .map(|u| {\n            u.query_pairs()\n                .map(|(k, v)| json!({ \"name\": k.as_ref(), \"value\": v.as_ref() }))\n                .collect()\n        })\n        .unwrap_or_default()\n}\n\n/// Parse a `Cookie: name1=val1; name2=val2` header value into HAR cookie objects.\nfn har_parse_request_cookies(cookie_header: &str) -> Vec<Value> {\n    cookie_header\n        .split(';')\n        .filter_map(|pair| {\n            let pair = pair.trim();\n            if pair.is_empty() {\n                return None;\n            }\n            let (name, value) = pair.split_once('=').unwrap_or((pair, \"\"));\n            Some(json!({ \"name\": name.trim(), \"value\": value.trim() }))\n        })\n        .collect()\n}\n\n/// Compute HAR `timings` and total `time` (ms) from a CDP `ResourceTiming`\n/// object and the optional `Network.loadingFinished` monotonic timestamp.\n///\n/// CDP timing values are milliseconds relative to `requestTime` (seconds since\n/// browser start). A value of `-1` means the phase did not occur.\nfn har_compute_timings(\n    cdp_timing: Option<&Value>,\n    loading_finished_ts: Option<f64>,\n) -> (Value, f64) {\n    let Some(t) = cdp_timing else {\n        return (json!({ \"send\": 0, \"wait\": 0, \"receive\": 0 }), 0.0);\n    };\n\n    let get = |key: &str| t.get(key).and_then(|v| v.as_f64()).unwrap_or(-1.0);\n\n    let request_time = get(\"requestTime\");\n    let dns_start = get(\"dnsStart\");\n    let dns_end = get(\"dnsEnd\");\n    let connect_start = get(\"connectStart\");\n    let connect_end = get(\"connectEnd\");\n    let ssl_start = get(\"sslStart\");\n    let ssl_end = get(\"sslEnd\");\n    let send_start = get(\"sendStart\");\n    let send_end = get(\"sendEnd\");\n    let recv_headers_start = get(\"receiveHeadersStart\");\n    let recv_headers_end = get(\"receiveHeadersEnd\");\n\n    let dns = if dns_start >= 0.0 && dns_end >= 0.0 {\n        dns_end - dns_start\n    } else {\n        -1.0\n    };\n    let connect = if connect_start >= 0.0 && connect_end >= 0.0 {\n        connect_end - connect_start\n    } else {\n        -1.0\n    };\n    let ssl = if ssl_start >= 0.0 && ssl_end >= 0.0 {\n        ssl_end - ssl_start\n    } else {\n        -1.0\n    };\n    let send = (send_end - send_start).max(0.0);\n\n    // wait: end of sending → first byte of response headers.\n    let wait_end = if recv_headers_start >= 0.0 {\n        recv_headers_start\n    } else {\n        recv_headers_end\n    };\n    let wait = if send_end >= 0.0 && wait_end >= send_end {\n        wait_end - send_end\n    } else {\n        0.0\n    };\n\n    // receive: first response byte → loading complete.\n    // requestTime (seconds) + recv_headers_end (ms) / 1000 = absolute headers-end timestamp.\n    let receive = loading_finished_ts\n        .filter(|_| request_time >= 0.0 && recv_headers_end >= 0.0)\n        .map(|lf_ts| {\n            let recv_start_abs = request_time + recv_headers_end / 1000.0;\n            ((lf_ts - recv_start_abs) * 1000.0).max(0.0)\n        })\n        .unwrap_or(0.0);\n\n    let blocked = if dns_start > 0.0 {\n        dns_start\n    } else if connect_start > 0.0 {\n        connect_start\n    } else if send_start > 0.0 {\n        send_start\n    } else {\n        -1.0\n    };\n\n    let total: f64 = [\n        if blocked > 0.0 { blocked } else { 0.0 },\n        if dns >= 0.0 { dns } else { 0.0 },\n        if connect >= 0.0 { connect } else { 0.0 },\n        send,\n        wait,\n        receive,\n    ]\n    .iter()\n    .sum();\n\n    let mut timings = json!({ \"send\": send, \"wait\": wait, \"receive\": receive });\n    if blocked > 0.0 {\n        timings[\"blocked\"] = json!(blocked);\n    }\n    if dns >= 0.0 {\n        timings[\"dns\"] = json!(dns);\n    }\n    if connect >= 0.0 {\n        timings[\"connect\"] = json!(connect);\n    }\n    if ssl >= 0.0 {\n        timings[\"ssl\"] = json!(ssl);\n    }\n\n    (timings, total)\n}\n\n/// Format a Unix epoch timestamp (seconds, fractional) as RFC 3339 using the\n/// `time` crate, e.g. `\"2024-03-17T10:30:00.456Z\"`.\nfn har_wall_time_to_rfc3339(wall_time: f64) -> String {\n    if wall_time > 0.0 {\n        let nanos = (wall_time * 1_000_000_000.0).round() as i128;\n        if let Ok(dt) = OffsetDateTime::from_unix_timestamp_nanos(nanos) {\n            if let Ok(s) = dt.format(&Rfc3339) {\n                return s;\n            }\n        }\n    }\n    OffsetDateTime::now_utc()\n        .format(&Rfc3339)\n        .unwrap_or_else(|_| \"1970-01-01T00:00:00Z\".to_string())\n}\n\nfn har_output_path(explicit_path: Option<&str>) -> String {\n    match explicit_path {\n        Some(path) => path.to_string(),\n        None => {\n            let dir = get_har_dir();\n            let _ = std::fs::create_dir_all(&dir);\n            dir.join(format!(\"har-{}.har\", unix_timestamp_millis()))\n                .to_string_lossy()\n                .to_string()\n        }\n    }\n}\n\nfn get_har_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\").join(\"tmp\").join(\"har\")\n    } else {\n        std::env::temp_dir().join(\"agent-browser\").join(\"har\")\n    }\n}\n\nfn unix_timestamp_millis() -> u128 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap_or_default()\n        .as_millis()\n}\n\nasync fn har_browser_metadata(state: &DaemonState) -> Option<Value> {\n    let mgr = state.browser.as_ref()?;\n    if !mgr.is_connection_alive().await {\n        return None;\n    }\n\n    let version = mgr\n        .client\n        .send_command_no_params(\"Browser.getVersion\", None)\n        .await\n        .ok()?;\n    browser_metadata_from_version(&version)\n}\n\nfn browser_metadata_from_version(version: &Value) -> Option<Value> {\n    let product = version.get(\"product\").and_then(|v| v.as_str())?;\n    let (name, browser_version) = product.split_once('/').unwrap_or((product, \"\"));\n    Some(json!({\n        \"name\": name,\n        \"version\": browser_version,\n    }))\n}\n\n// ---------------------------------------------------------------------------\n// Fetch interception resolver (domain filter + routes + origin headers)\n// ---------------------------------------------------------------------------\n\nasync fn resolve_fetch_paused(\n    client: &CdpClient,\n    domain_filter: Option<&DomainFilter>,\n    routes: &[RouteEntry],\n    origin_headers: &HashMap<String, HashMap<String, String>>,\n    paused: &FetchPausedRequest,\n) {\n    let session_id = &paused.session_id;\n\n    // Domain filter check (takes priority over routes and origin headers)\n    if let Some(filter) = domain_filter {\n        if let Ok(parsed) = url::Url::parse(&paused.url) {\n            let scheme = parsed.scheme();\n            if scheme != \"http\" && scheme != \"https\" {\n                if paused.resource_type.eq_ignore_ascii_case(\"document\") {\n                    let _ = client\n                        .send_command(\n                            \"Fetch.failRequest\",\n                            Some(json!({\n                                \"requestId\": paused.request_id,\n                                \"errorReason\": \"BlockedByClient\"\n                            })),\n                            Some(session_id),\n                        )\n                        .await;\n                } else {\n                    let _ = client\n                        .send_command(\n                            \"Fetch.continueRequest\",\n                            Some(json!({ \"requestId\": paused.request_id })),\n                            Some(session_id),\n                        )\n                        .await;\n                }\n                return;\n            }\n\n            if let Some(hostname) = parsed.host_str() {\n                if !filter.is_allowed(hostname) {\n                    if paused.resource_type.eq_ignore_ascii_case(\"document\") {\n                        let error_body = format!(\n                            \"<html><body><h1>Blocked</h1><p>Navigation to {} is not allowed by domain filter.</p></body></html>\",\n                            hostname\n                        );\n                        let encoded = base64::Engine::encode(\n                            &base64::engine::general_purpose::STANDARD,\n                            error_body.as_bytes(),\n                        );\n                        let _ = client\n                            .send_command(\n                                \"Fetch.fulfillRequest\",\n                                Some(json!({\n                                    \"requestId\": paused.request_id,\n                                    \"responseCode\": 403,\n                                    \"responseHeaders\": [\n                                        { \"name\": \"Content-Type\", \"value\": \"text/html\" },\n                                    ],\n                                    \"body\": encoded,\n                                })),\n                                Some(session_id),\n                            )\n                            .await;\n                    } else {\n                        let _ = client\n                            .send_command(\n                                \"Fetch.failRequest\",\n                                Some(json!({\n                                    \"requestId\": paused.request_id,\n                                    \"errorReason\": \"BlockedByClient\"\n                                })),\n                                Some(session_id),\n                            )\n                            .await;\n                    }\n                    return;\n                }\n            }\n        }\n    }\n\n    // Route matching\n    for route in routes {\n        let matches = if route.url_pattern == \"*\" {\n            true\n        } else if route.url_pattern.contains('*') {\n            let parts: Vec<&str> = route.url_pattern.split('*').collect();\n            if parts.len() == 2 {\n                paused.url.starts_with(parts[0]) && paused.url.ends_with(parts[1])\n            } else {\n                paused.url.contains(&route.url_pattern)\n            }\n        } else {\n            paused.url.contains(&route.url_pattern)\n        };\n\n        if matches {\n            if route.abort {\n                let _ = client\n                    .send_command(\n                        \"Fetch.failRequest\",\n                        Some(json!({\n                            \"requestId\": paused.request_id,\n                            \"errorReason\": \"Failed\"\n                        })),\n                        Some(session_id),\n                    )\n                    .await;\n                return;\n            }\n\n            if let Some(ref resp) = route.response {\n                let status = resp.status.unwrap_or(200);\n                let body_str = resp.body.as_deref().unwrap_or(\"\");\n                let encoded = base64::Engine::encode(\n                    &base64::engine::general_purpose::STANDARD,\n                    body_str.as_bytes(),\n                );\n                let mut headers = vec![];\n                if let Some(ct) = &resp.content_type {\n                    headers.push(json!({ \"name\": \"Content-Type\", \"value\": ct }));\n                }\n                if let Some(h) = &resp.headers {\n                    for (k, v) in h {\n                        headers.push(json!({ \"name\": k, \"value\": v }));\n                    }\n                }\n\n                let _ = client\n                    .send_command(\n                        \"Fetch.fulfillRequest\",\n                        Some(json!({\n                            \"requestId\": paused.request_id,\n                            \"responseCode\": status,\n                            \"responseHeaders\": headers,\n                            \"body\": encoded,\n                        })),\n                        Some(session_id),\n                    )\n                    .await;\n                return;\n            }\n        }\n    }\n\n    // No matching route — continue, injecting origin-scoped headers if applicable.\n    let extra = url::Url::parse(&paused.url)\n        .ok()\n        .map(|u| u.origin().ascii_serialization())\n        .and_then(|o| origin_headers.get(&o));\n\n    if let Some(extra_headers) = extra {\n        // Merge original request headers with extra headers.\n        // Fetch.continueRequest replaces (not merges), so include originals.\n        let mut combined: Vec<Value> = Vec::new();\n        if let Some(ref orig) = paused.request_headers {\n            for (k, v) in orig {\n                if !extra_headers.keys().any(|ek| ek.eq_ignore_ascii_case(k)) {\n                    if let Some(s) = v.as_str() {\n                        combined.push(json!({ \"name\": k, \"value\": s }));\n                    }\n                }\n            }\n        }\n        for (k, v) in extra_headers {\n            combined.push(json!({ \"name\": k, \"value\": v }));\n        }\n        let _ = client\n            .send_command(\n                \"Fetch.continueRequest\",\n                Some(json!({ \"requestId\": paused.request_id, \"headers\": combined })),\n                Some(session_id),\n            )\n            .await;\n    } else {\n        let _ = client\n            .send_command(\n                \"Fetch.continueRequest\",\n                Some(json!({ \"requestId\": paused.request_id })),\n                Some(session_id),\n            )\n            .await;\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Route handlers\n// ---------------------------------------------------------------------------\n\n/// Build the Fetch.enable patterns list from current routes, domain filter,\n/// and origin headers state.  When domain filtering or origin-scoped headers\n/// are active a wildcard pattern is included so all requests are intercepted.\nasync fn build_fetch_patterns(state: &DaemonState) -> Vec<Value> {\n    let routes = state.routes.read().await;\n    let mut patterns: Vec<Value> = routes\n        .iter()\n        .map(|r| json!({ \"urlPattern\": r.url_pattern }))\n        .collect();\n    let has_domain_filter = state.domain_filter.read().await.is_some();\n    let has_origin_headers = !state.origin_headers.read().await.is_empty();\n    if (has_domain_filter || has_origin_headers) && !patterns.iter().any(|p| p[\"urlPattern\"] == \"*\")\n    {\n        patterns.push(json!({ \"urlPattern\": \"*\" }));\n    }\n    patterns\n}\n\nasync fn handle_route(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let url_pattern = cmd\n        .get(\"url\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url' parameter\")?\n        .to_string();\n    let abort = cmd.get(\"abort\").and_then(|v| v.as_bool()).unwrap_or(false);\n\n    let response = cmd.get(\"response\").and_then(|v| {\n        if v.is_null() {\n            return None;\n        }\n        Some(RouteResponse {\n            status: v.get(\"status\").and_then(|s| s.as_u64()).map(|s| s as u16),\n            body: v.get(\"body\").and_then(|s| s.as_str()).map(String::from),\n            content_type: v\n                .get(\"contentType\")\n                .and_then(|s| s.as_str())\n                .map(String::from),\n            headers: v.get(\"headers\").and_then(|h| {\n                h.as_object().map(|m| {\n                    m.iter()\n                        .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))\n                        .collect()\n                })\n            }),\n        })\n    });\n\n    {\n        let mut routes = state.routes.write().await;\n        routes.push(RouteEntry {\n            url_pattern: url_pattern.clone(),\n            response,\n            abort,\n        });\n    }\n\n    let patterns = build_fetch_patterns(state).await;\n    mgr.client\n        .send_command(\n            \"Fetch.enable\",\n            Some(json!({ \"patterns\": patterns })),\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"routed\": url_pattern }))\n}\n\nasync fn handle_unroute(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let url = cmd.get(\"url\").and_then(|v| v.as_str());\n\n    {\n        let mut routes = state.routes.write().await;\n        match url {\n            Some(pattern) => {\n                routes.retain(|r| r.url_pattern != pattern);\n            }\n            None => {\n                routes.clear();\n            }\n        }\n    }\n\n    let patterns = build_fetch_patterns(state).await;\n    if patterns.is_empty() {\n        mgr.client\n            .send_command(\"Fetch.disable\", None, Some(&session_id))\n            .await?;\n    } else {\n        mgr.client\n            .send_command(\n                \"Fetch.enable\",\n                Some(json!({ \"patterns\": patterns })),\n                Some(&session_id),\n            )\n            .await?;\n    }\n\n    let label = url.unwrap_or(\"all\");\n    Ok(json!({ \"unrouted\": label }))\n}\n\nasync fn handle_requests(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    if cmd.get(\"clear\").and_then(|v| v.as_bool()).unwrap_or(false) {\n        state.tracked_requests.clear();\n        return Ok(json!({ \"cleared\": true }));\n    }\n\n    if !state.request_tracking {\n        state.request_tracking = true;\n        if let Some(ref mgr) = state.browser {\n            if let Ok(session_id) = mgr.active_session_id() {\n                let _ = mgr\n                    .client\n                    .send_command_no_params(\"Network.enable\", Some(session_id))\n                    .await;\n            }\n        }\n    }\n\n    let filter = cmd.get(\"filter\").and_then(|v| v.as_str());\n    let requests: Vec<&TrackedRequest> = if let Some(f) = filter {\n        state\n            .tracked_requests\n            .iter()\n            .filter(|r| r.url.contains(f))\n            .collect()\n    } else {\n        state.tracked_requests.iter().collect()\n    };\n\n    Ok(json!({ \"requests\": requests }))\n}\n\nasync fn handle_http_credentials(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let username = cmd\n        .get(\"username\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'username' parameter\")?;\n    let password = cmd\n        .get(\"password\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'password' parameter\")?;\n\n    let encoded = base64::Engine::encode(\n        &base64::engine::general_purpose::STANDARD,\n        format!(\"{}:{}\", username, password),\n    );\n\n    let mut headers = HashMap::new();\n    headers.insert(\"Authorization\".to_string(), format!(\"Basic {}\", encoded));\n    network::set_extra_headers(&mgr.client, &session_id, &headers).await?;\n\n    Ok(json!({ \"set\": true }))\n}\n\n// ---------------------------------------------------------------------------\n// Auth handlers\n// ---------------------------------------------------------------------------\n\nasync fn handle_auth_save(cmd: &Value) -> Result<Value, String> {\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name'\")?;\n    let url = cmd\n        .get(\"url\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'url'\")?;\n    let username = cmd\n        .get(\"username\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'username'\")?;\n    let password = cmd\n        .get(\"password\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'password'\")?;\n    let username_selector = cmd.get(\"usernameSelector\").and_then(|v| v.as_str());\n    let password_selector = cmd.get(\"passwordSelector\").and_then(|v| v.as_str());\n    let submit_selector = cmd.get(\"submitSelector\").and_then(|v| v.as_str());\n    auth::auth_save(\n        name,\n        url,\n        username,\n        password,\n        username_selector,\n        password_selector,\n        submit_selector,\n    )\n}\n\nasync fn handle_auth_login(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let name = cmd\n        .get(\"name\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'name'\")?;\n    let cred = auth::credentials_get_full(name)?;\n    if cred.url.is_empty() {\n        return Err(\"Credential has no URL\".to_string());\n    }\n    let url = cred.url;\n    let username = cred.username;\n    let password = cred.password;\n\n    let mgr = state.browser.as_mut().ok_or(\"Browser not launched\")?;\n    mgr.navigate(&url, WaitUntil::Load).await?;\n\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let auto_user_selectors = [\n        \"input[type=email]\",\n        \"input[name=email]\",\n        \"input[type=text][name*=user]\",\n        \"input[id*=user]\",\n        \"input[type=text]\",\n    ];\n    let auto_submit_selectors = [\n        \"button[type=submit]\",\n        \"input[type=submit]\",\n        \"button:not([type])\",\n    ];\n\n    let username_sel = cmd\n        .get(\"usernameSelector\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .or(cred.username_selector);\n    let password_sel = cmd\n        .get(\"passwordSelector\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .or(cred.password_selector);\n    let submit_sel = cmd\n        .get(\"submitSelector\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .or(cred.submit_selector);\n\n    // Find and fill username\n    let user_sel = if let Some(s) = username_sel {\n        s\n    } else {\n        let mut found = None;\n        for sel in &auto_user_selectors {\n            let js = format!(\n                \"!!document.querySelector({})\",\n                serde_json::to_string(sel).unwrap_or_default()\n            );\n            if let Ok(val) = mgr.evaluate(&js, None).await {\n                if val.as_bool().unwrap_or(false) {\n                    found = Some(sel.to_string());\n                    break;\n                }\n            }\n        }\n        found.ok_or(\"Could not find username field\")?\n    };\n    interaction::fill(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        &user_sel,\n        &username,\n    )\n    .await?;\n\n    // Find and fill password\n    let pass_sel = password_sel.unwrap_or_else(|| \"input[type=password]\".to_string());\n    interaction::fill(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        &pass_sel,\n        &password,\n    )\n    .await?;\n\n    // Find and click submit\n    let sub_sel = if let Some(s) = submit_sel {\n        s\n    } else {\n        let mut found = None;\n        for sel in &auto_submit_selectors {\n            let js = format!(\n                \"!!document.querySelector({})\",\n                serde_json::to_string(sel).unwrap_or_default()\n            );\n            if let Ok(val) = mgr.evaluate(&js, None).await {\n                if val.as_bool().unwrap_or(false) {\n                    found = Some(sel.to_string());\n                    break;\n                }\n            }\n        }\n        found.ok_or(\"Could not find submit button\")?\n    };\n    interaction::click(\n        &mgr.client,\n        &session_id,\n        &state.ref_map,\n        &sub_sel,\n        \"left\",\n        1,\n    )\n    .await?;\n\n    // Wait for navigation after submit (with fallback timeout)\n    let mut rx = mgr.client.subscribe();\n    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10);\n    let mut navigated = false;\n\n    loop {\n        let result = tokio::time::timeout_at(deadline, rx.recv()).await;\n        match result {\n            Ok(Ok(event)) => {\n                if event.session_id.as_deref() == Some(&session_id) {\n                    match event.method.as_str() {\n                        \"Page.frameNavigated\" | \"Page.loadEventFired\" => {\n                            navigated = true;\n                            break;\n                        }\n                        _ => {}\n                    }\n                }\n            }\n            Ok(Err(_)) => break,\n            Err(_) => break,\n        }\n    }\n\n    if !navigated {\n        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;\n    }\n\n    Ok(json!({ \"loggedIn\": true, \"name\": name }))\n}\n\n// ---------------------------------------------------------------------------\n// Confirmation handlers (stub)\n// ---------------------------------------------------------------------------\n\nasync fn handle_confirm(_cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let pending = state\n        .pending_confirmation\n        .take()\n        .ok_or(\"No pending confirmation\")?;\n\n    // Temporarily remove policy and confirm_actions to avoid re-triggering confirmation\n    let policy = state.policy.take();\n    let confirm_actions = state.confirm_actions.take();\n    let result = Box::pin(execute_command(&pending.cmd, state)).await;\n    state.policy = policy;\n    state.confirm_actions = confirm_actions;\n\n    Ok(json!({ \"confirmed\": true, \"action\": pending.action, \"result\": result }))\n}\n\nasync fn handle_deny(_cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let pending = state\n        .pending_confirmation\n        .take()\n        .ok_or(\"No pending confirmation\")?;\n\n    Ok(json!({ \"denied\": true, \"action\": pending.action }))\n}\n\n// ---------------------------------------------------------------------------\n// iOS handlers (stub)\n// ---------------------------------------------------------------------------\n\nasync fn handle_swipe(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    // Route through Appium for iOS/WebDriver\n    if let Some(ref appium) = state.appium {\n        if state.browser.is_none() {\n            let start_x = cmd.get(\"startX\").and_then(|v| v.as_f64()).unwrap_or(200.0);\n            let start_y = cmd.get(\"startY\").and_then(|v| v.as_f64()).unwrap_or(400.0);\n            let end_x = cmd.get(\"endX\").and_then(|v| v.as_f64()).unwrap_or(200.0);\n            let end_y = cmd.get(\"endY\").and_then(|v| v.as_f64()).unwrap_or(100.0);\n\n            if let Some(direction) = cmd.get(\"direction\").and_then(|v| v.as_str()) {\n                let distance = cmd\n                    .get(\"distance\")\n                    .and_then(|v| v.as_f64())\n                    .unwrap_or(300.0);\n                let (dx, dy) = match direction {\n                    \"up\" => (0.0, -distance),\n                    \"down\" => (0.0, distance),\n                    \"left\" => (-distance, 0.0),\n                    \"right\" => (distance, 0.0),\n                    _ => (0.0, -distance),\n                };\n                let actual_end_x = start_x + dx;\n                let actual_end_y = start_y + dy;\n                let duration = cmd.get(\"duration\").and_then(|v| v.as_u64()).unwrap_or(800);\n                appium\n                    .swipe(start_x, start_y, actual_end_x, actual_end_y, duration)\n                    .await?;\n                return Ok(json!({ \"swiped\": direction }));\n            }\n\n            let duration = cmd.get(\"duration\").and_then(|v| v.as_u64()).unwrap_or(800);\n            appium\n                .swipe(start_x, start_y, end_x, end_y, duration)\n                .await?;\n            return Ok(json!({ \"swiped\": true, \"from\": [start_x, start_y], \"to\": [end_x, end_y] }));\n        }\n    }\n\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n\n    let start_x = cmd.get(\"startX\").and_then(|v| v.as_f64()).unwrap_or(200.0);\n    let start_y = cmd.get(\"startY\").and_then(|v| v.as_f64()).unwrap_or(400.0);\n    let end_x = cmd.get(\"endX\").and_then(|v| v.as_f64()).unwrap_or(200.0);\n    let end_y = cmd.get(\"endY\").and_then(|v| v.as_f64()).unwrap_or(100.0);\n\n    if let Some(direction) = cmd.get(\"direction\").and_then(|v| v.as_str()) {\n        let distance = cmd\n            .get(\"distance\")\n            .and_then(|v| v.as_f64())\n            .unwrap_or(300.0);\n        let (dx, dy) = match direction {\n            \"up\" => (0.0, -distance),\n            \"down\" => (0.0, distance),\n            \"left\" => (-distance, 0.0),\n            \"right\" => (distance, 0.0),\n            _ => (0.0, -distance),\n        };\n        let cx = start_x;\n        let cy = start_y;\n\n        mgr.client\n            .send_command(\n                \"Input.dispatchTouchEvent\",\n                Some(json!({ \"type\": \"touchStart\", \"touchPoints\": [{ \"x\": cx, \"y\": cy }] })),\n                Some(&session_id),\n            )\n            .await?;\n\n        let steps = 10;\n        for i in 1..=steps {\n            let x = cx + dx * (i as f64) / (steps as f64);\n            let y = cy + dy * (i as f64) / (steps as f64);\n            mgr.client\n                .send_command(\n                    \"Input.dispatchTouchEvent\",\n                    Some(json!({ \"type\": \"touchMove\", \"touchPoints\": [{ \"x\": x, \"y\": y }] })),\n                    Some(&session_id),\n                )\n                .await?;\n            tokio::time::sleep(tokio::time::Duration::from_millis(16)).await;\n        }\n\n        mgr.client\n            .send_command(\n                \"Input.dispatchTouchEvent\",\n                Some(json!({ \"type\": \"touchEnd\", \"touchPoints\": [] })),\n                Some(&session_id),\n            )\n            .await?;\n\n        return Ok(json!({ \"swiped\": direction }));\n    }\n\n    // Manual coordinates\n    mgr.client\n        .send_command(\n            \"Input.dispatchTouchEvent\",\n            Some(json!({ \"type\": \"touchStart\", \"touchPoints\": [{ \"x\": start_x, \"y\": start_y }] })),\n            Some(&session_id),\n        )\n        .await?;\n\n    let steps = 10;\n    for i in 1..=steps {\n        let x = start_x + (end_x - start_x) * (i as f64) / (steps as f64);\n        let y = start_y + (end_y - start_y) * (i as f64) / (steps as f64);\n        mgr.client\n            .send_command(\n                \"Input.dispatchTouchEvent\",\n                Some(json!({ \"type\": \"touchMove\", \"touchPoints\": [{ \"x\": x, \"y\": y }] })),\n                Some(&session_id),\n            )\n            .await?;\n        tokio::time::sleep(tokio::time::Duration::from_millis(16)).await;\n    }\n\n    mgr.client\n        .send_command(\n            \"Input.dispatchTouchEvent\",\n            Some(json!({ \"type\": \"touchEnd\", \"touchPoints\": [] })),\n            Some(&session_id),\n        )\n        .await?;\n\n    Ok(json!({ \"swiped\": true, \"from\": [start_x, start_y], \"to\": [end_x, end_y] }))\n}\n\nasync fn handle_device_list() -> Result<Value, String> {\n    #[cfg(target_os = \"macos\")]\n    {\n        use super::webdriver::ios;\n        let devices = ios::list_all_devices()?;\n        Ok(ios::to_device_json(&devices))\n    }\n\n    #[cfg(not(target_os = \"macos\"))]\n    {\n        Err(\"device_list is only available on macOS with Xcode\".to_string())\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Input event handlers\n// ---------------------------------------------------------------------------\n\nfn mouse_button_mask(button: &str) -> i32 {\n    match button {\n        \"left\" => 1,\n        \"right\" => 2,\n        \"middle\" => 4,\n        \"back\" => 8,\n        \"forward\" => 16,\n        _ => 0,\n    }\n}\n\nfn primary_button_from_mask(buttons: i32) -> &'static str {\n    if buttons & 1 != 0 {\n        \"left\"\n    } else if buttons & 2 != 0 {\n        \"right\"\n    } else if buttons & 4 != 0 {\n        \"middle\"\n    } else if buttons & 8 != 0 {\n        \"back\"\n    } else if buttons & 16 != 0 {\n        \"forward\"\n    } else {\n        \"none\"\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn build_mouse_event_params(\n    mouse_state: &mut MouseState,\n    event_type: &str,\n    x: Option<f64>,\n    y: Option<f64>,\n    button: Option<&str>,\n    buttons: Option<i32>,\n    click_count: Option<i32>,\n    delta_x: Option<f64>,\n    delta_y: Option<f64>,\n    modifiers: Option<i32>,\n) -> DispatchMouseEventParams {\n    let x = x.unwrap_or(mouse_state.x);\n    let y = y.unwrap_or(mouse_state.y);\n    mouse_state.x = x;\n    mouse_state.y = y;\n\n    let mut next_buttons = buttons.unwrap_or(mouse_state.buttons);\n    if buttons.is_none() {\n        match event_type {\n            \"mousePressed\" => {\n                next_buttons |= mouse_button_mask(button.unwrap_or(\"left\"));\n            }\n            \"mouseReleased\" => {\n                next_buttons &= !mouse_button_mask(button.unwrap_or(\"left\"));\n            }\n            _ => {}\n        }\n    }\n    mouse_state.buttons = next_buttons;\n\n    DispatchMouseEventParams {\n        event_type: event_type.to_string(),\n        x,\n        y,\n        button: Some(\n            button\n                .unwrap_or(primary_button_from_mask(next_buttons))\n                .to_string(),\n        ),\n        buttons: Some(next_buttons),\n        click_count,\n        delta_x,\n        delta_y,\n        modifiers,\n    }\n}\n\nasync fn handle_input_mouse(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let event_type = cmd\n        .get(\"type\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"mouseMoved\");\n    let params = build_mouse_event_params(\n        &mut state.mouse_state,\n        event_type,\n        cmd.get(\"x\").and_then(|v| v.as_f64()),\n        cmd.get(\"y\").and_then(|v| v.as_f64()),\n        cmd.get(\"button\").and_then(|v| v.as_str()),\n        cmd.get(\"buttons\")\n            .and_then(|v| v.as_i64())\n            .map(|v| v as i32),\n        cmd.get(\"clickCount\")\n            .and_then(|v| v.as_i64())\n            .map(|v| v as i32),\n        cmd.get(\"deltaX\").and_then(|v| v.as_f64()),\n        cmd.get(\"deltaY\").and_then(|v| v.as_f64()),\n        cmd.get(\"modifiers\")\n            .and_then(|v| v.as_i64())\n            .map(|v| v as i32),\n    );\n\n    mgr.client\n        .send_command_typed::<_, Value>(\"Input.dispatchMouseEvent\", &params, Some(&session_id))\n        .await?;\n    Ok(json!({ \"dispatched\": event_type }))\n}\n\nasync fn handle_input_keyboard(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let event_type = cmd\n        .get(\"type\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"keyDown\");\n\n    let mut params = json!({ \"type\": event_type });\n    for key in &[\"key\", \"code\", \"text\"] {\n        if let Some(v) = cmd.get(*key) {\n            params[*key] = v.clone();\n        }\n    }\n\n    mgr.client\n        .send_command(\"Input.dispatchKeyEvent\", Some(params), Some(&session_id))\n        .await?;\n    Ok(json!({ \"dispatched\": event_type }))\n}\n\nasync fn handle_input_touch(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let event_type = cmd\n        .get(\"type\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"touchStart\");\n\n    mgr.client\n        .send_command(\n            \"Input.dispatchTouchEvent\",\n            Some(json!({\n                \"type\": event_type,\n                \"touchPoints\": cmd.get(\"touchPoints\").unwrap_or(&json!([])),\n            })),\n            Some(&session_id),\n        )\n        .await?;\n    Ok(json!({ \"dispatched\": event_type }))\n}\n\nasync fn handle_keydown(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let key = cmd\n        .get(\"key\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'key' parameter\")?;\n\n    mgr.client\n        .send_command(\n            \"Input.dispatchKeyEvent\",\n            Some(json!({ \"type\": \"keyDown\", \"key\": key })),\n            Some(&session_id),\n        )\n        .await?;\n    Ok(json!({ \"keydown\": key }))\n}\n\nasync fn handle_keyup(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let key = cmd\n        .get(\"key\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'key' parameter\")?;\n\n    mgr.client\n        .send_command(\n            \"Input.dispatchKeyEvent\",\n            Some(json!({ \"type\": \"keyUp\", \"key\": key })),\n            Some(&session_id),\n        )\n        .await?;\n    Ok(json!({ \"keyup\": key }))\n}\n\nasync fn handle_inserttext(cmd: &Value, state: &DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let text = cmd\n        .get(\"text\")\n        .and_then(|v| v.as_str())\n        .ok_or(\"Missing 'text' parameter\")?;\n\n    mgr.client\n        .send_command(\n            \"Input.insertText\",\n            Some(json!({ \"text\": text })),\n            Some(&session_id),\n        )\n        .await?;\n    Ok(json!({ \"inserted\": true }))\n}\n\nasync fn handle_mousemove(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let x = cmd.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let y = cmd.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let params = build_mouse_event_params(\n        &mut state.mouse_state,\n        \"mouseMoved\",\n        Some(x),\n        Some(y),\n        None,\n        None,\n        None,\n        None,\n        None,\n        None,\n    );\n\n    mgr.client\n        .send_command_typed::<_, Value>(\"Input.dispatchMouseEvent\", &params, Some(&session_id))\n        .await?;\n    Ok(json!({ \"moved\": true }))\n}\n\nasync fn handle_mousedown(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let button = cmd.get(\"button\").and_then(|v| v.as_str()).unwrap_or(\"left\");\n    let params = build_mouse_event_params(\n        &mut state.mouse_state,\n        \"mousePressed\",\n        None,\n        None,\n        Some(button),\n        None,\n        Some(1),\n        None,\n        None,\n        None,\n    );\n\n    mgr.client\n        .send_command_typed::<_, Value>(\"Input.dispatchMouseEvent\", &params, Some(&session_id))\n        .await?;\n    Ok(json!({ \"pressed\": true }))\n}\n\nasync fn handle_mouseup(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {\n    let mgr = state.browser.as_ref().ok_or(\"Browser not launched\")?;\n    let session_id = mgr.active_session_id()?.to_string();\n    let button = cmd.get(\"button\").and_then(|v| v.as_str()).unwrap_or(\"left\");\n    let params = build_mouse_event_params(\n        &mut state.mouse_state,\n        \"mouseReleased\",\n        None,\n        None,\n        Some(button),\n        None,\n        Some(1),\n        None,\n        None,\n        None,\n    );\n\n    mgr.client\n        .send_command_typed::<_, Value>(\"Input.dispatchMouseEvent\", &params, Some(&session_id))\n        .await?;\n    Ok(json!({ \"released\": true }))\n}\n\n// ---------------------------------------------------------------------------\n// Response helpers\n// ---------------------------------------------------------------------------\n\nfn success_response(id: &str, data: Value) -> Value {\n    json!({\n        \"id\": id,\n        \"success\": true,\n        \"data\": data,\n    })\n}\n\nfn error_response(id: &str, error: &str) -> Value {\n    json!({\n        \"id\": id,\n        \"success\": false,\n        \"error\": error,\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_utils::EnvGuard;\n    use std::fs;\n\n    #[test]\n    fn test_success_response_structure() {\n        let resp = success_response(\"cmd-1\", json!({\"url\": \"https://example.com\"}));\n        assert_eq!(resp[\"id\"], \"cmd-1\");\n        assert_eq!(resp[\"success\"], true);\n        assert!(resp[\"data\"].is_object());\n        assert_eq!(resp[\"data\"][\"url\"], \"https://example.com\");\n    }\n\n    #[test]\n    fn test_error_response_structure() {\n        let resp = error_response(\"cmd-2\", \"Something went wrong\");\n        assert_eq!(resp[\"id\"], \"cmd-2\");\n        assert_eq!(resp[\"success\"], false);\n        assert_eq!(resp[\"error\"], \"Something went wrong\");\n    }\n\n    #[tokio::test]\n    async fn test_daemon_state_new() {\n        let state = DaemonState::new();\n        assert!(state.browser.is_none());\n        assert!(state.domain_filter.read().await.is_none());\n        assert_eq!(state.session_id, \"default\");\n        assert!(!state.tracing_state.active);\n        assert!(!state.recording_state.active);\n        assert_eq!(state.mouse_state.x, 0.0);\n        assert_eq!(state.mouse_state.y, 0.0);\n        assert_eq!(state.mouse_state.buttons, 0);\n    }\n\n    #[test]\n    fn test_mouse_event_params_preserve_position_and_buttons() {\n        let mut mouse_state = MouseState::default();\n\n        let move_params = build_mouse_event_params(\n            &mut mouse_state,\n            \"mouseMoved\",\n            Some(120.0),\n            Some(240.0),\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n        );\n        assert_eq!(move_params.x, 120.0);\n        assert_eq!(move_params.y, 240.0);\n        assert_eq!(move_params.buttons, Some(0));\n\n        let down_params = build_mouse_event_params(\n            &mut mouse_state,\n            \"mousePressed\",\n            None,\n            None,\n            Some(\"left\"),\n            None,\n            Some(1),\n            None,\n            None,\n            None,\n        );\n        assert_eq!(down_params.x, 120.0);\n        assert_eq!(down_params.y, 240.0);\n        assert_eq!(down_params.button.as_deref(), Some(\"left\"));\n        assert_eq!(down_params.buttons, Some(1));\n        assert_eq!(mouse_state.buttons, 1);\n\n        let drag_move_params = build_mouse_event_params(\n            &mut mouse_state,\n            \"mouseMoved\",\n            Some(150.0),\n            Some(260.0),\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n        );\n        assert_eq!(drag_move_params.buttons, Some(1));\n        assert_eq!(drag_move_params.button.as_deref(), Some(\"left\"));\n        assert_eq!(mouse_state.x, 150.0);\n        assert_eq!(mouse_state.y, 260.0);\n\n        let up_params = build_mouse_event_params(\n            &mut mouse_state,\n            \"mouseReleased\",\n            None,\n            None,\n            Some(\"left\"),\n            None,\n            Some(1),\n            None,\n            None,\n            None,\n        );\n        assert_eq!(up_params.x, 150.0);\n        assert_eq!(up_params.y, 260.0);\n        assert_eq!(up_params.buttons, Some(0));\n        assert_eq!(mouse_state.buttons, 0);\n    }\n\n    #[test]\n    fn test_reset_input_state_clears_mouse_state() {\n        let mut state = DaemonState::new();\n        state.mouse_state.x = 12.0;\n        state.mouse_state.y = 34.0;\n        state.mouse_state.buttons = 1;\n\n        state.reset_input_state();\n\n        assert_eq!(state.mouse_state.x, 0.0);\n        assert_eq!(state.mouse_state.y, 0.0);\n        assert_eq!(state.mouse_state.buttons, 0);\n    }\n\n    #[test]\n    fn test_launch_options_from_env_defaults() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_HEADED\"]);\n        let opts = launch_options_from_env();\n        assert!(opts.headless);\n        assert!(opts.args.is_empty());\n        assert!(!opts.allow_file_access);\n    }\n\n    #[test]\n    fn test_launch_options_from_env_headed_flag() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_HEADED\"]);\n        _guard.set(\"AGENT_BROWSER_HEADED\", \"1\");\n        let opts = launch_options_from_env();\n        assert!(\n            !opts.headless,\n            \"AGENT_BROWSER_HEADED=1 should set headless=false\"\n        );\n    }\n\n    #[test]\n    fn test_har_entry_to_json_enriches_request_and_response() {\n        // wall_time: 2026-03-15T12:00:00Z = 1_773_576_000\n        let entry = HarEntry {\n            request_id: \"req-1\".to_string(),\n            wall_time: 1773576000.0,\n            method: \"POST\".to_string(),\n            url: \"https://example.com/api?foo=bar&baz=qux\".to_string(),\n            request_headers: vec![\n                (\"Accept\".to_string(), \"application/json\".to_string()),\n                (\"Content-Type\".to_string(), \"application/json\".to_string()),\n                (\"Cookie\".to_string(), \"session=abc; theme=dark\".to_string()),\n            ],\n            post_data: Some(r#\"{\"x\":1}\"#.to_string()),\n            request_body_size: 7,\n            resource_type: \"XHR\".to_string(),\n            status: Some(201),\n            status_text: \"Created\".to_string(),\n            http_version: \"HTTP/2.0\".to_string(),\n            response_headers: vec![\n                (\"content-type\".to_string(), \"application/json\".to_string()),\n                (\n                    \"location\".to_string(),\n                    \"https://example.com/api/1\".to_string(),\n                ),\n                (\n                    \"set-cookie\".to_string(),\n                    \"token=xyz; Path=/; HttpOnly\".to_string(),\n                ),\n            ],\n            mime_type: \"application/json\".to_string(),\n            redirect_url: \"https://example.com/api/1\".to_string(),\n            response_body_size: 42,\n            cdp_timing: None,\n            loading_finished_timestamp: None,\n        };\n\n        let har = har_entry_to_json(entry);\n        assert_eq!(har[\"startedDateTime\"], \"2026-03-15T12:00:00Z\");\n        assert_eq!(har[\"request\"][\"method\"], \"POST\");\n        assert_eq!(har[\"request\"][\"httpVersion\"], \"HTTP/2.0\");\n        assert_eq!(har[\"request\"][\"queryString\"][0][\"name\"], \"foo\");\n        assert_eq!(har[\"request\"][\"queryString\"][0][\"value\"], \"bar\");\n        assert_eq!(har[\"request\"][\"bodySize\"], 7);\n        assert_eq!(har[\"request\"][\"postData\"][\"mimeType\"], \"application/json\");\n        assert_eq!(har[\"request\"][\"postData\"][\"text\"], r#\"{\"x\":1}\"#);\n        assert_eq!(har[\"request\"][\"cookies\"][0][\"name\"], \"session\");\n        assert_eq!(har[\"request\"][\"cookies\"][0][\"value\"], \"abc\");\n        assert_eq!(har[\"request\"][\"cookies\"][1][\"name\"], \"theme\");\n        assert_eq!(har[\"request\"][\"cookies\"][1][\"value\"], \"dark\");\n        assert_eq!(har[\"response\"][\"status\"], 201);\n        assert_eq!(har[\"response\"][\"statusText\"], \"Created\");\n        assert_eq!(har[\"response\"][\"content\"][\"mimeType\"], \"application/json\");\n        assert_eq!(har[\"response\"][\"content\"][\"size\"], 42);\n        assert_eq!(har[\"response\"][\"redirectURL\"], \"https://example.com/api/1\");\n        assert_eq!(har[\"response\"][\"cookies\"][0][\"name\"], \"token\");\n        assert_eq!(har[\"response\"][\"cookies\"][0][\"value\"], \"xyz\");\n        assert_eq!(har[\"_resourceType\"], \"XHR\");\n    }\n\n    #[test]\n    fn test_har_wall_time_to_rfc3339_epoch() {\n        // Known timestamp: 2026-03-15T12:00:00Z = 1_773_576_000\n        let result = har_wall_time_to_rfc3339(1773576000.0);\n        assert!(result.starts_with(\"2026-03-15T12:00:00\"));\n    }\n\n    #[test]\n    fn test_har_wall_time_to_rfc3339_fractional_seconds() {\n        let result = har_wall_time_to_rfc3339(1773576000.456);\n        assert!(result.contains(\".456\") || result.contains(\"456\"));\n    }\n\n    #[test]\n    fn test_har_cdp_protocol_to_http_version() {\n        assert_eq!(har_cdp_protocol_to_http_version(\"h2\"), \"HTTP/2.0\");\n        assert_eq!(har_cdp_protocol_to_http_version(\"h3\"), \"HTTP/3.0\");\n        assert_eq!(har_cdp_protocol_to_http_version(\"http/1.0\"), \"HTTP/1.0\");\n        assert_eq!(har_cdp_protocol_to_http_version(\"http/1.1\"), \"HTTP/1.1\");\n        assert_eq!(har_cdp_protocol_to_http_version(\"unknown\"), \"HTTP/1.1\");\n    }\n\n    #[test]\n    fn test_har_parse_request_cookies() {\n        let cookies = har_parse_request_cookies(\"session=abc; theme=dark; empty=\");\n        assert_eq!(cookies.len(), 3);\n        assert_eq!(cookies[0][\"name\"], \"session\");\n        assert_eq!(cookies[0][\"value\"], \"abc\");\n        assert_eq!(cookies[1][\"name\"], \"theme\");\n        assert_eq!(cookies[1][\"value\"], \"dark\");\n        assert_eq!(cookies[2][\"name\"], \"empty\");\n        assert_eq!(cookies[2][\"value\"], \"\");\n    }\n\n    #[test]\n    fn test_har_set_cookie_strips_attributes_before_equal_split() {\n        let entry = HarEntry {\n            request_id: \"r\".to_string(),\n            wall_time: 1773576000.0,\n            method: \"GET\".to_string(),\n            url: \"https://example.com/\".to_string(),\n            request_headers: vec![],\n            post_data: None,\n            request_body_size: 0,\n            resource_type: \"Document\".to_string(),\n            status: Some(200),\n            status_text: \"OK\".to_string(),\n            http_version: \"HTTP/1.1\".to_string(),\n            response_headers: vec![(\n                \"set-cookie\".to_string(),\n                \"token=abc; Path=/; HttpOnly\".to_string(),\n            )],\n            mime_type: \"text/html\".to_string(),\n            redirect_url: String::new(),\n            response_body_size: 0,\n            cdp_timing: None,\n            loading_finished_timestamp: None,\n        };\n        let har = har_entry_to_json(entry);\n        assert_eq!(har[\"response\"][\"cookies\"][0][\"name\"], \"token\");\n        assert_eq!(har[\"response\"][\"cookies\"][0][\"value\"], \"abc\");\n    }\n\n    #[test]\n    fn test_har_compute_timings_no_cdp_timing() {\n        let (timings, total) = har_compute_timings(None, None);\n        assert_eq!(timings[\"send\"], 0);\n        assert_eq!(timings[\"wait\"], 0);\n        assert_eq!(timings[\"receive\"], 0);\n        assert_eq!(total, 0.0);\n    }\n\n    #[test]\n    fn test_har_compute_timings_with_cdp_timing() {\n        let cdp = json!({\n            \"requestTime\": 1000.0,\n            \"dnsStart\": 0.0, \"dnsEnd\": 5.0,\n            \"connectStart\": 5.0, \"connectEnd\": 15.0,\n            \"sslStart\": 8.0, \"sslEnd\": 15.0,\n            \"sendStart\": 15.0, \"sendEnd\": 16.0,\n            \"receiveHeadersStart\": 16.0, \"receiveHeadersEnd\": 50.0,\n        });\n        let (timings, total) = har_compute_timings(Some(&cdp), Some(1000.1));\n        assert_eq!(timings[\"dns\"], 5.0);\n        assert_eq!(timings[\"connect\"], 10.0);\n        assert_eq!(timings[\"ssl\"], 7.0);\n        assert_eq!(timings[\"send\"], 1.0);\n        assert!(total > 0.0);\n    }\n\n    #[tokio::test]\n    async fn test_handle_har_stop_without_path_uses_default_location() {\n        let mut state = DaemonState::new();\n        state.har_recording = true;\n        state.har_entries.push(HarEntry {\n            request_id: \"req-2\".to_string(),\n            wall_time: 1773576000.0,\n            method: \"GET\".to_string(),\n            url: \"https://example.com/\".to_string(),\n            request_headers: vec![(\"Accept\".to_string(), \"text/html\".to_string())],\n            post_data: None,\n            request_body_size: 0,\n            resource_type: \"Document\".to_string(),\n            status: Some(200),\n            status_text: \"OK\".to_string(),\n            http_version: \"HTTP/2.0\".to_string(),\n            response_headers: vec![(\"content-type\".to_string(), \"text/html\".to_string())],\n            mime_type: \"text/html\".to_string(),\n            redirect_url: String::new(),\n            response_body_size: 128,\n            cdp_timing: None,\n            loading_finished_timestamp: None,\n        });\n\n        let result = handle_har_stop(&json!({ \"action\": \"har_stop\" }), &mut state)\n            .await\n            .unwrap();\n\n        let path = result[\"path\"].as_str().unwrap();\n        assert!(path.ends_with(\".har\"));\n        assert!(std::path::Path::new(path).starts_with(get_har_dir()));\n        assert_eq!(result[\"requestCount\"], 1);\n        assert!(!state.har_recording);\n        assert!(state.har_entries.is_empty());\n\n        let har: Value = serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap();\n        assert_eq!(har[\"log\"][\"version\"], \"1.2\");\n        assert_eq!(har[\"log\"][\"creator\"][\"name\"], \"agent-browser\");\n        assert!(har[\"log\"].get(\"browser\").is_none());\n        assert_eq!(har[\"log\"][\"entries\"][0][\"response\"][\"content\"][\"size\"], 128);\n\n        let _ = fs::remove_file(path);\n    }\n\n    #[tokio::test]\n    async fn test_execute_har_stop_skips_browser_auto_launch() {\n        let path = std::env::temp_dir().join(format!(\n            \"agent-browser-har-stop-{}.har\",\n            unix_timestamp_millis()\n        ));\n        let mut state = DaemonState::new();\n        state.har_entries.push(HarEntry {\n            request_id: \"req-3\".to_string(),\n            wall_time: 1773576000.0,\n            method: \"GET\".to_string(),\n            url: \"https://example.com/\".to_string(),\n            request_headers: vec![],\n            post_data: None,\n            request_body_size: 0,\n            resource_type: \"Document\".to_string(),\n            status: Some(200),\n            status_text: \"OK\".to_string(),\n            http_version: \"HTTP/1.1\".to_string(),\n            response_headers: vec![],\n            mime_type: \"text/html\".to_string(),\n            redirect_url: String::new(),\n            response_body_size: 64,\n            cdp_timing: None,\n            loading_finished_timestamp: None,\n        });\n\n        let result = execute_command(\n            &json!({\n                \"action\": \"har_stop\",\n                \"id\": \"har-stop-1\",\n                \"path\": path.to_string_lossy().to_string()\n            }),\n            &mut state,\n        )\n        .await;\n\n        assert_eq!(result[\"success\"], true);\n        assert_eq!(result[\"data\"][\"requestCount\"], 1);\n        let _ = fs::remove_file(path);\n    }\n\n    #[test]\n    fn test_browser_metadata_from_version_parses_product() {\n        let metadata = browser_metadata_from_version(&json!({\n            \"product\": \"HeadlessChrome/123.0.6312.0\"\n        }))\n        .unwrap();\n\n        assert_eq!(metadata[\"name\"], \"HeadlessChrome\");\n        assert_eq!(metadata[\"version\"], \"123.0.6312.0\");\n    }\n\n    #[tokio::test]\n    async fn test_execute_unknown_command() {\n        let mut state = DaemonState::new();\n        let cmd = json!({ \"action\": \"unknown_action_xyz\", \"id\": \"test-1\" });\n        let result = execute_command(&cmd, &mut state).await;\n        assert_eq!(result[\"success\"], false);\n        let error_msg = result[\"error\"].as_str().unwrap();\n        assert!(\n            error_msg.contains(\"Not yet implemented\") || error_msg.contains(\"Auto-launch failed\"),\n            \"Unexpected error: {}\",\n            error_msg\n        );\n    }\n\n    #[tokio::test]\n    async fn test_execute_empty_action() {\n        let mut state = DaemonState::new();\n        let cmd = json!({ \"id\": \"test-2\" });\n        let result = execute_command(&cmd, &mut state).await;\n        // Empty action triggers auto-launch which will fail without a browser\n        assert_eq!(result[\"success\"], false);\n    }\n\n    #[tokio::test]\n    async fn test_execute_close_without_browser() {\n        let mut state = DaemonState::new();\n        let cmd = json!({ \"action\": \"close\", \"id\": \"test-3\" });\n        let result = execute_command(&cmd, &mut state).await;\n        assert_eq!(result[\"success\"], true);\n        assert_eq!(result[\"data\"][\"closed\"], true);\n    }\n\n    #[tokio::test]\n    async fn test_navigate_without_browser() {\n        let mut state = DaemonState::new();\n        {\n            let mut df = state.domain_filter.write().await;\n            *df = Some(DomainFilter::new(\"example.com\"));\n        }\n        let cmd = json!({\n            \"action\": \"navigate\",\n            \"url\": \"https://blocked.com\",\n            \"id\": \"test-4\"\n        });\n        let result = execute_command(&cmd, &mut state).await;\n        // Will fail because auto-launch fails, but the domain filter won't block since\n        // auto-launch happens first\n        assert_eq!(result[\"success\"], false);\n    }\n\n    #[tokio::test]\n    async fn test_credentials_roundtrip_via_actions() {\n        let _lock = crate::native::auth::AUTH_TEST_MUTEX.lock().unwrap();\n        let key_var = \"AGENT_BROWSER_ENCRYPTION_KEY\";\n        let original = std::env::var(key_var).ok();\n        // SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.\n        unsafe { std::env::set_var(key_var, \"a\".repeat(64)) };\n\n        let mut state = DaemonState::new();\n\n        let set_cmd = json!({\n            \"action\": \"credentials_set\",\n            \"name\": \"test-cred-action\",\n            \"username\": \"user\",\n            \"password\": \"pass\",\n            \"id\": \"c1\"\n        });\n        let result = execute_command(&set_cmd, &mut state).await;\n        assert_eq!(result[\"success\"], true);\n\n        let get_cmd = json!({\n            \"action\": \"credentials_get\",\n            \"name\": \"test-cred-action\",\n            \"id\": \"c2\"\n        });\n        let result = execute_command(&get_cmd, &mut state).await;\n        assert_eq!(result[\"success\"], true);\n        assert_eq!(result[\"data\"][\"username\"], \"user\");\n\n        let list_cmd = json!({ \"action\": \"credentials_list\", \"id\": \"c3\" });\n        let result = execute_command(&list_cmd, &mut state).await;\n        assert_eq!(result[\"success\"], true);\n\n        let del_cmd = json!({\n            \"action\": \"credentials_delete\",\n            \"name\": \"test-cred-action\",\n            \"id\": \"c4\"\n        });\n        let result = execute_command(&del_cmd, &mut state).await;\n        assert_eq!(result[\"success\"], true);\n\n        // SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.\n        match original {\n            Some(val) => unsafe { std::env::set_var(key_var, val) },\n            None => unsafe { std::env::remove_var(key_var) },\n        }\n    }\n\n    #[tokio::test]\n    async fn test_state_list_via_actions() {\n        let mut state = DaemonState::new();\n        let cmd = json!({ \"action\": \"state_list\", \"id\": \"s1\" });\n        let result = execute_command(&cmd, &mut state).await;\n        assert_eq!(result[\"success\"], true);\n        assert!(result[\"data\"][\"files\"].is_array());\n    }\n\n    #[tokio::test]\n    async fn test_build_fetch_patterns_empty_state() {\n        let state = DaemonState::new();\n        let patterns = build_fetch_patterns(&state).await;\n        assert!(\n            patterns.is_empty(),\n            \"No routes/filters/headers → no patterns\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_build_fetch_patterns_with_routes() {\n        let state = DaemonState::new();\n        {\n            let mut routes = state.routes.write().await;\n            routes.push(super::RouteEntry {\n                url_pattern: \"https://example.com/*\".to_string(),\n                response: None,\n                abort: true,\n            });\n        }\n        let patterns = build_fetch_patterns(&state).await;\n        assert_eq!(patterns.len(), 1);\n        assert_eq!(patterns[0][\"urlPattern\"], \"https://example.com/*\");\n    }\n\n    #[tokio::test]\n    async fn test_build_fetch_patterns_adds_wildcard_for_domain_filter() {\n        let state = DaemonState::new();\n        {\n            let mut df = state.domain_filter.write().await;\n            *df = Some(super::super::network::DomainFilter::new(\"example.com\"));\n        }\n        let patterns = build_fetch_patterns(&state).await;\n        assert_eq!(patterns.len(), 1);\n        assert_eq!(patterns[0][\"urlPattern\"], \"*\");\n    }\n\n    #[tokio::test]\n    async fn test_build_fetch_patterns_adds_wildcard_for_origin_headers() {\n        let state = DaemonState::new();\n        {\n            let mut oh = state.origin_headers.write().await;\n            let mut headers = HashMap::new();\n            headers.insert(\"Authorization\".to_string(), \"Bearer xxx\".to_string());\n            oh.insert(\"http://example.com\".to_string(), headers);\n        }\n        let patterns = build_fetch_patterns(&state).await;\n        assert_eq!(patterns.len(), 1);\n        assert_eq!(patterns[0][\"urlPattern\"], \"*\");\n    }\n\n    #[tokio::test]\n    async fn test_build_fetch_patterns_no_duplicate_wildcard() {\n        let state = DaemonState::new();\n        {\n            let mut routes = state.routes.write().await;\n            routes.push(super::RouteEntry {\n                url_pattern: \"*\".to_string(),\n                response: None,\n                abort: false,\n            });\n        }\n        {\n            let mut df = state.domain_filter.write().await;\n            *df = Some(super::super::network::DomainFilter::new(\"example.com\"));\n        }\n        let patterns = build_fetch_patterns(&state).await;\n        assert_eq!(\n            patterns.len(),\n            1,\n            \"Should not add a second wildcard when routes already contain one\"\n        );\n    }\n}\n"
  },
  {
    "path": "cli/src/native/auth.rs",
    "content": "use aes_gcm::{aead::Aead, aead::KeyInit, Aes256Gcm};\nuse base64::{engine::general_purpose::STANDARD, Engine};\nuse serde::{Deserialize, Serialize};\nuse serde_json::{json, Value};\nuse std::fs;\nuse std::io::Write;\nuse std::path::PathBuf;\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AuthProfile {\n    pub name: String,\n    pub url: String,\n    pub username: String,\n    pub password: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub username_selector: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub password_selector: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub submit_selector: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub created_at: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub last_login_at: Option<String>,\n}\n\n// Keep legacy Credential alias for backward compatibility\npub type Credential = AuthProfile;\n\nfn validate_profile_name(name: &str) -> Result<(), String> {\n    if name.is_empty()\n        || !name\n            .chars()\n            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')\n    {\n        return Err(format!(\n            \"Invalid profile name '{}'. Must match /^[a-zA-Z0-9_-]+$/\",\n            name\n        ));\n    }\n    Ok(())\n}\n\nfn get_auth_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\").join(\"auth\")\n    } else {\n        std::env::temp_dir().join(\"agent-browser\").join(\"auth\")\n    }\n}\n\nfn get_profile_path(name: &str) -> PathBuf {\n    get_auth_dir().join(format!(\"{}.json\", name))\n}\n\nconst ENCRYPTION_KEY_ENV: &str = \"AGENT_BROWSER_ENCRYPTION_KEY\";\nconst KEY_FILE_NAME: &str = \".encryption-key\";\n\nfn get_agent_browser_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\")\n    } else {\n        std::env::temp_dir().join(\"agent-browser\")\n    }\n}\n\nfn get_key_file_path() -> PathBuf {\n    get_agent_browser_dir().join(KEY_FILE_NAME)\n}\n\nfn parse_key_hex(hex_str: &str) -> Option<Vec<u8>> {\n    let hex_str = hex_str.trim();\n    if hex_str.len() != 64 || !hex_str.chars().all(|c| c.is_ascii_hexdigit()) {\n        return None;\n    }\n    let bytes: Vec<u8> = (0..32)\n        .map(|i| u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16).unwrap())\n        .collect();\n    Some(bytes)\n}\n\n/// Read the encryption key from AGENT_BROWSER_ENCRYPTION_KEY env var or\n/// ~/.agent-browser/.encryption-key file (matching the Node.js implementation).\nfn get_encryption_key() -> Result<Vec<u8>, String> {\n    if let Ok(key_hex) = std::env::var(ENCRYPTION_KEY_ENV) {\n        return parse_key_hex(&key_hex).ok_or_else(|| {\n            format!(\n                \"{} should be a 64-character hex string (256 bits). Generate one with: openssl rand -hex 32\",\n                ENCRYPTION_KEY_ENV\n            )\n        });\n    }\n\n    let key_file = get_key_file_path();\n    if key_file.exists() {\n        let hex = fs::read_to_string(&key_file)\n            .map_err(|e| format!(\"Failed to read encryption key file: {}\", e))?;\n        return parse_key_hex(&hex).ok_or_else(|| {\n            format!(\n                \"Invalid encryption key in {}. Expected 64-character hex string.\",\n                key_file.display()\n            )\n        });\n    }\n\n    Err(format!(\n        \"Encryption key required. Set {} or ensure {} exists.\",\n        ENCRYPTION_KEY_ENV,\n        key_file.display()\n    ))\n}\n\n/// Ensure an encryption key exists, auto-generating one if needed.\nfn ensure_encryption_key() -> Result<Vec<u8>, String> {\n    if let Ok(key) = get_encryption_key() {\n        return Ok(key);\n    }\n\n    let mut key = [0u8; 32];\n    getrandom::getrandom(&mut key).map_err(|e| format!(\"Failed to generate key: {}\", e))?;\n    let key_hex = key.iter().map(|b| format!(\"{:02x}\", b)).collect::<String>();\n\n    let dir = get_agent_browser_dir();\n    fs::create_dir_all(&dir).map_err(|e| format!(\"Failed to create directory: {}\", e))?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        let _ = fs::set_permissions(&dir, fs::Permissions::from_mode(0o700));\n    }\n\n    let key_file = get_key_file_path();\n    fs::write(&key_file, format!(\"{}\\n\", key_hex))\n        .map_err(|e| format!(\"Failed to write encryption key: {}\", e))?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        let _ = fs::set_permissions(&key_file, fs::Permissions::from_mode(0o600));\n    }\n\n    let _ = writeln!(\n        std::io::stderr(),\n        \"[agent-browser] Auto-generated encryption key at {} -- back up this file or set {}\",\n        key_file.display(),\n        ENCRYPTION_KEY_ENV\n    );\n\n    Ok(key.to_vec())\n}\n\n/// Encrypt a profile to the JSON+base64 format compatible with Node.js.\nfn encrypt_profile(profile: &AuthProfile) -> Result<String, String> {\n    let key = ensure_encryption_key()?;\n    let cipher =\n        Aes256Gcm::new_from_slice(&key).map_err(|e| format!(\"Encryption key error: {}\", e))?;\n\n    let plaintext = serde_json::to_string(profile)\n        .map_err(|e| format!(\"Failed to serialize profile: {}\", e))?;\n\n    let mut iv = [0u8; 12];\n    getrandom::getrandom(&mut iv).map_err(|e| format!(\"Failed to generate IV: {}\", e))?;\n\n    // aes_gcm appends the 16-byte auth tag to the ciphertext\n    let encrypted = cipher\n        .encrypt(aes_gcm::Nonce::from_slice(&iv), plaintext.as_bytes())\n        .map_err(|e| format!(\"Encryption failed: {}\", e))?;\n\n    let tag_offset = encrypted.len() - 16;\n    let ciphertext = &encrypted[..tag_offset];\n    let auth_tag = &encrypted[tag_offset..];\n\n    let payload = json!({\n        \"version\": 1,\n        \"encrypted\": true,\n        \"iv\": STANDARD.encode(iv),\n        \"authTag\": STANDARD.encode(auth_tag),\n        \"data\": STANDARD.encode(ciphertext),\n    });\n\n    serde_json::to_string_pretty(&payload)\n        .map_err(|e| format!(\"Failed to serialize payload: {}\", e))\n}\n\n/// JSON envelope written by Node.js encryption (src/encryption.ts).\n#[derive(Deserialize)]\nstruct EncryptedPayload {\n    #[allow(dead_code)]\n    version: u32,\n    #[allow(dead_code)]\n    encrypted: bool,\n    iv: String,\n    #[serde(rename = \"authTag\")]\n    auth_tag: String,\n    data: String,\n}\n\nfn decrypt_profile(data: &[u8]) -> Result<AuthProfile, String> {\n    let text = std::str::from_utf8(data).map_err(|_| {\n        \"Profile is not valid UTF-8 -- it may use an older incompatible binary format\".to_string()\n    })?;\n\n    if let Ok(payload) = serde_json::from_str::<EncryptedPayload>(text) {\n        let key = get_encryption_key()?;\n\n        let iv = STANDARD\n            .decode(&payload.iv)\n            .map_err(|e| format!(\"Invalid base64 iv: {}\", e))?;\n        let auth_tag = STANDARD\n            .decode(&payload.auth_tag)\n            .map_err(|e| format!(\"Invalid base64 authTag: {}\", e))?;\n        let ciphertext = STANDARD\n            .decode(&payload.data)\n            .map_err(|e| format!(\"Invalid base64 data: {}\", e))?;\n\n        // aes_gcm expects ciphertext || auth_tag as input to decrypt\n        let mut combined = Vec::with_capacity(ciphertext.len() + auth_tag.len());\n        combined.extend_from_slice(&ciphertext);\n        combined.extend_from_slice(&auth_tag);\n\n        let cipher =\n            Aes256Gcm::new_from_slice(&key).map_err(|e| format!(\"Decryption key error: {}\", e))?;\n        let plaintext = cipher\n            .decrypt(aes_gcm::Nonce::from_slice(&iv), combined.as_slice())\n            .map_err(|e| format!(\"Decryption failed: {}\", e))?;\n\n        let json_str = String::from_utf8(plaintext)\n            .map_err(|e| format!(\"Decrypted data is not valid UTF-8: {}\", e))?;\n        return serde_json::from_str(&json_str).map_err(|e| format!(\"Invalid profile data: {}\", e));\n    }\n\n    // Fallback: try as plain unencrypted JSON profile\n    serde_json::from_str::<AuthProfile>(text)\n        .map_err(|_| \"Profile is not a valid encrypted or unencrypted payload\".to_string())\n}\n\nfn save_profile(profile: &AuthProfile) -> Result<(), String> {\n    let dir = get_auth_dir();\n    fs::create_dir_all(&dir).map_err(|e| format!(\"Failed to create auth dir: {}\", e))?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        let _ = fs::set_permissions(&dir, fs::Permissions::from_mode(0o700));\n    }\n\n    let encrypted_json = encrypt_profile(profile)?;\n    let path = get_profile_path(&profile.name);\n    fs::write(&path, &encrypted_json).map_err(|e| format!(\"Failed to write profile: {}\", e))?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o600));\n    }\n    Ok(())\n}\n\nfn load_profile(name: &str) -> Result<AuthProfile, String> {\n    let path = get_profile_path(name);\n    if !path.exists() {\n        return Err(format!(\"Auth profile '{}' not found\", name));\n    }\n    let data = fs::read(&path).map_err(|e| format!(\"Failed to read profile: {}\", e))?;\n    decrypt_profile(&data)\n}\n\npub fn credentials_set(\n    name: &str,\n    username: &str,\n    password: &str,\n    url: Option<&str>,\n) -> Result<Value, String> {\n    validate_profile_name(name)?;\n    let profile = AuthProfile {\n        name: name.to_string(),\n        url: url.unwrap_or(\"\").to_string(),\n        username: username.to_string(),\n        password: password.to_string(),\n        username_selector: None,\n        password_selector: None,\n        submit_selector: None,\n        created_at: None,\n        last_login_at: None,\n    };\n    save_profile(&profile)?;\n    Ok(json!({ \"saved\": name }))\n}\n\npub fn auth_save(\n    name: &str,\n    url: &str,\n    username: &str,\n    password: &str,\n    username_selector: Option<&str>,\n    password_selector: Option<&str>,\n    submit_selector: Option<&str>,\n) -> Result<Value, String> {\n    validate_profile_name(name)?;\n    let profile = AuthProfile {\n        name: name.to_string(),\n        url: url.to_string(),\n        username: username.to_string(),\n        password: password.to_string(),\n        username_selector: username_selector.map(String::from),\n        password_selector: password_selector.map(String::from),\n        submit_selector: submit_selector.map(String::from),\n        created_at: None,\n        last_login_at: None,\n    };\n    save_profile(&profile)?;\n    Ok(json!({ \"saved\": name }))\n}\n\npub fn credentials_get(name: &str) -> Result<Value, String> {\n    let profile = load_profile(name)?;\n    Ok(json!({\n        \"name\": profile.name,\n        \"username\": profile.username,\n        \"url\": profile.url,\n        \"hasPassword\": true,\n    }))\n}\n\npub fn credentials_get_full(name: &str) -> Result<AuthProfile, String> {\n    load_profile(name)\n}\n\npub fn credentials_delete(name: &str) -> Result<Value, String> {\n    validate_profile_name(name)?;\n    let path = get_profile_path(name);\n    if !path.exists() {\n        return Err(format!(\"Auth profile '{}' not found\", name));\n    }\n    fs::remove_file(&path).map_err(|e| format!(\"Failed to delete profile: {}\", e))?;\n    Ok(json!({ \"deleted\": name }))\n}\n\npub fn credentials_list() -> Result<Value, String> {\n    let dir = get_auth_dir();\n    if !dir.exists() {\n        return Ok(json!({ \"profiles\": [] }));\n    }\n\n    let mut profiles = Vec::new();\n    if let Ok(entries) = fs::read_dir(&dir) {\n        for entry in entries.flatten() {\n            let path = entry.path();\n            if path.extension().and_then(|e| e.to_str()) != Some(\"json\") {\n                continue;\n            }\n            let name = path\n                .file_stem()\n                .unwrap_or_default()\n                .to_string_lossy()\n                .to_string();\n            match load_profile(&name) {\n                Ok(profile) => {\n                    profiles.push(json!({\n                        \"name\": profile.name,\n                        \"username\": profile.username,\n                        \"url\": profile.url,\n                    }));\n                }\n                Err(_) => {\n                    profiles.push(json!({\n                        \"name\": name,\n                        \"error\": \"Failed to decrypt\",\n                    }));\n                }\n            }\n        }\n    }\n    Ok(json!({ \"profiles\": profiles }))\n}\n\npub fn auth_show(name: &str) -> Result<Value, String> {\n    validate_profile_name(name)?;\n    let profile = load_profile(name)?;\n    Ok(json!({\n        \"profile\": {\n            \"name\": profile.name,\n            \"url\": profile.url,\n            \"username\": profile.username,\n            \"usernameSelector\": profile.username_selector,\n            \"passwordSelector\": profile.password_selector,\n            \"submitSelector\": profile.submit_selector,\n        }\n    }))\n}\n\n#[cfg(test)]\npub(crate) static AUTH_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn with_test_key<F: FnOnce()>(f: F) {\n        let _lock = AUTH_TEST_MUTEX.lock().unwrap();\n        let original = std::env::var(ENCRYPTION_KEY_ENV).ok();\n        let test_key = \"a\".repeat(64);\n        // SAFETY: TEST_MUTEX serializes all test access so no concurrent mutation.\n        unsafe { std::env::set_var(ENCRYPTION_KEY_ENV, &test_key) };\n        f();\n        // SAFETY: TEST_MUTEX serializes all test access so no concurrent mutation.\n        match original {\n            Some(val) => unsafe { std::env::set_var(ENCRYPTION_KEY_ENV, val) },\n            None => unsafe { std::env::remove_var(ENCRYPTION_KEY_ENV) },\n        }\n    }\n\n    #[test]\n    fn test_validate_profile_name() {\n        assert!(validate_profile_name(\"github\").is_ok());\n        assert!(validate_profile_name(\"my-app\").is_ok());\n        assert!(validate_profile_name(\"test_123\").is_ok());\n        assert!(validate_profile_name(\"\").is_err());\n        assert!(validate_profile_name(\"has space\").is_err());\n        assert!(validate_profile_name(\"../evil\").is_err());\n        assert!(validate_profile_name(\"foo/bar\").is_err());\n    }\n\n    #[test]\n    fn test_auth_profile_serialization() {\n        let profile = AuthProfile {\n            name: \"test\".to_string(),\n            url: \"https://example.com\".to_string(),\n            username: \"user\".to_string(),\n            password: \"pass\".to_string(),\n            username_selector: None,\n            password_selector: None,\n            submit_selector: Some(\"button[type=submit]\".to_string()),\n            created_at: None,\n            last_login_at: None,\n        };\n        let json = serde_json::to_string(&profile).unwrap();\n        let parsed: AuthProfile = serde_json::from_str(&json).unwrap();\n        assert_eq!(parsed.name, \"test\");\n        assert_eq!(\n            parsed.submit_selector,\n            Some(\"button[type=submit]\".to_string())\n        );\n        assert!(parsed.username_selector.is_none());\n    }\n\n    #[test]\n    fn test_encrypt_decrypt_roundtrip() {\n        with_test_key(|| {\n            let profile = AuthProfile {\n                name: \"roundtrip\".to_string(),\n                url: \"https://example.com\".to_string(),\n                username: \"user\".to_string(),\n                password: \"s3cret!\".to_string(),\n                username_selector: None,\n                password_selector: None,\n                submit_selector: None,\n                created_at: None,\n                last_login_at: None,\n            };\n            let encrypted_json = encrypt_profile(&profile).unwrap();\n            let decrypted = decrypt_profile(encrypted_json.as_bytes()).unwrap();\n            assert_eq!(decrypted.name, \"roundtrip\");\n            assert_eq!(decrypted.password, \"s3cret!\");\n        });\n    }\n\n    #[test]\n    fn test_get_encryption_key_from_env() {\n        with_test_key(|| {\n            let key = get_encryption_key().unwrap();\n            assert_eq!(key.len(), 32);\n            assert!(key.iter().all(|&b| b == 0xaa));\n        });\n    }\n\n    #[test]\n    fn test_parse_key_hex_valid() {\n        let hex = \"ab\".repeat(32);\n        let key = parse_key_hex(&hex).unwrap();\n        assert_eq!(key.len(), 32);\n        assert!(key.iter().all(|&b| b == 0xab));\n    }\n\n    #[test]\n    fn test_parse_key_hex_invalid() {\n        assert!(parse_key_hex(\"too_short\").is_none());\n        assert!(parse_key_hex(&\"g\".repeat(64)).is_none());\n        assert!(parse_key_hex(\"\").is_none());\n    }\n\n    #[test]\n    fn test_decrypt_json_payload_format() {\n        with_test_key(|| {\n            let key = get_encryption_key().unwrap();\n            let profile = AuthProfile {\n                name: \"json-test\".to_string(),\n                url: \"https://example.com/login\".to_string(),\n                username: \"admin\".to_string(),\n                password: \"hunter2\".to_string(),\n                username_selector: Some(\"#email\".to_string()),\n                password_selector: None,\n                submit_selector: None,\n                created_at: None,\n                last_login_at: None,\n            };\n\n            // Encrypt with aes_gcm, then manually build the JSON payload\n            // to simulate what Node.js would produce\n            let cipher = Aes256Gcm::new_from_slice(&key).unwrap();\n            let mut iv = [0u8; 12];\n            getrandom::getrandom(&mut iv).unwrap();\n            let plaintext = serde_json::to_string(&profile).unwrap();\n            let encrypted = cipher\n                .encrypt(aes_gcm::Nonce::from_slice(&iv), plaintext.as_bytes())\n                .unwrap();\n\n            let tag_offset = encrypted.len() - 16;\n            let ciphertext = &encrypted[..tag_offset];\n            let auth_tag = &encrypted[tag_offset..];\n\n            let payload = format!(\n                r#\"{{\"version\":1,\"encrypted\":true,\"iv\":\"{}\",\"authTag\":\"{}\",\"data\":\"{}\"}}\"#,\n                STANDARD.encode(iv),\n                STANDARD.encode(auth_tag),\n                STANDARD.encode(ciphertext),\n            );\n\n            let decrypted = decrypt_profile(payload.as_bytes()).unwrap();\n            assert_eq!(decrypted.name, \"json-test\");\n            assert_eq!(decrypted.password, \"hunter2\");\n            assert_eq!(decrypted.username_selector, Some(\"#email\".to_string()));\n        });\n    }\n\n    #[test]\n    fn test_encrypted_output_is_json_format() {\n        with_test_key(|| {\n            let profile = AuthProfile {\n                name: \"format-check\".to_string(),\n                url: \"https://example.com\".to_string(),\n                username: \"user\".to_string(),\n                password: \"pass\".to_string(),\n                username_selector: None,\n                password_selector: None,\n                submit_selector: None,\n                created_at: None,\n                last_login_at: None,\n            };\n            let encrypted = encrypt_profile(&profile).unwrap();\n            let parsed: Value = serde_json::from_str(&encrypted).unwrap();\n            assert_eq!(parsed[\"version\"], 1);\n            assert_eq!(parsed[\"encrypted\"], true);\n            assert!(parsed[\"iv\"].is_string());\n            assert!(parsed[\"authTag\"].is_string());\n            assert!(parsed[\"data\"].is_string());\n        });\n    }\n}\n"
  },
  {
    "path": "cli/src/native/browser.rs",
    "content": "use serde_json::{json, Value};\nuse std::collections::HashSet;\nuse std::future::Future;\nuse std::sync::Arc;\nuse std::time::{Duration, Instant};\nuse tokio::sync::{broadcast, Mutex};\n\nuse super::cdp::chrome::{auto_connect_cdp, launch_chrome, ChromeProcess, LaunchOptions};\nuse super::cdp::client::CdpClient;\nuse super::cdp::discovery::discover_cdp_url;\nuse super::cdp::lightpanda::{launch_lightpanda, LightpandaLaunchOptions, LightpandaProcess};\nuse super::cdp::types::*;\n\n// ---------------------------------------------------------------------------\n// Launch validation\n// ---------------------------------------------------------------------------\n\n/// Validates launch/connect options for incompatible combinations.\n/// Returns `Ok(())` if valid, or `Err(msg)` with a user-friendly error.\npub fn validate_launch_options(\n    extensions: Option<&[String]>,\n    has_cdp: bool,\n    profile: Option<&str>,\n    storage_state: Option<&str>,\n    allow_file_access: bool,\n    executable_path: Option<&str>,\n) -> Result<(), String> {\n    let has_extensions = extensions.map(|e| !e.is_empty()).unwrap_or(false);\n\n    if has_extensions && has_cdp {\n        return Err(\n            \"Cannot use extensions with cdp_url (extensions require local browser launch)\"\n                .to_string(),\n        );\n    }\n    if profile.is_some() && has_cdp {\n        return Err(\n            \"Cannot use profile with cdp_url (profile requires local browser launch)\".to_string(),\n        );\n    }\n    if storage_state.is_some() && profile.is_some() {\n        return Err(\"Cannot use storage_state with profile\".to_string());\n    }\n    if storage_state.is_some() && has_extensions {\n        return Err(\"Cannot use storage_state with extensions\".to_string());\n    }\n    if allow_file_access {\n        if let Some(path) = executable_path {\n            let lower = path.to_lowercase();\n            if lower.contains(\"firefox\") || lower.contains(\"webkit\") || lower.contains(\"safari\") {\n                return Err(\n                    \"allow_file_access is not supported with non-Chromium browsers\".to_string(),\n                );\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Validates that Chrome-only options are not used with Lightpanda.\nfn validate_lightpanda_options(options: &LaunchOptions) -> Result<(), String> {\n    if options\n        .extensions\n        .as_ref()\n        .map(|e| !e.is_empty())\n        .unwrap_or(false)\n    {\n        return Err(\"Extensions are not supported with Lightpanda\".to_string());\n    }\n    if options.profile.is_some() {\n        return Err(\"Profiles are not supported with Lightpanda\".to_string());\n    }\n    if options.storage_state.is_some() {\n        return Err(\"Storage state is not supported with Lightpanda\".to_string());\n    }\n    if options.allow_file_access {\n        return Err(\"File access is not supported with Lightpanda\".to_string());\n    }\n    if !options.headless {\n        return Err(\"Headed mode is not supported with Lightpanda (headless only)\".to_string());\n    }\n    if !options.args.is_empty() {\n        return Err(\n            \"Custom Chrome arguments (--args) are not supported with Lightpanda\".to_string(),\n        );\n    }\n    Ok(())\n}\n\n/// Returns true for Chrome internal targets that should not be selected\n/// during auto-connect (e.g. chrome://, chrome-extension://, devtools://).\nfn is_internal_chrome_target(url: &str) -> bool {\n    url.starts_with(\"chrome://\")\n        || url.starts_with(\"chrome-extension://\")\n        || url.starts_with(\"devtools://\")\n}\n\n/// Converts common error messages into AI-friendly, actionable descriptions.\npub fn to_ai_friendly_error(error: &str) -> String {\n    let lower = error.to_lowercase();\n    if lower.contains(\"strict mode violation\") {\n        return \"Element matched multiple results. Use a more specific selector.\".to_string();\n    }\n    if lower.contains(\"element is not visible\") {\n        return \"Element exists but is not visible. Wait for it to become visible or scroll it into view.\"\n            .to_string();\n    }\n    if lower.contains(\"intercept\") {\n        return \"Another element is covering the target element. Try scrolling or closing overlays.\"\n            .to_string();\n    }\n    if lower.contains(\"timeout\") {\n        return \"Operation timed out. The page may still be loading or the element may not exist.\"\n            .to_string();\n    }\n    if lower.contains(\"element not found\") || lower.contains(\"no element\") {\n        return \"Element not found. Verify the selector is correct and the element exists in the DOM.\"\n            .to_string();\n    }\n    error.to_string()\n}\n\n#[derive(Debug, Clone)]\npub struct PageInfo {\n    pub target_id: String,\n    pub session_id: String,\n    pub url: String,\n    pub title: String,\n    pub target_type: String, // \"page\" or \"webview\"\n}\n\n#[derive(Debug, Clone, Copy)]\npub enum WaitUntil {\n    Load,\n    DomContentLoaded,\n    NetworkIdle,\n}\n\nimpl WaitUntil {\n    pub fn from_str(s: &str) -> Self {\n        match s {\n            \"domcontentloaded\" => Self::DomContentLoaded,\n            \"networkidle\" => Self::NetworkIdle,\n            _ => Self::Load,\n        }\n    }\n}\n\npub enum BrowserProcess {\n    Chrome(ChromeProcess),\n    Lightpanda(LightpandaProcess),\n}\n\nimpl BrowserProcess {\n    pub fn kill(&mut self) {\n        match self {\n            BrowserProcess::Chrome(p) => p.kill(),\n            BrowserProcess::Lightpanda(p) => p.kill(),\n        }\n    }\n\n    pub fn wait_or_kill(&mut self, timeout: std::time::Duration) {\n        match self {\n            BrowserProcess::Chrome(p) => p.wait_or_kill(timeout),\n            BrowserProcess::Lightpanda(p) => p.kill(),\n        }\n    }\n}\n\npub struct BrowserManager {\n    pub client: Arc<CdpClient>,\n    browser_process: Option<BrowserProcess>,\n    ws_url: String,\n    pages: Vec<PageInfo>,\n    active_page_index: usize,\n    default_timeout_ms: u64,\n}\n\nconst LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);\nconst LIGHTPANDA_CDP_CONNECT_POLL_INTERVAL: Duration = Duration::from_millis(100);\nconst LIGHTPANDA_TARGET_INIT_TIMEOUT: Duration = Duration::from_secs(10);\n\nimpl BrowserManager {\n    pub async fn launch(options: LaunchOptions, engine: Option<&str>) -> Result<Self, String> {\n        let engine = engine.unwrap_or(\"chrome\");\n\n        match engine {\n            \"chrome\" => {\n                validate_launch_options(\n                    options.extensions.as_deref(),\n                    false,\n                    options.profile.as_deref(),\n                    options.storage_state.as_deref(),\n                    options.allow_file_access,\n                    options.executable_path.as_deref(),\n                )?;\n            }\n            \"lightpanda\" => {\n                validate_lightpanda_options(&options)?;\n            }\n            _ => {\n                return Err(format!(\n                    \"Unknown engine '{}'. Supported engines: chrome, lightpanda\",\n                    engine\n                ));\n            }\n        }\n\n        let ignore_https_errors = options.ignore_https_errors;\n        let user_agent = options.user_agent.clone();\n        let color_scheme = options.color_scheme.clone();\n        let download_path = options.download_path.clone();\n\n        let (ws_url, process) = match engine {\n            \"lightpanda\" => {\n                let lp_options = LightpandaLaunchOptions {\n                    executable_path: options.executable_path.clone(),\n                    proxy: options.proxy.clone(),\n                    port: None,\n                };\n                let lp = launch_lightpanda(&lp_options).await?;\n                let url = lp.ws_url.clone();\n                (url, BrowserProcess::Lightpanda(lp))\n            }\n            _ => {\n                let chrome = tokio::task::spawn_blocking(move || launch_chrome(&options))\n                    .await\n                    .map_err(|e| format!(\"Chrome launch task failed: {}\", e))??;\n                let url = chrome.ws_url.clone();\n                (url, BrowserProcess::Chrome(chrome))\n            }\n        };\n\n        let manager = if engine == \"lightpanda\" {\n            initialize_lightpanda_manager(ws_url, process).await?\n        } else {\n            let client = Arc::new(CdpClient::connect(&ws_url).await?);\n            let mut manager = Self {\n                client,\n                browser_process: Some(process),\n                ws_url,\n                pages: Vec::new(),\n                active_page_index: 0,\n                default_timeout_ms: 25_000,\n            };\n            manager.discover_and_attach_targets().await?;\n            manager\n        };\n\n        let session_id = manager.active_session_id()?.to_string();\n\n        if ignore_https_errors {\n            let _ = manager\n                .client\n                .send_command(\n                    \"Security.setIgnoreCertificateErrors\",\n                    Some(json!({ \"ignore\": true })),\n                    Some(&session_id),\n                )\n                .await;\n        }\n\n        if let Some(ref ua) = user_agent {\n            let _ = manager\n                .client\n                .send_command(\n                    \"Emulation.setUserAgentOverride\",\n                    Some(json!({ \"userAgent\": ua })),\n                    Some(&session_id),\n                )\n                .await;\n        }\n\n        if let Some(ref scheme) = color_scheme {\n            let _ = manager\n                .client\n                .send_command(\n                    \"Emulation.setEmulatedMedia\",\n                    Some(json!({ \"features\": [{ \"name\": \"prefers-color-scheme\", \"value\": scheme }] })),\n                    Some(&session_id),\n                )\n                .await;\n        }\n\n        if let Some(ref path) = download_path {\n            let _ = manager\n                .client\n                .send_command(\n                    \"Browser.setDownloadBehavior\",\n                    Some(json!({ \"behavior\": \"allow\", \"downloadPath\": path })),\n                    None,\n                )\n                .await;\n        }\n\n        Ok(manager)\n    }\n\n    pub async fn connect_cdp(url: &str) -> Result<Self, String> {\n        let ws_url = resolve_cdp_url(url).await?;\n        let client = Arc::new(CdpClient::connect(&ws_url).await?);\n        let mut manager = Self {\n            client,\n            browser_process: None,\n            ws_url,\n            pages: Vec::new(),\n            active_page_index: 0,\n            default_timeout_ms: 10_000,\n        };\n\n        manager.discover_and_attach_targets().await?;\n        Ok(manager)\n    }\n\n    pub async fn connect_auto() -> Result<Self, String> {\n        let ws_url = auto_connect_cdp().await?;\n        Self::connect_cdp(&ws_url).await\n    }\n\n    async fn discover_and_attach_targets(&mut self) -> Result<(), String> {\n        self.client\n            .send_command_typed::<_, Value>(\n                \"Target.setDiscoverTargets\",\n                &SetDiscoverTargetsParams { discover: true },\n                None,\n            )\n            .await?;\n\n        let result: GetTargetsResult = self\n            .client\n            .send_command_typed(\"Target.getTargets\", &json!({}), None)\n            .await?;\n\n        let page_targets: Vec<TargetInfo> = result\n            .target_infos\n            .into_iter()\n            .filter(|t| {\n                (t.target_type == \"page\" || t.target_type == \"webview\")\n                    && !t.url.is_empty()\n                    && !is_internal_chrome_target(&t.url)\n            })\n            .collect();\n\n        if page_targets.is_empty() {\n            // Create a new tab\n            let result: CreateTargetResult = self\n                .client\n                .send_command_typed(\n                    \"Target.createTarget\",\n                    &CreateTargetParams {\n                        url: \"about:blank\".to_string(),\n                    },\n                    None,\n                )\n                .await?;\n\n            let attach_result: AttachToTargetResult = self\n                .client\n                .send_command_typed(\n                    \"Target.attachToTarget\",\n                    &AttachToTargetParams {\n                        target_id: result.target_id.clone(),\n                        flatten: true,\n                    },\n                    None,\n                )\n                .await?;\n\n            self.pages.push(PageInfo {\n                target_id: result.target_id,\n                session_id: attach_result.session_id.clone(),\n                url: \"about:blank\".to_string(),\n                title: String::new(),\n                target_type: \"page\".to_string(),\n            });\n            self.active_page_index = 0;\n            self.enable_domains(&attach_result.session_id).await?;\n        } else {\n            for target in &page_targets {\n                let attach_result: AttachToTargetResult = self\n                    .client\n                    .send_command_typed(\n                        \"Target.attachToTarget\",\n                        &AttachToTargetParams {\n                            target_id: target.target_id.clone(),\n                            flatten: true,\n                        },\n                        None,\n                    )\n                    .await?;\n\n                self.pages.push(PageInfo {\n                    target_id: target.target_id.clone(),\n                    session_id: attach_result.session_id.clone(),\n                    url: target.url.clone(),\n                    title: target.title.clone(),\n                    target_type: target.target_type.clone(),\n                });\n            }\n\n            self.active_page_index = 0;\n            let session_id = self.pages[0].session_id.clone();\n            self.enable_domains(&session_id).await?;\n        }\n\n        Ok(())\n    }\n\n    pub async fn enable_domains_pub(&self, session_id: &str) -> Result<(), String> {\n        self.enable_domains(session_id).await\n    }\n\n    async fn enable_domains(&self, session_id: &str) -> Result<(), String> {\n        self.client\n            .send_command_no_params(\"Page.enable\", Some(session_id))\n            .await?;\n        self.client\n            .send_command_no_params(\"Runtime.enable\", Some(session_id))\n            .await?;\n        self.client\n            .send_command_no_params(\"Network.enable\", Some(session_id))\n            .await?;\n        Ok(())\n    }\n\n    pub fn active_session_id(&self) -> Result<&str, String> {\n        self.pages\n            .get(self.active_page_index)\n            .map(|p| p.session_id.as_str())\n            .ok_or_else(|| \"No active page\".to_string())\n    }\n\n    pub async fn navigate(&mut self, url: &str, wait_until: WaitUntil) -> Result<Value, String> {\n        let session_id = self.active_session_id()?.to_string();\n        let mut lifecycle_rx = self.client.subscribe();\n\n        let nav_result: PageNavigateResult = self\n            .client\n            .send_command_typed(\n                \"Page.navigate\",\n                &PageNavigateParams {\n                    url: url.to_string(),\n                    referrer: None,\n                },\n                Some(&session_id),\n            )\n            .await?;\n\n        if let Some(ref error_text) = nav_result.error_text {\n            return Err(format!(\"Navigation failed: {}\", error_text));\n        }\n\n        self.wait_for_lifecycle(wait_until, &session_id, &mut lifecycle_rx)\n            .await?;\n\n        let page_url = self.get_url().await.unwrap_or_else(|_| url.to_string());\n        let title = self.get_title().await.unwrap_or_default();\n\n        if let Some(page) = self.pages.get_mut(self.active_page_index) {\n            page.url = page_url.clone();\n            page.title = title.clone();\n        }\n\n        Ok(json!({ \"url\": page_url, \"title\": title }))\n    }\n\n    async fn wait_for_lifecycle(\n        &self,\n        wait_until: WaitUntil,\n        session_id: &str,\n        rx: &mut broadcast::Receiver<CdpEvent>,\n    ) -> Result<(), String> {\n        let event_name = match wait_until {\n            WaitUntil::Load => \"Page.loadEventFired\",\n            WaitUntil::DomContentLoaded => \"Page.domContentEventFired\",\n            WaitUntil::NetworkIdle => return self.wait_for_network_idle(session_id, rx).await,\n        };\n\n        let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms);\n\n        tokio::time::timeout(timeout, async {\n            loop {\n                match rx.recv().await {\n                    Ok(event) => {\n                        if event.method == event_name\n                            && event.session_id.as_deref() == Some(session_id)\n                        {\n                            return Ok(());\n                        }\n                    }\n                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,\n                    Err(tokio::sync::broadcast::error::RecvError::Closed) => break,\n                }\n            }\n            Err(\"Event stream closed\".to_string())\n        })\n        .await\n        .map_err(|_| format!(\"Timeout waiting for {}\", event_name))?\n    }\n\n    async fn wait_for_network_idle(\n        &self,\n        session_id: &str,\n        rx: &mut broadcast::Receiver<CdpEvent>,\n    ) -> Result<(), String> {\n        let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms);\n        poll_network_idle(session_id, rx, timeout).await\n    }\n\n    pub async fn get_url(&self) -> Result<String, String> {\n        let result = self.evaluate_simple(\"location.href\").await?;\n        Ok(result.as_str().unwrap_or(\"\").to_string())\n    }\n\n    pub async fn get_title(&self) -> Result<String, String> {\n        let result = self.evaluate_simple(\"document.title\").await?;\n        Ok(result.as_str().unwrap_or(\"\").to_string())\n    }\n\n    pub async fn get_content(&self) -> Result<String, String> {\n        let result = self\n            .evaluate_simple(\"document.documentElement.outerHTML\")\n            .await?;\n        Ok(result.as_str().unwrap_or(\"\").to_string())\n    }\n\n    pub async fn evaluate(&self, script: &str, _args: Option<Value>) -> Result<Value, String> {\n        let session_id = self.active_session_id()?.to_string();\n\n        let result: EvaluateResult = self\n            .client\n            .send_command_typed(\n                \"Runtime.evaluate\",\n                &EvaluateParams {\n                    expression: script.to_string(),\n                    return_by_value: Some(true),\n                    await_promise: Some(true),\n                },\n                Some(&session_id),\n            )\n            .await?;\n\n        if let Some(ref details) = result.exception_details {\n            let msg = details\n                .exception\n                .as_ref()\n                .and_then(|e| e.description.as_deref())\n                .unwrap_or(&details.text);\n            return Err(format!(\"Evaluation error: {}\", msg));\n        }\n\n        Ok(result.result.value.unwrap_or(Value::Null))\n    }\n\n    async fn evaluate_simple(&self, expression: &str) -> Result<Value, String> {\n        self.evaluate(expression, None).await\n    }\n\n    pub async fn wait_for_lifecycle_external(\n        &self,\n        wait_until: WaitUntil,\n        session_id: &str,\n    ) -> Result<(), String> {\n        let mut rx = self.client.subscribe();\n        self.wait_for_lifecycle(wait_until, session_id, &mut rx)\n            .await\n    }\n\n    pub async fn close(&mut self) -> Result<(), String> {\n        if self.browser_process.is_some() {\n            // Only send Browser.close when we launched the browser ourselves.\n            // For external connections (--auto-connect, --cdp) we just disconnect\n            // without shutting down the user's browser.\n            let _ = self\n                .client\n                .send_command_no_params(\"Browser.close\", None)\n                .await;\n        }\n\n        if let Some(mut process) = self.browser_process.take() {\n            let timeout = std::time::Duration::from_secs(5);\n            let _ = tokio::task::spawn_blocking(move || {\n                process.wait_or_kill(timeout);\n            })\n            .await;\n        }\n\n        Ok(())\n    }\n\n    pub fn has_pages(&self) -> bool {\n        !self.pages.is_empty()\n    }\n\n    /// Checks if the CDP connection is alive by sending a simple command.\n    /// Returns false if the command times out or fails.\n    pub async fn is_connection_alive(&self) -> bool {\n        let timeout = tokio::time::Duration::from_secs(3);\n        let result = tokio::time::timeout(\n            timeout,\n            self.client\n                .send_command_no_params(\"Browser.getVersion\", None),\n        )\n        .await;\n\n        match result {\n            Ok(Ok(_)) => true,\n            Ok(Err(_)) | Err(_) => false,\n        }\n    }\n\n    pub fn get_cdp_url(&self) -> &str {\n        &self.ws_url\n    }\n\n    /// Returns the Chrome debug server address as \"host:port\".\n    pub fn chrome_host_port(&self) -> &str {\n        let stripped = self\n            .ws_url\n            .strip_prefix(\"ws://\")\n            .or_else(|| self.ws_url.strip_prefix(\"wss://\"))\n            .unwrap_or(&self.ws_url);\n        stripped.split('/').next().unwrap_or(stripped)\n    }\n\n    pub fn active_target_id(&self) -> Result<&str, String> {\n        self.pages\n            .get(self.active_page_index)\n            .map(|p| p.target_id.as_str())\n            .ok_or_else(|| \"No active page\".to_string())\n    }\n\n    /// Returns true if this manager was connected via CDP (as opposed to local launch).\n    pub fn is_cdp_connection(&self) -> bool {\n        self.browser_process.is_none()\n    }\n\n    /// Ensures the browser has at least one page. If `pages` is empty, creates a new\n    /// about:blank page and attaches to it.\n    pub async fn ensure_page(&mut self) -> Result<(), String> {\n        if !self.pages.is_empty() {\n            return Ok(());\n        }\n\n        let result: CreateTargetResult = self\n            .client\n            .send_command_typed(\n                \"Target.createTarget\",\n                &CreateTargetParams {\n                    url: \"about:blank\".to_string(),\n                },\n                None,\n            )\n            .await?;\n\n        let attach_result: AttachToTargetResult = self\n            .client\n            .send_command_typed(\n                \"Target.attachToTarget\",\n                &AttachToTargetParams {\n                    target_id: result.target_id.clone(),\n                    flatten: true,\n                },\n                None,\n            )\n            .await?;\n\n        self.pages.push(PageInfo {\n            target_id: result.target_id,\n            session_id: attach_result.session_id.clone(),\n            url: \"about:blank\".to_string(),\n            title: String::new(),\n            target_type: \"page\".to_string(),\n        });\n        self.active_page_index = 0;\n        self.enable_domains(&attach_result.session_id).await?;\n\n        Ok(())\n    }\n\n    // -----------------------------------------------------------------------\n    // Tab management\n    // -----------------------------------------------------------------------\n\n    /// Checks if `active_page_index` is still valid and adjusts it if not\n    /// (e.g., after a tab was closed).\n    pub fn update_active_page_if_needed(&mut self) {\n        if self.pages.is_empty() {\n            self.active_page_index = 0;\n            return;\n        }\n        if self.active_page_index >= self.pages.len() {\n            self.active_page_index = self.pages.len() - 1;\n        }\n    }\n\n    pub fn tab_list(&self) -> Vec<Value> {\n        self.pages\n            .iter()\n            .enumerate()\n            .map(|(i, p)| {\n                json!({\n                    \"index\": i,\n                    \"title\": p.title,\n                    \"url\": p.url,\n                    \"type\": p.target_type,\n                    \"active\": i == self.active_page_index,\n                })\n            })\n            .collect()\n    }\n\n    pub async fn tab_new(&mut self, url: Option<&str>) -> Result<Value, String> {\n        let target_url = url.unwrap_or(\"about:blank\");\n\n        let result: CreateTargetResult = self\n            .client\n            .send_command_typed(\n                \"Target.createTarget\",\n                &CreateTargetParams {\n                    url: target_url.to_string(),\n                },\n                None,\n            )\n            .await?;\n\n        let attach: AttachToTargetResult = self\n            .client\n            .send_command_typed(\n                \"Target.attachToTarget\",\n                &AttachToTargetParams {\n                    target_id: result.target_id.clone(),\n                    flatten: true,\n                },\n                None,\n            )\n            .await?;\n\n        self.enable_domains(&attach.session_id).await?;\n\n        let index = self.pages.len();\n        self.pages.push(PageInfo {\n            target_id: result.target_id,\n            session_id: attach.session_id,\n            url: target_url.to_string(),\n            title: String::new(),\n            target_type: \"page\".to_string(),\n        });\n        self.active_page_index = index;\n\n        Ok(json!({ \"index\": index, \"url\": target_url }))\n    }\n\n    pub async fn tab_switch(&mut self, index: usize) -> Result<Value, String> {\n        if index >= self.pages.len() {\n            return Err(format!(\n                \"Tab index {} out of range (0-{})\",\n                index,\n                self.pages.len().saturating_sub(1)\n            ));\n        }\n\n        self.active_page_index = index;\n        let session_id = self.pages[index].session_id.clone();\n        self.enable_domains(&session_id).await?;\n\n        // Bring tab to front\n        let _ = self\n            .client\n            .send_command(\"Page.bringToFront\", None, Some(&session_id))\n            .await;\n\n        let url = self.get_url().await.unwrap_or_default();\n        let title = self.get_title().await.unwrap_or_default();\n\n        if let Some(page) = self.pages.get_mut(index) {\n            page.url = url.clone();\n            page.title = title.clone();\n        }\n\n        Ok(json!({ \"index\": index, \"url\": url, \"title\": title }))\n    }\n\n    pub async fn tab_close(&mut self, index: Option<usize>) -> Result<Value, String> {\n        let target_index = index.unwrap_or(self.active_page_index);\n\n        if target_index >= self.pages.len() {\n            return Err(format!(\"Tab index {} out of range\", target_index));\n        }\n\n        if self.pages.len() <= 1 {\n            return Err(\"Cannot close the last tab\".to_string());\n        }\n\n        let page = self.pages.remove(target_index);\n        let _ = self\n            .client\n            .send_command_typed::<_, Value>(\n                \"Target.closeTarget\",\n                &CloseTargetParams {\n                    target_id: page.target_id,\n                },\n                None,\n            )\n            .await;\n\n        if self.active_page_index >= self.pages.len() {\n            self.active_page_index = self.pages.len() - 1;\n        }\n\n        let session_id = self.pages[self.active_page_index].session_id.clone();\n        self.enable_domains(&session_id).await?;\n\n        Ok(json!({ \"closed\": target_index, \"activeIndex\": self.active_page_index }))\n    }\n\n    // -----------------------------------------------------------------------\n    // Emulation\n    // -----------------------------------------------------------------------\n\n    pub async fn set_viewport(\n        &self,\n        width: i32,\n        height: i32,\n        device_scale_factor: f64,\n        mobile: bool,\n    ) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\n                \"Emulation.setDeviceMetricsOverride\",\n                Some(json!({\n                    \"width\": width,\n                    \"height\": height,\n                    \"deviceScaleFactor\": device_scale_factor,\n                    \"mobile\": mobile,\n                })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn set_user_agent(&self, user_agent: &str) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\n                \"Emulation.setUserAgentOverride\",\n                Some(json!({ \"userAgent\": user_agent })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn set_emulated_media(\n        &self,\n        media: Option<&str>,\n        features: Option<Vec<(String, String)>>,\n    ) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        let mut params = json!({});\n        if let Some(m) = media {\n            params[\"media\"] = Value::String(m.to_string());\n        }\n        if let Some(feats) = features {\n            let features_arr: Vec<Value> = feats\n                .iter()\n                .map(|(name, value)| json!({ \"name\": name, \"value\": value }))\n                .collect();\n            params[\"features\"] = Value::Array(features_arr);\n        }\n        self.client\n            .send_command(\"Emulation.setEmulatedMedia\", Some(params), Some(session_id))\n            .await?;\n        Ok(())\n    }\n\n    pub async fn bring_to_front(&self) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\"Page.bringToFront\", None, Some(session_id))\n            .await?;\n        Ok(())\n    }\n\n    pub async fn set_timezone(&self, timezone_id: &str) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\n                \"Emulation.setTimezoneOverride\",\n                Some(json!({ \"timezoneId\": timezone_id })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn set_locale(&self, locale: &str) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\n                \"Emulation.setLocaleOverride\",\n                Some(json!({ \"locale\": locale })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn set_geolocation(\n        &self,\n        latitude: f64,\n        longitude: f64,\n        accuracy: Option<f64>,\n    ) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\n                \"Emulation.setGeolocationOverride\",\n                Some(json!({\n                    \"latitude\": latitude,\n                    \"longitude\": longitude,\n                    \"accuracy\": accuracy.unwrap_or(1.0),\n                })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn grant_permissions(&self, permissions: &[String]) -> Result<(), String> {\n        self.client\n            .send_command(\n                \"Browser.grantPermissions\",\n                Some(json!({ \"permissions\": permissions })),\n                None,\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn handle_dialog(\n        &self,\n        accept: bool,\n        prompt_text: Option<&str>,\n    ) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        let mut params = json!({ \"accept\": accept });\n        if let Some(text) = prompt_text {\n            params[\"promptText\"] = Value::String(text.to_string());\n        }\n        self.client\n            .send_command(\n                \"Page.handleJavaScriptDialog\",\n                Some(params),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n\n    pub async fn upload_files(&self, selector: &str, files: &[String]) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n\n        let node_result = self\n            .client\n            .send_command(\n                \"DOM.querySelector\",\n                Some(json!({\n                    \"nodeId\": 1,\n                    \"selector\": selector,\n                })),\n                Some(session_id),\n            )\n            .await;\n\n        // Alternative: resolve via JS\n        let result: EvaluateResult = self\n            .client\n            .send_command_typed(\n                \"Runtime.evaluate\",\n                &EvaluateParams {\n                    expression: format!(\n                        \"document.querySelector({})\",\n                        serde_json::to_string(selector).unwrap_or_default()\n                    ),\n                    return_by_value: Some(false),\n                    await_promise: Some(false),\n                },\n                Some(session_id),\n            )\n            .await?;\n\n        let object_id = result\n            .result\n            .object_id\n            .ok_or(\"File input element not found\")?;\n\n        // Get the DOM node from the remote object\n        let describe: Value = self\n            .client\n            .send_command(\n                \"DOM.describeNode\",\n                Some(json!({ \"objectId\": object_id })),\n                Some(session_id),\n            )\n            .await?;\n\n        let backend_node_id = describe\n            .get(\"node\")\n            .and_then(|n| n.get(\"backendNodeId\"))\n            .and_then(|v| v.as_i64())\n            .ok_or(\"Could not get backendNodeId for file input\")?;\n\n        // Suppress unused variable warning\n        let _ = node_result;\n\n        self.client\n            .send_command(\n                \"DOM.setFileInputFiles\",\n                Some(json!({\n                    \"files\": files,\n                    \"backendNodeId\": backend_node_id,\n                })),\n                Some(session_id),\n            )\n            .await?;\n\n        Ok(())\n    }\n\n    pub async fn add_script_to_evaluate(&self, source: &str) -> Result<String, String> {\n        let session_id = self.active_session_id()?;\n        let result = self\n            .client\n            .send_command(\n                \"Page.addScriptToEvaluateOnNewDocument\",\n                Some(json!({ \"source\": source })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(result\n            .get(\"identifier\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .to_string())\n    }\n\n    pub fn add_page(&mut self, page: PageInfo) {\n        let index = self.pages.len();\n        self.pages.push(page);\n        self.active_page_index = index;\n    }\n\n    pub fn remove_page_by_target_id(&mut self, target_id: &str) {\n        if let Some(pos) = self.pages.iter().position(|p| p.target_id == target_id) {\n            self.pages.remove(pos);\n            self.update_active_page_if_needed();\n        }\n    }\n\n    pub fn has_target(&self, target_id: &str) -> bool {\n        self.pages.iter().any(|p| p.target_id == target_id)\n    }\n\n    pub fn page_count(&self) -> usize {\n        self.pages.len()\n    }\n\n    pub fn pages_list(&self) -> Vec<PageInfo> {\n        self.pages.clone()\n    }\n\n    pub async fn set_download_behavior(&self, download_path: &str) -> Result<(), String> {\n        let session_id = self.active_session_id()?;\n        self.client\n            .send_command(\n                \"Browser.setDownloadBehavior\",\n                Some(json!({\n                    \"behavior\": \"allowAndName\",\n                    \"downloadPath\": download_path,\n                    \"eventsEnabled\": true,\n                })),\n                Some(session_id),\n            )\n            .await?;\n        Ok(())\n    }\n}\n\n/// Core network-idle polling loop, extracted so it can be unit-tested without a\n/// full `BrowserManager` / CDP connection.\n///\n/// Returns `Ok(())` once no network requests have been in-flight for at least\n/// 500 ms, or `Err` if `overall_timeout` elapses first.\nasync fn poll_network_idle(\n    session_id: &str,\n    rx: &mut broadcast::Receiver<CdpEvent>,\n    overall_timeout: tokio::time::Duration,\n) -> Result<(), String> {\n    let pending = Arc::new(Mutex::new(HashSet::<String>::new()));\n\n    tokio::time::timeout(overall_timeout, async {\n        let mut idle_start: Option<tokio::time::Instant> = None;\n\n        loop {\n            let recv_result =\n                tokio::time::timeout(tokio::time::Duration::from_millis(600), rx.recv()).await;\n\n            match recv_result {\n                Ok(Ok(event)) if event.session_id.as_deref() == Some(session_id) => {\n                    let mut p = pending.lock().await;\n                    match event.method.as_str() {\n                        \"Network.requestWillBeSent\" => {\n                            if let Some(id) = event.params.get(\"requestId\").and_then(|v| v.as_str())\n                            {\n                                p.insert(id.to_string());\n                                idle_start = None;\n                            }\n                        }\n                        \"Network.loadingFinished\" | \"Network.loadingFailed\" => {\n                            if let Some(id) = event.params.get(\"requestId\").and_then(|v| v.as_str())\n                            {\n                                p.remove(id);\n                                if p.is_empty() {\n                                    idle_start = Some(tokio::time::Instant::now());\n                                }\n                            }\n                        }\n                        \"Page.loadEventFired\" => {\n                            if p.is_empty() {\n                                idle_start = Some(tokio::time::Instant::now());\n                            }\n                        }\n                        _ => {}\n                    }\n                }\n                Ok(Ok(_)) => {}\n                Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,\n                Ok(Err(_)) => break,\n                Err(_) => {\n                    // Timeout on recv -- if no pending requests, start (or\n                    // continue) the idle timer instead of returning\n                    // immediately.  This prevents false-positive idle\n                    // detection when the subscription starts after the page\n                    // has already loaded (e.g. cached pages).\n                    let p = pending.lock().await;\n                    if p.is_empty() && idle_start.is_none() {\n                        idle_start = Some(tokio::time::Instant::now());\n                    }\n                }\n            }\n\n            if let Some(start) = idle_start {\n                if start.elapsed() >= tokio::time::Duration::from_millis(500) {\n                    return Ok(());\n                }\n            }\n        }\n\n        Ok(())\n    })\n    .await\n    .map_err(|_| \"Timeout waiting for networkidle\".to_string())?\n}\n\nasync fn connect_cdp_with_retry(\n    ws_url: &str,\n    total_timeout: Duration,\n    poll_interval: Duration,\n) -> Result<CdpClient, String> {\n    let deadline = Instant::now() + total_timeout;\n\n    loop {\n        match CdpClient::connect(ws_url).await {\n            Ok(client) => return Ok(client),\n            Err(err) => {\n                if Instant::now() >= deadline {\n                    return Err(err);\n                }\n            }\n        }\n\n        tokio::time::sleep(poll_interval).await;\n    }\n}\n\nasync fn initialize_lightpanda_manager(\n    ws_url: String,\n    process: BrowserProcess,\n) -> Result<BrowserManager, String> {\n    let deadline = Instant::now() + LIGHTPANDA_TARGET_INIT_TIMEOUT;\n    let mut process = Some(process);\n\n    loop {\n        let client = match connect_cdp_with_retry(\n            &ws_url,\n            LIGHTPANDA_CDP_CONNECT_TIMEOUT,\n            LIGHTPANDA_CDP_CONNECT_POLL_INTERVAL,\n        )\n        .await\n        {\n            Ok(client) => client,\n            Err(err) => {\n                if Instant::now() >= deadline {\n                    return Err(lightpanda_target_init_timeout(Some(&err)));\n                }\n                tokio::time::sleep(LIGHTPANDA_CDP_CONNECT_POLL_INTERVAL).await;\n                continue;\n            }\n        };\n\n        let mut manager = BrowserManager {\n            client: Arc::new(client),\n            browser_process: None,\n            ws_url: ws_url.clone(),\n            pages: Vec::new(),\n            active_page_index: 0,\n            default_timeout_ms: 25_000,\n        };\n\n        match discover_and_attach_lightpanda_targets(&mut manager, deadline).await {\n            Ok(()) => {\n                manager.browser_process = process.take();\n                return Ok(manager);\n            }\n            Err(err) => {\n                if Instant::now() >= deadline {\n                    return Err(lightpanda_target_init_timeout(Some(&err)));\n                }\n                tokio::time::sleep(LIGHTPANDA_CDP_CONNECT_POLL_INTERVAL).await;\n            }\n        }\n    }\n}\n\nasync fn discover_and_attach_lightpanda_targets(\n    manager: &mut BrowserManager,\n    deadline: Instant,\n) -> Result<(), String> {\n    run_with_lightpanda_deadline(\n        deadline,\n        manager.discover_and_attach_targets(),\n        \"Target domain initialization attempt exceeded the remaining startup deadline\",\n    )\n    .await\n}\n\nfn remaining_until(deadline: Instant) -> Option<Duration> {\n    deadline.checked_duration_since(Instant::now())\n}\n\nasync fn run_with_lightpanda_deadline<F, T>(\n    deadline: Instant,\n    operation: F,\n    timeout_context: &'static str,\n) -> Result<T, String>\nwhere\n    F: Future<Output = Result<T, String>>,\n{\n    let remaining = remaining_until(deadline)\n        .ok_or_else(|| lightpanda_target_init_timeout(Some(\"deadline expired before retry\")))?;\n\n    match tokio::time::timeout(remaining, operation).await {\n        Ok(result) => result,\n        Err(_) => Err(lightpanda_target_init_timeout(Some(timeout_context))),\n    }\n}\n\nfn lightpanda_target_init_timeout(last_error: Option<&str>) -> String {\n    let mut message = format!(\n        \"Timed out after {}ms waiting for Lightpanda Target domain to initialize\",\n        LIGHTPANDA_TARGET_INIT_TIMEOUT.as_millis(),\n    );\n    if let Some(last_error) = last_error {\n        message.push_str(&format!(\"\\nLast error: {}\", last_error));\n    }\n    message\n}\n\nasync fn resolve_cdp_url(input: &str) -> Result<String, String> {\n    if input.starts_with(\"ws://\") || input.starts_with(\"wss://\") {\n        return Ok(input.to_string());\n    }\n\n    if input.starts_with(\"http://\") || input.starts_with(\"https://\") {\n        let parsed = url::Url::parse(input).map_err(|e| format!(\"Invalid CDP URL: {}\", e))?;\n        let host = parsed\n            .host_str()\n            .ok_or_else(|| format!(\"No host in CDP URL: {}\", input))?;\n        let port = parsed.port().unwrap_or(9222);\n        return discover_cdp_url(host, port).await;\n    }\n\n    // Try as numeric port\n    if let Ok(port) = input.parse::<u16>() {\n        return discover_cdp_url(\"127.0.0.1\", port).await;\n    }\n\n    Err(format!(\n        \"Invalid CDP target: {}. Use ws://, http://, or a port number.\",\n        input\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tokio::time::sleep;\n\n    #[test]\n    fn test_validate_launch_options_extensions_and_cdp() {\n        let ext = vec![\"/path/to/ext\".to_string()];\n        assert!(validate_launch_options(Some(&ext), true, None, None, false, None,).is_err());\n    }\n\n    #[test]\n    fn test_validate_launch_options_profile_and_cdp() {\n        assert!(validate_launch_options(None, true, Some(\"/path\"), None, false, None,).is_err());\n    }\n\n    #[test]\n    fn test_validate_launch_options_storage_state_and_profile() {\n        assert!(validate_launch_options(\n            None,\n            false,\n            Some(\"/profile\"),\n            Some(\"/state.json\"),\n            false,\n            None,\n        )\n        .is_err());\n    }\n\n    #[test]\n    fn test_validate_launch_options_storage_state_and_extensions() {\n        let ext = vec![\"/ext\".to_string()];\n        assert!(\n            validate_launch_options(Some(&ext), false, None, Some(\"/state.json\"), false, None,)\n                .is_err()\n        );\n    }\n\n    #[test]\n    fn test_validate_launch_options_allow_file_access_firefox() {\n        assert!(\n            validate_launch_options(None, false, None, None, true, Some(\"/usr/bin/firefox\"),)\n                .is_err()\n        );\n    }\n\n    #[test]\n    fn test_validate_launch_options_valid() {\n        assert!(validate_launch_options(None, false, None, None, false, None,).is_ok());\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_strict_mode() {\n        assert_eq!(\n            to_ai_friendly_error(\"Strict mode violation: multiple elements\"),\n            \"Element matched multiple results. Use a more specific selector.\"\n        );\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_not_visible() {\n        assert_eq!(\n            to_ai_friendly_error(\"element is not visible\"),\n            \"Element exists but is not visible. Wait for it to become visible or scroll it into view.\"\n        );\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_intercept() {\n        assert_eq!(\n            to_ai_friendly_error(\"element intercepted by another element\"),\n            \"Another element is covering the target element. Try scrolling or closing overlays.\"\n        );\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_timeout() {\n        assert_eq!(\n            to_ai_friendly_error(\"Timeout waiting for element\"),\n            \"Operation timed out. The page may still be loading or the element may not exist.\"\n        );\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_not_found() {\n        assert_eq!(\n            to_ai_friendly_error(\"Element not found\"),\n            \"Element not found. Verify the selector is correct and the element exists in the DOM.\"\n        );\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_unknown() {\n        let msg = \"Some custom error message\";\n        assert_eq!(to_ai_friendly_error(msg), msg);\n    }\n\n    /// Errors containing \"not found\" but NOT \"element\" should pass through unchanged.\n    #[test]\n    fn test_to_ai_friendly_error_ignores_non_element_not_found() {\n        let err = \"Chrome not found. Install Chrome or use --executable-path.\";\n        assert_eq!(to_ai_friendly_error(err), err);\n    }\n\n    #[test]\n    fn test_to_ai_friendly_error_catches_no_element() {\n        let mapped =\n            \"Element not found. Verify the selector is correct and the element exists in the DOM.\";\n        assert_eq!(to_ai_friendly_error(\"No element found for css 'x'\"), mapped);\n    }\n\n    #[test]\n    fn test_remaining_until_returns_none_for_past_deadline() {\n        let deadline = Instant::now()\n            .checked_sub(Duration::from_millis(1))\n            .expect(\"past instant should be representable\");\n        assert!(remaining_until(deadline).is_none());\n    }\n\n    #[tokio::test]\n    async fn test_run_with_lightpanda_deadline_enforces_timeout() {\n        let deadline = Instant::now() + Duration::from_millis(25);\n        let err = tokio::time::timeout(\n            Duration::from_secs(1),\n            run_with_lightpanda_deadline(\n                deadline,\n                async {\n                    sleep(Duration::from_millis(100)).await;\n                    Ok::<(), String>(())\n                },\n                \"Target domain initialization attempt exceeded the remaining startup deadline\",\n            ),\n        )\n        .await\n        .expect(\"outer timeout should not fire\")\n        .unwrap_err();\n\n        assert!(err.contains(\n            \"Timed out after 10000ms waiting for Lightpanda Target domain to initialize\"\n        ));\n        assert!(err.contains(\"remaining startup deadline\"));\n    }\n\n    #[tokio::test]\n    async fn test_run_with_lightpanda_deadline_returns_operation_error() {\n        let deadline = Instant::now() + Duration::from_secs(1);\n        let err = run_with_lightpanda_deadline(\n            deadline,\n            async { Err::<(), String>(\"Target.getTargets failed\".to_string()) },\n            \"unused timeout context\",\n        )\n        .await\n        .unwrap_err();\n\n        assert_eq!(err, \"Target.getTargets failed\");\n    }\n\n    #[test]\n    fn test_lightpanda_target_init_timeout_includes_last_error() {\n        let err = lightpanda_target_init_timeout(Some(\"Target.setDiscoverTargets failed\"));\n        assert!(err.contains(\n            \"Timed out after 10000ms waiting for Lightpanda Target domain to initialize\"\n        ));\n        assert!(err.contains(\"Target.setDiscoverTargets failed\"));\n    }\n\n    #[test]\n    fn test_is_internal_chrome_target() {\n        assert!(is_internal_chrome_target(\"chrome://newtab/\"));\n        assert!(is_internal_chrome_target(\n            \"chrome://omnibox-popup.top-chrome/\"\n        ));\n        assert!(is_internal_chrome_target(\n            \"chrome-extension://abc123/popup.html\"\n        ));\n        assert!(is_internal_chrome_target(\n            \"devtools://devtools/bundled/inspector.html\"\n        ));\n        assert!(!is_internal_chrome_target(\"https://example.com\"));\n        assert!(!is_internal_chrome_target(\"http://localhost:3000\"));\n        assert!(!is_internal_chrome_target(\"about:blank\"));\n    }\n\n    // -----------------------------------------------------------------------\n    // poll_network_idle tests\n    // -----------------------------------------------------------------------\n\n    fn cdp_event(method: &str, session_id: &str, params: Value) -> CdpEvent {\n        CdpEvent {\n            method: method.to_string(),\n            params,\n            session_id: Some(session_id.to_string()),\n        }\n    }\n\n    /// Regression test for #846: when no network events arrive at all (e.g.\n    /// page fully served from cache), poll_network_idle must NOT return\n    /// instantly.  It should observe at least 500 ms of idle before resolving.\n    #[tokio::test]\n    async fn test_network_idle_no_events_does_not_return_instantly() {\n        let (tx, mut rx) = broadcast::channel::<CdpEvent>(16);\n        let session = \"s1\";\n\n        let start = tokio::time::Instant::now();\n        let result = tokio::time::timeout(\n            Duration::from_secs(5),\n            poll_network_idle(session, &mut rx, Duration::from_secs(5)),\n        )\n        .await\n        .expect(\"outer timeout should not fire\");\n\n        assert!(result.is_ok());\n        let elapsed = start.elapsed();\n        assert!(\n            elapsed >= Duration::from_millis(500),\n            \"network idle returned in {:?}, expected >= 500ms\",\n            elapsed\n        );\n\n        drop(tx);\n    }\n\n    /// Normal flow: requests start and finish, idle is detected after the last\n    /// request completes and 500 ms of silence passes.\n    #[tokio::test]\n    async fn test_network_idle_after_requests_complete() {\n        let (tx, mut rx) = broadcast::channel::<CdpEvent>(16);\n        let session = \"s1\";\n\n        let _keep_alive = tx.clone();\n        tokio::spawn(async move {\n            sleep(Duration::from_millis(50)).await;\n            let _ = tx.send(cdp_event(\n                \"Network.requestWillBeSent\",\n                session,\n                json!({ \"requestId\": \"r1\" }),\n            ));\n            sleep(Duration::from_millis(100)).await;\n            let _ = tx.send(cdp_event(\n                \"Network.loadingFinished\",\n                session,\n                json!({ \"requestId\": \"r1\" }),\n            ));\n        });\n\n        let start = tokio::time::Instant::now();\n        let result = tokio::time::timeout(\n            Duration::from_secs(5),\n            poll_network_idle(session, &mut rx, Duration::from_secs(5)),\n        )\n        .await\n        .expect(\"outer timeout should not fire\");\n\n        assert!(result.is_ok());\n        let elapsed = start.elapsed();\n        assert!(\n            elapsed >= Duration::from_millis(500),\n            \"should wait >= 500ms after last request finishes, got {:?}\",\n            elapsed\n        );\n    }\n\n    /// A new request arriving during the idle window resets the timer.\n    #[tokio::test]\n    async fn test_network_idle_resets_on_new_request() {\n        let (tx, mut rx) = broadcast::channel::<CdpEvent>(16);\n        let session = \"s1\";\n\n        let _keep_alive = tx.clone();\n        tokio::spawn(async move {\n            sleep(Duration::from_millis(50)).await;\n            let _ = tx.send(cdp_event(\n                \"Network.requestWillBeSent\",\n                session,\n                json!({ \"requestId\": \"r1\" }),\n            ));\n            sleep(Duration::from_millis(50)).await;\n            let _ = tx.send(cdp_event(\n                \"Network.loadingFinished\",\n                session,\n                json!({ \"requestId\": \"r1\" }),\n            ));\n            // Wait 200ms (< 500ms idle window), then fire another request\n            sleep(Duration::from_millis(200)).await;\n            let _ = tx.send(cdp_event(\n                \"Network.requestWillBeSent\",\n                session,\n                json!({ \"requestId\": \"r2\" }),\n            ));\n            sleep(Duration::from_millis(100)).await;\n            let _ = tx.send(cdp_event(\n                \"Network.loadingFinished\",\n                session,\n                json!({ \"requestId\": \"r2\" }),\n            ));\n        });\n\n        let start = tokio::time::Instant::now();\n        let result = tokio::time::timeout(\n            Duration::from_secs(5),\n            poll_network_idle(session, &mut rx, Duration::from_secs(5)),\n        )\n        .await\n        .expect(\"outer timeout should not fire\");\n\n        assert!(result.is_ok());\n        let elapsed = start.elapsed();\n        // r2 finishes at ~400ms; idle should be detected at ~900ms\n        assert!(\n            elapsed >= Duration::from_millis(800),\n            \"should wait for idle after second request, got {:?}\",\n            elapsed\n        );\n    }\n\n    /// When the overall timeout expires before idle is reached, the function\n    /// returns an error.\n    #[tokio::test]\n    async fn test_network_idle_overall_timeout() {\n        let (tx, mut rx) = broadcast::channel::<CdpEvent>(16);\n        let session = \"s1\";\n\n        // Keep sending requests so idle is never reached\n        tokio::spawn(async move {\n            for i in 0u64.. {\n                let _ = tx.send(cdp_event(\n                    \"Network.requestWillBeSent\",\n                    session,\n                    json!({ \"requestId\": format!(\"r{}\", i) }),\n                ));\n                sleep(Duration::from_millis(100)).await;\n            }\n        });\n\n        let result = poll_network_idle(session, &mut rx, Duration::from_millis(800)).await;\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .contains(\"Timeout waiting for networkidle\"));\n    }\n}\n"
  },
  {
    "path": "cli/src/native/cdp/chrome.rs",
    "content": "use std::io::{BufRead, BufReader, Write};\nuse std::path::{Path, PathBuf};\nuse std::process::{Child, Command, Stdio};\nuse std::time::Duration;\n\nuse super::discovery::discover_cdp_url;\n\npub struct ChromeProcess {\n    child: Child,\n    pub ws_url: String,\n    temp_user_data_dir: Option<PathBuf>,\n}\n\nimpl ChromeProcess {\n    pub fn kill(&mut self) {\n        let _ = self.child.kill();\n        let _ = self.child.wait();\n    }\n\n    /// Wait for Chrome to exit on its own (after Browser.close CDP command),\n    /// falling back to kill() if it doesn't exit within the timeout.\n    /// This allows Chrome to flush cookies and other state to the user-data-dir.\n    pub fn wait_or_kill(&mut self, timeout: Duration) {\n        let start = std::time::Instant::now();\n        let poll_interval = Duration::from_millis(50);\n\n        while start.elapsed() < timeout {\n            match self.child.try_wait() {\n                Ok(Some(_)) => return,\n                Ok(None) => std::thread::sleep(poll_interval),\n                Err(_) => break,\n            }\n        }\n\n        self.kill();\n    }\n}\n\nimpl Drop for ChromeProcess {\n    fn drop(&mut self) {\n        self.kill();\n        if let Some(ref dir) = self.temp_user_data_dir {\n            for attempt in 0..3 {\n                match std::fs::remove_dir_all(dir) {\n                    Ok(()) => break,\n                    Err(_) if attempt < 2 => {\n                        std::thread::sleep(Duration::from_millis(100));\n                    }\n                    Err(e) => {\n                        // Use write! instead of eprintln! to avoid panicking\n                        // if the daemon's stderr pipe is broken (parent dropped it).\n                        let _ = writeln!(\n                            std::io::stderr(),\n                            \"Warning: failed to clean up temp profile {}: {}\",\n                            dir.display(),\n                            e\n                        );\n                    }\n                }\n            }\n        }\n    }\n}\n\npub struct LaunchOptions {\n    pub headless: bool,\n    pub executable_path: Option<String>,\n    pub proxy: Option<String>,\n    pub proxy_bypass: Option<String>,\n    pub profile: Option<String>,\n    pub args: Vec<String>,\n    pub allow_file_access: bool,\n    pub extensions: Option<Vec<String>>,\n    pub storage_state: Option<String>,\n    pub user_agent: Option<String>,\n    pub ignore_https_errors: bool,\n    pub color_scheme: Option<String>,\n    pub download_path: Option<String>,\n}\n\nimpl Default for LaunchOptions {\n    fn default() -> Self {\n        Self {\n            headless: true,\n            executable_path: None,\n            proxy: None,\n            proxy_bypass: None,\n            profile: None,\n            args: Vec::new(),\n            allow_file_access: false,\n            extensions: None,\n            storage_state: None,\n            user_agent: None,\n            ignore_https_errors: false,\n            color_scheme: None,\n            download_path: None,\n        }\n    }\n}\n\nstruct ChromeArgs {\n    args: Vec<String>,\n    user_data_dir: PathBuf,\n    temp_user_data_dir: Option<PathBuf>,\n}\n\nfn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {\n    let mut args = vec![\n        \"--remote-debugging-port=0\".to_string(),\n        \"--no-first-run\".to_string(),\n        \"--no-default-browser-check\".to_string(),\n        \"--disable-background-networking\".to_string(),\n        \"--disable-backgrounding-occluded-windows\".to_string(),\n        \"--disable-component-update\".to_string(),\n        \"--disable-default-apps\".to_string(),\n        \"--disable-hang-monitor\".to_string(),\n        \"--disable-popup-blocking\".to_string(),\n        \"--disable-prompt-on-repost\".to_string(),\n        \"--disable-sync\".to_string(),\n        \"--disable-features=Translate\".to_string(),\n        \"--enable-features=NetworkService,NetworkServiceInProcess\".to_string(),\n        \"--metrics-recording-only\".to_string(),\n        \"--password-store=basic\".to_string(),\n        \"--use-mock-keychain\".to_string(),\n    ];\n\n    let has_extensions = options\n        .extensions\n        .as_ref()\n        .is_some_and(|exts| !exts.is_empty());\n\n    // Extensions require headed mode in native Chrome (content scripts are not\n    // injected in headless mode).  Skip --headless when extensions are loaded.\n    if options.headless && !has_extensions {\n        args.push(\"--headless=new\".to_string());\n        // Enable SwiftShader software rendering in headless mode.  This\n        // prevents silent crashes in environments where GPU drivers are\n        // missing or restricted (VMs, containers, some cloud machines)\n        // while preserving WebGL support.  Playwright uses the same flag.\n        args.push(\"--enable-unsafe-swiftshader\".to_string());\n    }\n\n    if let Some(ref proxy) = options.proxy {\n        args.push(format!(\"--proxy-server={}\", proxy));\n    }\n\n    if let Some(ref bypass) = options.proxy_bypass {\n        args.push(format!(\"--proxy-bypass-list={}\", bypass));\n    }\n\n    let (user_data_dir, temp_user_data_dir) = if let Some(ref profile) = options.profile {\n        let expanded = expand_tilde(profile);\n        let dir = PathBuf::from(&expanded);\n        args.push(format!(\"--user-data-dir={}\", expanded));\n        (dir, None)\n    } else {\n        let dir =\n            std::env::temp_dir().join(format!(\"agent-browser-chrome-{}\", uuid::Uuid::new_v4()));\n        std::fs::create_dir_all(&dir)\n            .map_err(|e| format!(\"Failed to create temp profile dir: {}\", e))?;\n        args.push(format!(\"--user-data-dir={}\", dir.display()));\n        (dir.clone(), Some(dir))\n    };\n\n    if options.allow_file_access {\n        args.push(\"--allow-file-access-from-files\".to_string());\n        args.push(\"--allow-file-access\".to_string());\n    }\n\n    if let Some(ref exts) = options.extensions {\n        if !exts.is_empty() {\n            let ext_list = exts.join(\",\");\n            args.push(format!(\"--load-extension={}\", ext_list));\n            args.push(format!(\"--disable-extensions-except={}\", ext_list));\n        }\n    }\n\n    let has_window_size = options\n        .args\n        .iter()\n        .any(|a| a.starts_with(\"--start-maximized\") || a.starts_with(\"--window-size=\"));\n\n    if !has_window_size && options.headless && !has_extensions {\n        args.push(\"--window-size=1280,720\".to_string());\n    }\n\n    args.extend(options.args.iter().cloned());\n\n    if should_disable_sandbox(&args) {\n        args.push(\"--no-sandbox\".to_string());\n    }\n\n    if should_disable_dev_shm(&args) {\n        args.push(\"--disable-dev-shm-usage\".to_string());\n    }\n\n    Ok(ChromeArgs {\n        args,\n        user_data_dir,\n        temp_user_data_dir,\n    })\n}\n\npub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {\n    let chrome_path = match &options.executable_path {\n        Some(p) => PathBuf::from(p),\n        None => {\n            find_chrome().ok_or(\"Chrome not found. Run `agent-browser install` to download Chrome, or use --executable-path.\")?\n        }\n    };\n\n    let max_attempts = 3;\n    let mut last_err = String::new();\n\n    for attempt in 1..=max_attempts {\n        match try_launch_chrome(&chrome_path, options) {\n            Ok(process) => return Ok(process),\n            Err(e) => {\n                last_err = e;\n                if attempt < max_attempts {\n                    // Use write! instead of eprintln! to avoid panicking\n                    // if the daemon's stderr pipe is broken (parent dropped it).\n                    let _ = writeln!(\n                        std::io::stderr(),\n                        \"[chrome] Launch attempt {}/{} failed, retrying in 500ms...\",\n                        attempt,\n                        max_attempts\n                    );\n                    std::thread::sleep(Duration::from_millis(500));\n                }\n            }\n        }\n    }\n\n    Err(last_err)\n}\n\nfn try_launch_chrome(chrome_path: &Path, options: &LaunchOptions) -> Result<ChromeProcess, String> {\n    let ChromeArgs {\n        args,\n        user_data_dir,\n        temp_user_data_dir,\n    } = build_chrome_args(options)?;\n\n    // Mitigate stale DevToolsActivePort risk (e.g., previous crash left it behind).\n    // Puppeteer does similar cleanup before spawning.\n    let _ = std::fs::remove_file(user_data_dir.join(\"DevToolsActivePort\"));\n\n    let cleanup_temp_dir = |dir: &Option<PathBuf>| {\n        if let Some(ref d) = dir {\n            let _ = std::fs::remove_dir_all(d);\n        }\n    };\n\n    let mut child = Command::new(chrome_path)\n        .args(&args)\n        .stdin(Stdio::null())\n        .stdout(Stdio::null())\n        .stderr(Stdio::piped())\n        .spawn()\n        .map_err(|e| {\n            cleanup_temp_dir(&temp_user_data_dir);\n            format!(\"Failed to launch Chrome at {:?}: {}\", chrome_path, e)\n        })?;\n\n    // Shared overall deadline so we don't double-wait (poll + stderr fallback).\n    let deadline = std::time::Instant::now() + Duration::from_secs(30);\n\n    // Primary path: use DevToolsActivePort written into user-data-dir.\n    // This is more reliable on Windows than scraping stderr for \"DevTools listening on ...\",\n    // which can be missing/empty depending on how Chrome is launched.\n    let ws_url = match wait_for_devtools_active_port(&mut child, &user_data_dir, deadline) {\n        Ok(url) => url,\n        Err(primary_err) => {\n            // Fallback: scrape stderr (legacy behavior) for better diagnostics.\n            let stderr = child.stderr.take().ok_or_else(|| {\n                let _ = child.kill();\n                cleanup_temp_dir(&temp_user_data_dir);\n                \"Failed to capture Chrome stderr\".to_string()\n            })?;\n            let reader = BufReader::new(stderr);\n            match wait_for_ws_url_until(reader, deadline) {\n                Ok(url) => url,\n                Err(fallback_err) => {\n                    let _ = child.kill();\n                    cleanup_temp_dir(&temp_user_data_dir);\n                    return Err(format!(\n                        \"{}\\n(also tried parsing stderr) {}\",\n                        primary_err, fallback_err\n                    ));\n                }\n            }\n        }\n    };\n\n    Ok(ChromeProcess {\n        child,\n        ws_url,\n        temp_user_data_dir,\n    })\n}\n\nfn wait_for_devtools_active_port(\n    child: &mut Child,\n    user_data_dir: &Path,\n    deadline: std::time::Instant,\n) -> Result<String, String> {\n    let poll_interval = Duration::from_millis(50);\n\n    while std::time::Instant::now() <= deadline {\n        if let Ok(Some(status)) = child.try_wait() {\n            // Chrome exited before writing DevToolsActivePort -- report the\n            // exit code so the caller can surface it alongside stderr output.\n            let code = status\n                .code()\n                .map(|c| format!(\"{}\", c))\n                .unwrap_or_else(|| \"unknown\".to_string());\n            return Err(format!(\n                \"Chrome exited early (exit code: {}) without writing DevToolsActivePort\",\n                code\n            ));\n        }\n\n        if let Some((port, ws_path)) = read_devtools_active_port(user_data_dir) {\n            let ws_url = format!(\"ws://127.0.0.1:{}{}\", port, ws_path);\n            return Ok(ws_url);\n        }\n\n        std::thread::sleep(poll_interval);\n    }\n\n    Err(\"Timeout waiting for DevToolsActivePort\".to_string())\n}\n\nfn wait_for_ws_url_until(\n    reader: BufReader<std::process::ChildStderr>,\n    deadline: std::time::Instant,\n) -> Result<String, String> {\n    let prefix = \"DevTools listening on \";\n    let mut stderr_lines: Vec<String> = Vec::new();\n\n    for line in reader.lines() {\n        if std::time::Instant::now() > deadline {\n            return Err(chrome_launch_error(\n                \"Timeout waiting for Chrome DevTools URL\",\n                &stderr_lines,\n            ));\n        }\n        let line = line.map_err(|e| format!(\"Failed to read Chrome stderr: {}\", e))?;\n        if let Some(url) = line.strip_prefix(prefix) {\n            return Ok(url.trim().to_string());\n        }\n        stderr_lines.push(line);\n    }\n\n    Err(chrome_launch_error(\n        \"Chrome exited before providing DevTools URL\",\n        &stderr_lines,\n    ))\n}\n\nfn chrome_launch_error(message: &str, stderr_lines: &[String]) -> String {\n    let relevant: Vec<&String> = stderr_lines\n        .iter()\n        .filter(|l| {\n            let lower = l.to_lowercase();\n            lower.contains(\"error\")\n                || lower.contains(\"fatal\")\n                || lower.contains(\"sandbox\")\n                || lower.contains(\"namespace\")\n                || lower.contains(\"permission\")\n                || lower.contains(\"cannot\")\n                || lower.contains(\"failed\")\n                || lower.contains(\"abort\")\n        })\n        .collect();\n\n    if relevant.is_empty() {\n        if stderr_lines.is_empty() {\n            return format!(\n                \"{} (no stderr output from Chrome)\\nHint: try passing --args \\\"--no-sandbox\\\" if Chrome crashes silently in your environment\",\n                message\n            );\n        }\n        let last_lines: Vec<&String> = stderr_lines.iter().rev().take(5).collect();\n        return format!(\n            \"{}\\nChrome stderr (last {} lines):\\n  {}\",\n            message,\n            last_lines.len(),\n            last_lines\n                .into_iter()\n                .rev()\n                .map(|s| s.as_str())\n                .collect::<Vec<_>>()\n                .join(\"\\n  \")\n        );\n    }\n\n    let hint = if relevant.iter().any(|l| {\n        let lower = l.to_lowercase();\n        lower.contains(\"sandbox\") || lower.contains(\"namespace\")\n    }) {\n        \"\\nHint: try --args \\\"--no-sandbox\\\" (required in containers, VMs, and some Linux setups)\"\n    } else {\n        \"\"\n    };\n\n    format!(\n        \"{}\\nChrome stderr:\\n  {}{}\",\n        message,\n        relevant\n            .iter()\n            .map(|s| s.as_str())\n            .collect::<Vec<_>>()\n            .join(\"\\n  \"),\n        hint\n    )\n}\n\npub fn find_chrome() -> Option<PathBuf> {\n    // 1. Check Chrome downloaded by `agent-browser install`\n    if let Some(p) = crate::install::find_installed_chrome() {\n        return Some(p);\n    }\n\n    // 2. Check system-installed Chrome\n    #[cfg(target_os = \"macos\")]\n    {\n        let candidates = [\n            \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n            \"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary\",\n            \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n            \"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\",\n        ];\n        for c in &candidates {\n            let p = PathBuf::from(c);\n            if p.exists() {\n                return Some(p);\n            }\n        }\n    }\n\n    #[cfg(target_os = \"linux\")]\n    {\n        let candidates = [\n            \"google-chrome\",\n            \"google-chrome-stable\",\n            \"chromium-browser\",\n            \"chromium\",\n            \"brave-browser\",\n            \"brave-browser-stable\",\n        ];\n        for name in &candidates {\n            if let Ok(output) = Command::new(\"which\").arg(name).output() {\n                if output.status.success() {\n                    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();\n                    if !path.is_empty() {\n                        return Some(PathBuf::from(path));\n                    }\n                }\n            }\n        }\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        let candidates = [\n            r\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\",\n            r\"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe\",\n        ];\n        if let Ok(local) = std::env::var(\"LOCALAPPDATA\") {\n            let chrome = PathBuf::from(&local).join(r\"Google\\Chrome\\Application\\chrome.exe\");\n            if chrome.exists() {\n                return Some(chrome);\n            }\n            let brave =\n                PathBuf::from(&local).join(r\"BraveSoftware\\Brave-Browser\\Application\\brave.exe\");\n            if brave.exists() {\n                return Some(brave);\n            }\n        }\n        for c in &candidates {\n            let p = PathBuf::from(c);\n            if p.exists() {\n                return Some(p);\n            }\n        }\n    }\n\n    // 3. Fallback: check Playwright's browser cache (for existing installs)\n    if let Some(p) = find_playwright_chromium() {\n        return Some(p);\n    }\n\n    None\n}\n\npub fn read_devtools_active_port(user_data_dir: &Path) -> Option<(u16, String)> {\n    let path = user_data_dir.join(\"DevToolsActivePort\");\n    let content = std::fs::read_to_string(&path).ok()?;\n    let mut lines = content.lines();\n    let port: u16 = lines.next()?.trim().parse().ok()?;\n    let ws_path = lines\n        .next()\n        .unwrap_or(\"/devtools/browser\")\n        .trim()\n        .to_string();\n    Some((port, ws_path))\n}\n\npub async fn auto_connect_cdp() -> Result<String, String> {\n    let user_data_dirs = get_chrome_user_data_dirs();\n\n    for dir in &user_data_dirs {\n        if let Some((port, ws_path)) = read_devtools_active_port(dir) {\n            // Try HTTP endpoint first (pre-M144)\n            if let Ok(ws_url) = discover_cdp_url(\"127.0.0.1\", port).await {\n                return Ok(ws_url);\n            }\n            // M144+: direct WebSocket — verify the port is actually listening\n            // before returning, otherwise a stale DevToolsActivePort file\n            // (left behind after Chrome exits/crashes) produces a confusing\n            // \"connection refused\" error instead of falling through.\n            if is_port_reachable(port) {\n                let ws_url = format!(\"ws://127.0.0.1:{}{}\", port, ws_path);\n                return Ok(ws_url);\n            }\n            // Port is dead — remove the stale file so future runs skip it.\n            let stale = dir.join(\"DevToolsActivePort\");\n            let _ = std::fs::remove_file(&stale);\n        }\n    }\n\n    // Fallback: probe common ports\n    for port in [9222u16, 9229] {\n        if let Ok(ws_url) = discover_cdp_url(\"127.0.0.1\", port).await {\n            return Ok(ws_url);\n        }\n    }\n\n    Err(\"No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --cdp.\".to_string())\n}\n\nfn is_port_reachable(port: u16) -> bool {\n    use std::net::TcpStream;\n    let addr = format!(\"127.0.0.1:{}\", port);\n    TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(500)).is_ok()\n}\n\nfn get_chrome_user_data_dirs() -> Vec<PathBuf> {\n    let mut dirs = Vec::new();\n\n    #[cfg(target_os = \"macos\")]\n    {\n        if let Some(home) = dirs::home_dir() {\n            let base = home.join(\"Library/Application Support\");\n            for name in [\n                \"Google/Chrome\",\n                \"Google/Chrome Canary\",\n                \"Chromium\",\n                \"BraveSoftware/Brave-Browser\",\n            ] {\n                dirs.push(base.join(name));\n            }\n        }\n    }\n\n    #[cfg(target_os = \"linux\")]\n    {\n        if let Some(home) = dirs::home_dir() {\n            let config = home.join(\".config\");\n            for name in [\n                \"google-chrome\",\n                \"google-chrome-unstable\",\n                \"chromium\",\n                \"BraveSoftware/Brave-Browser\",\n            ] {\n                dirs.push(config.join(name));\n            }\n        }\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        if let Ok(local) = std::env::var(\"LOCALAPPDATA\") {\n            let base = PathBuf::from(local);\n            for name in [\n                r\"Google\\Chrome\\User Data\",\n                r\"Google\\Chrome SxS\\User Data\",\n                r\"Chromium\\User Data\",\n                r\"BraveSoftware\\Brave-Browser\\User Data\",\n            ] {\n                dirs.push(base.join(name));\n            }\n        }\n    }\n\n    dirs\n}\n\n/// Returns true if Chrome's sandbox should be disabled because the environment\n/// doesn't support it (containers, VMs, CI runners, running as root).\nfn should_disable_sandbox(existing_args: &[String]) -> bool {\n    if existing_args.iter().any(|a| a == \"--no-sandbox\") {\n        return false; // already set by user\n    }\n\n    // CI environments (GitHub Actions, GitLab CI, etc.) often lack user namespace\n    // support due to AppArmor or kernel restrictions.\n    if std::env::var(\"CI\").is_ok() {\n        return true;\n    }\n\n    #[cfg(unix)]\n    {\n        // Root user -- standard container default, Chrome sandbox requires non-root\n        if unsafe { libc::geteuid() } == 0 {\n            return true;\n        }\n\n        // Docker container\n        if Path::new(\"/.dockerenv\").exists() {\n            return true;\n        }\n\n        // Podman container\n        if Path::new(\"/run/.containerenv\").exists() {\n            return true;\n        }\n\n        // Generic container detection: cgroup contains docker/kubepods/lxc\n        if let Ok(cgroup) = std::fs::read_to_string(\"/proc/1/cgroup\") {\n            if cgroup.contains(\"docker\") || cgroup.contains(\"kubepods\") || cgroup.contains(\"lxc\") {\n                return true;\n            }\n        }\n    }\n\n    false\n}\n\n/// Returns true if Chrome should use disk instead of /dev/shm for shared memory.\n/// On CI runners and containers, /dev/shm is often too small (64MB default),\n/// which causes Chrome to crash mid-session.\nfn should_disable_dev_shm(existing_args: &[String]) -> bool {\n    if existing_args.iter().any(|a| a == \"--disable-dev-shm-usage\") {\n        return false;\n    }\n\n    if std::env::var(\"CI\").is_ok() {\n        return true;\n    }\n\n    #[cfg(unix)]\n    {\n        if unsafe { libc::geteuid() } == 0 {\n            return true;\n        }\n        if Path::new(\"/.dockerenv\").exists() || Path::new(\"/run/.containerenv\").exists() {\n            return true;\n        }\n        if let Ok(cgroup) = std::fs::read_to_string(\"/proc/1/cgroup\") {\n            if cgroup.contains(\"docker\") || cgroup.contains(\"kubepods\") || cgroup.contains(\"lxc\") {\n                return true;\n            }\n        }\n    }\n\n    false\n}\n\n/// Search Playwright's browser cache for a Chromium binary.\n/// Legacy fallback for users who previously installed Chromium via Playwright.\nfn find_playwright_chromium() -> Option<PathBuf> {\n    let mut search_dirs = Vec::new();\n\n    if let Ok(custom) = std::env::var(\"PLAYWRIGHT_BROWSERS_PATH\") {\n        search_dirs.push(PathBuf::from(custom));\n    }\n\n    if let Some(home) = dirs::home_dir() {\n        search_dirs.push(home.join(\".cache/ms-playwright\"));\n    }\n\n    for dir in &search_dirs {\n        if !dir.is_dir() {\n            continue;\n        }\n        if let Ok(entries) = std::fs::read_dir(dir) {\n            let mut matches: Vec<PathBuf> = entries\n                .filter_map(|e| e.ok())\n                .filter(|e| {\n                    e.file_name()\n                        .to_str()\n                        .map(|n| n.starts_with(\"chromium-\"))\n                        .unwrap_or(false)\n                })\n                .filter_map(|e| {\n                    let candidate = build_playwright_binary_path(&e.path());\n                    if candidate.exists() {\n                        Some(candidate)\n                    } else {\n                        None\n                    }\n                })\n                .collect();\n            // Sort descending so the newest version wins\n            matches.sort();\n            matches.reverse();\n            if let Some(p) = matches.into_iter().next() {\n                return Some(p);\n            }\n        }\n    }\n\n    None\n}\n\n#[cfg(target_os = \"linux\")]\nfn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {\n    chromium_dir.join(\"chrome-linux64/chrome\")\n}\n\n#[cfg(target_os = \"macos\")]\nfn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {\n    chromium_dir.join(\"chrome-mac/Chromium.app/Contents/MacOS/Chromium\")\n}\n\n#[cfg(target_os = \"windows\")]\nfn build_playwright_binary_path(chromium_dir: &Path) -> PathBuf {\n    chromium_dir.join(\"chrome-win/chrome.exe\")\n}\n\nfn expand_tilde(path: &str) -> String {\n    if let Some(rest) = path.strip_prefix('~') {\n        if let Some(home) = dirs::home_dir() {\n            return home\n                .join(rest.strip_prefix('/').unwrap_or(rest))\n                .to_string_lossy()\n                .to_string();\n        }\n    }\n    path.to_string()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_utils::EnvGuard;\n\n    #[cfg(unix)]\n    fn spawn_noop_child() -> Child {\n        Command::new(\"/bin/sh\")\n            .args([\"-c\", \"exit 0\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .spawn()\n            .unwrap()\n    }\n\n    #[cfg(windows)]\n    fn spawn_noop_child() -> Child {\n        Command::new(\"cmd.exe\")\n            .args([\"/C\", \"exit 0\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .spawn()\n            .unwrap()\n    }\n\n    #[test]\n    fn test_find_chrome_returns_some_on_host() {\n        // This test only makes sense on systems with Chrome installed\n        if cfg!(target_os = \"macos\") || cfg!(target_os = \"linux\") {\n            let result = find_chrome();\n            // Don't assert Some -- CI may not have Chrome\n            if let Some(path) = result {\n                assert!(path.exists());\n            }\n        }\n    }\n\n    #[test]\n    fn test_expand_tilde() {\n        let expanded = expand_tilde(\"~/test/path\");\n        assert!(!expanded.starts_with('~'));\n        assert!(expanded.ends_with(\"test/path\"));\n    }\n\n    #[test]\n    fn test_expand_tilde_no_tilde() {\n        assert_eq!(expand_tilde(\"/absolute/path\"), \"/absolute/path\");\n    }\n\n    #[test]\n    fn test_read_devtools_active_port_missing() {\n        let result = read_devtools_active_port(Path::new(\"/nonexistent\"));\n        assert!(result.is_none());\n    }\n\n    #[test]\n    fn test_should_disable_sandbox_skips_if_already_set() {\n        let args = vec![\"--headless=new\".to_string(), \"--no-sandbox\".to_string()];\n        assert!(!should_disable_sandbox(&args));\n    }\n\n    #[test]\n    fn test_chrome_launch_error_no_stderr() {\n        let msg = chrome_launch_error(\"Chrome exited\", &[]);\n        assert!(msg.contains(\"no stderr output\"));\n        assert!(msg.contains(\"Hint:\"));\n        assert!(msg.contains(\"--no-sandbox\"));\n    }\n\n    #[test]\n    fn test_chrome_launch_error_with_sandbox_hint() {\n        let lines = vec![\n            \"some log line\".to_string(),\n            \"Failed to move to new namespace: sandbox error\".to_string(),\n        ];\n        let msg = chrome_launch_error(\"Chrome exited\", &lines);\n        assert!(msg.contains(\"sandbox error\"));\n        assert!(msg.contains(\"Hint:\"));\n        assert!(msg.contains(\"--no-sandbox\"));\n    }\n\n    #[test]\n    fn test_chrome_launch_error_generic() {\n        let lines = vec![\"info line\".to_string(), \"another info line\".to_string()];\n        let msg = chrome_launch_error(\"Chrome exited\", &lines);\n        assert!(msg.contains(\"last 2 lines\"));\n    }\n\n    #[test]\n    fn test_find_playwright_chromium_nonexistent() {\n        let _guard = EnvGuard::new(&[\"PLAYWRIGHT_BROWSERS_PATH\"]);\n        _guard.set(\"PLAYWRIGHT_BROWSERS_PATH\", \"/nonexistent/path\");\n        let result = find_playwright_chromium();\n        assert!(result.is_none());\n    }\n\n    #[test]\n    fn test_build_args_headless_includes_headless_flag() {\n        let opts = LaunchOptions {\n            headless: true,\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(result.args.iter().any(|a| a == \"--headless=new\"));\n        assert!(result\n            .args\n            .iter()\n            .any(|a| a == \"--enable-unsafe-swiftshader\"));\n        assert!(result.args.iter().any(|a| a == \"--window-size=1280,720\"));\n        // Temp dir created when no profile\n        assert!(result.temp_user_data_dir.is_some());\n        let dir = result.temp_user_data_dir.unwrap();\n        assert!(dir.exists());\n        let _ = std::fs::remove_dir_all(&dir);\n    }\n\n    #[test]\n    fn test_build_args_headed_no_headless_flag() {\n        let opts = LaunchOptions {\n            headless: false,\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(!result.args.iter().any(|a| a.contains(\"--headless\")));\n        assert!(!result\n            .args\n            .iter()\n            .any(|a| a == \"--enable-unsafe-swiftshader\"));\n        assert!(!result.args.iter().any(|a| a.starts_with(\"--window-size=\")));\n        // Temp dir created when no profile\n        assert!(result.temp_user_data_dir.is_some());\n        let dir = result.temp_user_data_dir.unwrap();\n        assert!(dir.exists());\n        let _ = std::fs::remove_dir_all(&dir);\n    }\n\n    #[test]\n    fn test_build_args_temp_user_data_dir_created() {\n        let opts = LaunchOptions::default();\n        let result = build_chrome_args(&opts).unwrap();\n        let dir = result.temp_user_data_dir.as_ref().unwrap();\n        assert!(dir.exists());\n        assert!(result\n            .args\n            .iter()\n            .any(|a| a.starts_with(\"--user-data-dir=\")));\n        let _ = std::fs::remove_dir_all(dir);\n    }\n\n    #[test]\n    fn test_build_args_profile_no_temp_dir() {\n        let opts = LaunchOptions {\n            profile: Some(\"/tmp/my-profile\".to_string()),\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(result.temp_user_data_dir.is_none());\n        assert!(result\n            .args\n            .iter()\n            .any(|a| a == \"--user-data-dir=/tmp/my-profile\"));\n    }\n\n    #[test]\n    fn test_build_args_custom_window_size_not_overridden() {\n        let opts = LaunchOptions {\n            headless: true,\n            args: vec![\"--window-size=1920,1080\".to_string()],\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(!result.args.iter().any(|a| a == \"--window-size=1280,720\"));\n        assert!(result.args.iter().any(|a| a == \"--window-size=1920,1080\"));\n        if let Some(ref dir) = result.temp_user_data_dir {\n            let _ = std::fs::remove_dir_all(dir);\n        }\n    }\n\n    #[test]\n    fn test_build_args_start_maximized_suppresses_default_window_size() {\n        let opts = LaunchOptions {\n            headless: true,\n            args: vec![\"--start-maximized\".to_string()],\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(!result.args.iter().any(|a| a == \"--window-size=1280,720\"));\n        assert!(result.args.iter().any(|a| a == \"--start-maximized\"));\n        if let Some(ref dir) = result.temp_user_data_dir {\n            let _ = std::fs::remove_dir_all(dir);\n        }\n    }\n\n    #[test]\n    fn test_build_args_disables_translate() {\n        let opts = LaunchOptions::default();\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(result\n            .args\n            .iter()\n            .any(|a| a.contains(\"--disable-features\") && a.contains(\"Translate\")));\n        if let Some(ref dir) = result.temp_user_data_dir {\n            let _ = std::fs::remove_dir_all(dir);\n        }\n    }\n\n    #[test]\n    fn test_build_args_headless_with_extensions_skips_headless_flag() {\n        let opts = LaunchOptions {\n            headless: true,\n            extensions: Some(vec![\"/tmp/my-ext\".to_string()]),\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(\n            !result.args.iter().any(|a| a.contains(\"--headless\")),\n            \"headless flag should be omitted when extensions are present\"\n        );\n        assert!(\n            !result.args.iter().any(|a| a.contains(\"--window-size\")),\n            \"window-size should be omitted when extensions force headed mode\"\n        );\n        assert!(result\n            .args\n            .iter()\n            .any(|a| a.starts_with(\"--load-extension=\")));\n        if let Some(ref dir) = result.temp_user_data_dir {\n            let _ = std::fs::remove_dir_all(dir);\n        }\n    }\n\n    #[test]\n    fn test_build_args_headed_with_extensions_no_headless_flag() {\n        let opts = LaunchOptions {\n            headless: false,\n            extensions: Some(vec![\"/tmp/my-ext\".to_string()]),\n            ..Default::default()\n        };\n        let result = build_chrome_args(&opts).unwrap();\n        assert!(\n            !result.args.iter().any(|a| a.contains(\"--headless\")),\n            \"headless flag should not be present in headed mode\"\n        );\n        assert!(result\n            .args\n            .iter()\n            .any(|a| a.starts_with(\"--load-extension=\")));\n        if let Some(ref dir) = result.temp_user_data_dir {\n            let _ = std::fs::remove_dir_all(dir);\n        }\n    }\n\n    #[test]\n    fn test_chrome_process_drop_cleans_temp_dir() {\n        let dir = std::env::temp_dir().join(format!(\n            \"agent-browser-chrome-drop-test-{}\",\n            uuid::Uuid::new_v4()\n        ));\n        let _ = std::fs::create_dir_all(&dir);\n        assert!(dir.exists());\n\n        {\n            // Simulate a ChromeProcess with a temp dir but a dummy child.\n            // We can't actually spawn Chrome here, but we can verify the Drop\n            // logic by creating a small helper process.\n            let child = spawn_noop_child();\n            let _process = ChromeProcess {\n                child,\n                ws_url: String::new(),\n                temp_user_data_dir: Some(dir.clone()),\n            };\n            // _process dropped here\n        }\n\n        assert!(!dir.exists(), \"Temp dir should be cleaned up on drop\");\n    }\n}\n"
  },
  {
    "path": "cli/src/native/cdp/client.rs",
    "content": "use std::collections::HashMap;\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::Arc;\n\nuse futures_util::{SinkExt, StreamExt};\nuse serde_json::Value;\nuse tokio::sync::{broadcast, oneshot, Mutex};\nuse tokio_tungstenite::tungstenite::protocol::WebSocketConfig;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::types::{CdpCommand, CdpEvent, CdpMessage};\n\ntype PendingMap = Arc<Mutex<HashMap<u64, oneshot::Sender<CdpMessage>>>>;\n\n/// Interval between WebSocket ping frames sent to keep the connection alive\n/// through intermediate proxies (reverse proxies, load balancers, service meshes).\nconst WS_KEEPALIVE_INTERVAL_SECS: u64 = 30;\n\n/// Raw incoming CDP message (text) broadcast to all subscribers.\n/// Used by the inspect proxy to forward responses and events to DevTools.\n#[derive(Debug, Clone)]\npub struct RawCdpMessage {\n    pub text: String,\n    pub session_id: Option<String>,\n}\n\npub struct CdpClient {\n    ws_tx: Arc<\n        Mutex<\n            futures_util::stream::SplitSink<\n                tokio_tungstenite::WebSocketStream<\n                    tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,\n                >,\n                Message,\n            >,\n        >,\n    >,\n    next_id: AtomicU64,\n    pending: PendingMap,\n    event_tx: broadcast::Sender<CdpEvent>,\n    raw_tx: broadcast::Sender<RawCdpMessage>,\n    _reader_handle: tokio::task::JoinHandle<()>,\n    _keepalive_handle: tokio::task::JoinHandle<()>,\n}\n\nimpl CdpClient {\n    pub async fn connect(url: &str) -> Result<Self, String> {\n        // Use unlimited message/frame sizes to handle large CDP responses\n        // (e.g. Accessibility.getFullAXTree) over remote WSS connections where\n        // proxies may produce frames exceeding the default 16 MiB limit.\n        let ws_config = WebSocketConfig {\n            max_message_size: None,\n            max_frame_size: None,\n            ..Default::default()\n        };\n\n        let (ws_stream, _) =\n            tokio_tungstenite::connect_async_with_config(url, Some(ws_config), false)\n                .await\n                .map_err(|e| format!(\"CDP WebSocket connect failed: {}\", e))?;\n\n        // Enable TCP SO_KEEPALIVE on the underlying socket. This matches the\n        // behavior of Playwright's WebSocket transport (pre-v0.20.0) which used\n        // Node.js HTTP agents with keepAlive: true. TCP-level keepalive probes\n        // maintain the connection at the transport layer, complementing the\n        // WebSocket-level Ping frames sent by the keepalive task below.\n        enable_tcp_keepalive(ws_stream.get_ref());\n\n        let (ws_tx, mut ws_rx) = ws_stream.split();\n        let ws_tx = Arc::new(Mutex::new(ws_tx));\n\n        let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));\n        let (event_tx, _) = broadcast::channel(256);\n        let (raw_tx, _) = broadcast::channel(512);\n\n        let pending_clone = pending.clone();\n        let event_tx_clone = event_tx.clone();\n        let raw_tx_clone = raw_tx.clone();\n\n        // Notify used to stop the keepalive task when the reader loop exits.\n        let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);\n\n        let reader_handle = tokio::spawn(async move {\n            while let Some(msg) = ws_rx.next().await {\n                // Accept both Text and Binary frames — remote CDP proxies\n                // (e.g. Browserless) may send responses as Binary frames.\n                let msg = match msg {\n                    Ok(Message::Text(text)) => text,\n                    Ok(Message::Binary(data)) => match String::from_utf8(data) {\n                        Ok(text) => text,\n                        Err(_) => continue,\n                    },\n                    Ok(Message::Close(_)) => break,\n                    Ok(Message::Pong(_)) => continue,\n                    Ok(_) => continue,\n                    Err(_) => break,\n                };\n\n                // Broadcast raw message for inspect proxy subscribers before typed parse,\n                // so messages with negative IDs (used by the inspect proxy) are still delivered.\n                if raw_tx_clone.receiver_count() > 0 {\n                    let session_id = serde_json::from_str::<serde_json::Value>(&msg)\n                        .ok()\n                        .and_then(|v| v.get(\"sessionId\")?.as_str().map(String::from));\n                    let _ = raw_tx_clone.send(RawCdpMessage {\n                        text: msg.clone(),\n                        session_id,\n                    });\n                }\n\n                let parsed: CdpMessage = match serde_json::from_str(&msg) {\n                    Ok(m) => m,\n                    // Expected for inspect proxy messages with negative IDs\n                    // (CdpMessage.id is u64); handled via raw broadcast above.\n                    Err(_) => continue,\n                };\n\n                if let Some(id) = parsed.id {\n                    // Response to a command\n                    let mut pending = pending_clone.lock().await;\n                    if let Some(tx) = pending.remove(&id) {\n                        let _ = tx.send(parsed);\n                    }\n                } else if let Some(ref method) = parsed.method {\n                    // Event\n                    let event = CdpEvent {\n                        method: method.clone(),\n                        params: parsed.params.clone().unwrap_or(Value::Null),\n                        session_id: parsed.session_id.clone(),\n                    };\n                    let _ = event_tx_clone.send(event);\n                }\n            }\n\n            // Reader loop exited (connection closed or error). Drop all pending\n            // command senders so callers get an immediate channel-closed error\n            // instead of waiting for the 30-second timeout.\n            pending_clone.lock().await.clear();\n\n            // Stop the keepalive task — the connection is gone.\n            let _ = cancel_tx.send(true);\n        });\n\n        // Spawn a keepalive task that sends WebSocket Ping frames at a regular\n        // interval. This prevents intermediate proxies (Envoy, nginx, OpenResty,\n        // cloud load balancers) from closing idle WebSocket connections. If the\n        // send fails, the connection is dead and we stop pinging.\n        let keepalive_tx = ws_tx.clone();\n        let keepalive_handle = tokio::spawn(async move {\n            let interval = std::time::Duration::from_secs(WS_KEEPALIVE_INTERVAL_SECS);\n            loop {\n                tokio::select! {\n                    _ = tokio::time::sleep(interval) => {}\n                    _ = cancel_rx.changed() => break,\n                }\n                let mut tx = keepalive_tx.lock().await;\n                if tx.send(Message::Ping(Vec::new())).await.is_err() {\n                    break;\n                }\n            }\n        });\n\n        Ok(Self {\n            ws_tx,\n            next_id: AtomicU64::new(1),\n            pending,\n            event_tx,\n            raw_tx,\n            _reader_handle: reader_handle,\n            _keepalive_handle: keepalive_handle,\n        })\n    }\n\n    pub async fn send_command(\n        &self,\n        method: &str,\n        params: Option<Value>,\n        session_id: Option<&str>,\n    ) -> Result<Value, String> {\n        let id = self.next_id.fetch_add(1, Ordering::SeqCst);\n\n        let cmd = CdpCommand {\n            id,\n            method: method.to_string(),\n            params,\n            session_id: session_id.map(|s| s.to_string()),\n        };\n\n        let json = serde_json::to_string(&cmd)\n            .map_err(|e| format!(\"Failed to serialize CDP command: {}\", e))?;\n\n        let (tx, rx) = oneshot::channel();\n\n        {\n            let mut pending = self.pending.lock().await;\n            pending.insert(id, tx);\n        }\n\n        {\n            let mut ws_tx = self.ws_tx.lock().await;\n            ws_tx\n                .send(Message::Text(json))\n                .await\n                .map_err(|e| format!(\"Failed to send CDP command: {}\", e))?;\n        }\n\n        let response = match tokio::time::timeout(std::time::Duration::from_secs(30), rx).await {\n            Ok(Ok(resp)) => resp,\n            Ok(Err(_)) => return Err(\"CDP response channel closed\".to_string()),\n            Err(_) => {\n                self.pending.lock().await.remove(&id);\n                return Err(format!(\"CDP command timed out: {}\", method));\n            }\n        };\n\n        if let Some(error) = response.error {\n            return Err(format!(\"CDP error ({}): {}\", method, error));\n        }\n\n        Ok(response.result.unwrap_or(Value::Null))\n    }\n\n    pub fn subscribe(&self) -> broadcast::Receiver<CdpEvent> {\n        self.event_tx.subscribe()\n    }\n\n    /// Subscribe to all raw incoming CDP messages (responses + events).\n    /// Used by the inspect proxy to forward traffic to the DevTools frontend.\n    pub fn subscribe_raw(&self) -> broadcast::Receiver<RawCdpMessage> {\n        self.raw_tx.subscribe()\n    }\n\n    /// Create a lightweight handle for the inspect WebSocket proxy.\n    /// Contains only what's needed to forward messages bidirectionally.\n    pub fn inspect_handle(&self) -> InspectProxyHandle {\n        InspectProxyHandle {\n            ws_tx: self.ws_tx.clone(),\n            raw_tx: self.raw_tx.clone(),\n        }\n    }\n\n    pub async fn send_command_typed<P: serde::Serialize, R: serde::de::DeserializeOwned>(\n        &self,\n        method: &str,\n        params: &P,\n        session_id: Option<&str>,\n    ) -> Result<R, String> {\n        let params_value = serde_json::to_value(params)\n            .map_err(|e| format!(\"Failed to serialize params: {}\", e))?;\n        let result = self\n            .send_command(method, Some(params_value), session_id)\n            .await?;\n        serde_json::from_value(result)\n            .map_err(|e| format!(\"Failed to deserialize CDP response for {}: {}\", method, e))\n    }\n\n    pub async fn send_command_no_params(\n        &self,\n        method: &str,\n        session_id: Option<&str>,\n    ) -> Result<Value, String> {\n        self.send_command(method, None, session_id).await\n    }\n\n    /// Send raw JSON through the WebSocket without tracking a response.\n    /// Used by the inspect proxy to forward DevTools frontend messages.\n    pub async fn send_raw(&self, json: String) -> Result<(), String> {\n        let mut ws_tx = self.ws_tx.lock().await;\n        ws_tx\n            .send(Message::Text(json))\n            .await\n            .map_err(|e| format!(\"Failed to send raw CDP message: {}\", e))\n    }\n}\n\ntype WsTx = Arc<\n    Mutex<\n        futures_util::stream::SplitSink<\n            tokio_tungstenite::WebSocketStream<\n                tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,\n            >,\n            Message,\n        >,\n    >,\n>;\n\n/// Lightweight handle for the inspect WebSocket proxy, holding only\n/// the cloneable parts of CdpClient needed for bidirectional message forwarding.\npub struct InspectProxyHandle {\n    ws_tx: WsTx,\n    raw_tx: broadcast::Sender<RawCdpMessage>,\n}\n\nimpl InspectProxyHandle {\n    pub async fn send_raw(&self, json: String) -> Result<(), String> {\n        let mut ws_tx = self.ws_tx.lock().await;\n        ws_tx\n            .send(Message::Text(json))\n            .await\n            .map_err(|e| format!(\"Failed to send raw CDP message: {}\", e))\n    }\n\n    pub fn subscribe_raw(&self) -> broadcast::Receiver<RawCdpMessage> {\n        self.raw_tx.subscribe()\n    }\n}\n\n/// Enable TCP SO_KEEPALIVE on the underlying socket of a WebSocket connection.\n/// This is best-effort: failures are silently ignored since the WebSocket-level\n/// Ping keepalive provides the primary connection liveness mechanism.\nfn enable_tcp_keepalive(stream: &tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>) {\n    let tcp_stream = match stream {\n        tokio_tungstenite::MaybeTlsStream::Plain(s) => s,\n        tokio_tungstenite::MaybeTlsStream::Rustls(s) => s.get_ref().0,\n        _ => return,\n    };\n\n    // SockRef borrows the fd without taking ownership.\n    let sock = socket2::SockRef::from(tcp_stream);\n    let keepalive = socket2::TcpKeepalive::new().with_time(std::time::Duration::from_secs(30));\n\n    // with_interval sets TCP_KEEPINTVL — the time between probes after the\n    // first keepalive probe goes unanswered. Available on most platforms\n    // (Linux, macOS, Windows, FreeBSD, etc.) but not OpenBSD or Haiku.\n    #[cfg(not(any(target_os = \"openbsd\", target_os = \"haiku\")))]\n    let keepalive = keepalive.with_interval(std::time::Duration::from_secs(10));\n\n    let _ = sock.set_tcp_keepalive(&keepalive);\n}\n"
  },
  {
    "path": "cli/src/native/cdp/discovery.rs",
    "content": "use std::time::Duration;\n\nuse futures_util::{SinkExt, StreamExt};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::types::BrowserVersionInfo;\n\n/// Default timeout for CDP discovery HTTP requests.\nconst DEFAULT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(2);\n\n/// Discover the CDP WebSocket URL for the given host and port.\n///\n/// Tries three methods in order: `/json/version`, `/json/list`, and a direct\n/// WebSocket connection to `/devtools/browser`. The returned URL has its\n/// host/port rewritten to match the requested target.\npub async fn discover_cdp_url(host: &str, port: u16) -> Result<String, String> {\n    discover_cdp_url_with_timeout(host, port, DEFAULT_DISCOVERY_TIMEOUT).await\n}\n\n/// Like [`discover_cdp_url`] but with a custom request timeout.\npub async fn discover_cdp_url_with_timeout(\n    host: &str,\n    port: u16,\n    timeout: Duration,\n) -> Result<String, String> {\n    // Primary: /json/version (standard path)\n    let version_err = match fetch_cdp_info(host, port, timeout).await {\n        Ok(info) => {\n            if let Some(ws_url) = info.web_socket_debugger_url {\n                return Ok(rewrite_ws_host(&ws_url, host, port));\n            }\n            format!(\n                \"No webSocketDebuggerUrl in /json/version at {}:{}\",\n                host, port\n            )\n        }\n        Err(e) => e,\n    };\n\n    // Fallback: /json/list (returns target list; look for the browser target)\n    let list_err = match fetch_cdp_list(host, port, timeout).await {\n        Ok(ws_url) => return Ok(rewrite_ws_host(&ws_url, host, port)),\n        Err(e) => e,\n    };\n\n    // Final fallback: direct WebSocket at /devtools/browser.\n    // Chrome 136+ with UI-based remote debugging (chrome://inspect) exposes\n    // CDP over WebSocket but does not serve HTTP discovery endpoints.\n    match discover_cdp_ws(host, port, timeout).await {\n        Ok(ws_url) => Ok(ws_url),\n        Err(ws_err) => Err(format!(\n            \"All CDP discovery methods failed for {}:{}: /json/version: {}; /json/list: {}; WebSocket: {}\",\n            host, port, version_err, list_err, ws_err\n        )),\n    }\n}\n\n/// Bracket an IPv6 address for use in URLs. No-op for IPv4 or already-bracketed addresses.\nfn bracket_ipv6(host: &str) -> String {\n    if host.contains(':') && !host.starts_with('[') {\n        format!(\"[{}]\", host)\n    } else {\n        host.to_string()\n    }\n}\n\n/// Fetch `/json/version` from the given host:port and parse the response.\nasync fn fetch_cdp_info(\n    host: &str,\n    port: u16,\n    timeout: Duration,\n) -> Result<BrowserVersionInfo, String> {\n    let url = format!(\"http://{}:{}/json/version\", bracket_ipv6(host), port);\n\n    let body = tokio::time::timeout(timeout, reqwest_get_string(&url))\n        .await\n        .map_err(|_| format!(\"Timeout connecting to CDP at {}:{}\", host, port))?\n        .map_err(|e| format!(\"Failed to connect to CDP at {}:{}: {}\", host, port, e))?;\n\n    serde_json::from_str(&body).map_err(|e| format!(\"Invalid /json/version response: {}\", e))\n}\n\n/// Rewrite the host and port in a WebSocket URL to match the target we\n/// actually connected to. Chrome's `/json/version` always returns\n/// `ws://127.0.0.1:<local-port>/...` which is unreachable when the\n/// browser is on a remote machine or behind a port-forward.\nfn rewrite_ws_host(ws_url: &str, host: &str, port: u16) -> String {\n    if let Ok(mut parsed) = url::Url::parse(ws_url) {\n        let _ = parsed.set_host(Some(&bracket_ipv6(host)));\n        let _ = parsed.set_port(Some(port));\n        parsed.to_string()\n    } else {\n        ws_url.to_string()\n    }\n}\n\n/// Fetch `/json/list` and extract the `webSocketDebuggerUrl` from the first\n/// target with `type == \"browser\"`, or the first target if none has that type.\nasync fn fetch_cdp_list(host: &str, port: u16, timeout: Duration) -> Result<String, String> {\n    let url = format!(\"http://{}:{}/json/list\", bracket_ipv6(host), port);\n\n    let body = tokio::time::timeout(timeout, reqwest_get_string(&url))\n        .await\n        .map_err(|_| format!(\"Timeout connecting to /json/list at {}:{}\", host, port))?\n        .map_err(|e| {\n            format!(\n                \"Failed to connect to /json/list at {}:{}: {}\",\n                host, port, e\n            )\n        })?;\n\n    let targets: Vec<serde_json::Value> =\n        serde_json::from_str(&body).map_err(|e| format!(\"Invalid /json/list response: {}\", e))?;\n\n    // Prefer targets with type \"browser\", fall back to first target with a ws URL\n    let browser_target = targets\n        .iter()\n        .find(|t| t.get(\"type\").and_then(|v| v.as_str()) == Some(\"browser\"));\n\n    let target = browser_target.or_else(|| targets.first());\n\n    target\n        .and_then(|t| t.get(\"webSocketDebuggerUrl\"))\n        .and_then(|v| v.as_str())\n        .map(|s| s.to_string())\n        .ok_or_else(|| \"No webSocketDebuggerUrl found in /json/list targets\".to_string())\n}\n\n/// Discover a CDP endpoint by connecting directly to `ws://host:port/devtools/browser`\n/// and verifying it responds to `Browser.getVersion`.\n/// Returns the WebSocket URL on success.\nasync fn discover_cdp_ws(host: &str, port: u16, timeout: Duration) -> Result<String, String> {\n    let ws_url = format!(\"ws://{}:{}/devtools/browser\", bracket_ipv6(host), port);\n\n    tokio::time::timeout(timeout, async {\n        let (mut ws_stream, _) = tokio_tungstenite::connect_async(&ws_url)\n            .await\n            .map_err(|e| format!(\"WebSocket connect failed at {}: {}\", ws_url, e))?;\n\n        let cmd = r#\"{\"id\":1,\"method\":\"Browser.getVersion\"}\"#;\n        ws_stream\n            .send(Message::Text(cmd.into()))\n            .await\n            .map_err(|e| format!(\"Failed to send command: {}\", e))?;\n\n        #[derive(serde::Deserialize)]\n        struct CdpReply {\n            id: u64,\n        }\n\n        let mut result: Result<(), String> = Err(\"No valid CDP response received\".to_string());\n        while let Some(msg) = ws_stream.next().await {\n            match msg {\n                Ok(Message::Text(text)) => {\n                    if serde_json::from_str::<CdpReply>(&text).is_ok_and(|r| r.id == 1) {\n                        result = Ok(());\n                        break;\n                    }\n                }\n                Ok(Message::Close(_)) | Err(_) => break,\n                _ => continue,\n            }\n        }\n\n        let _ = ws_stream.close(None).await;\n        result\n    })\n    .await\n    .map_err(|_| format!(\"Timeout connecting to WebSocket at {}\", ws_url))?\n    .map(|()| ws_url)\n}\n\nasync fn reqwest_get_string(url: &str) -> Result<String, String> {\n    let resp = reqwest::get(url).await.map_err(|e| e.to_string())?;\n    resp.text().await.map_err(|e| e.to_string())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tokio::io::{AsyncReadExt, AsyncWriteExt};\n    use tokio::net::TcpListener;\n\n    const HTTP_404: &str =\n        \"HTTP/1.1 404 Not Found\\r\\nContent-Length: 0\\r\\nConnection: close\\r\\n\\r\\n\";\n\n    fn http_200(body: &str) -> String {\n        format!(\n            \"HTTP/1.1 200 OK\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\nContent-Type: application/json\\r\\n\\r\\n{}\",\n            body.len(), body\n        )\n    }\n\n    async fn accept_http(listener: &TcpListener, response: &str) {\n        let (mut s, _) = listener.accept().await.unwrap();\n        let mut buf = [0u8; 1024];\n        let _ = s.read(&mut buf).await;\n        s.write_all(response.as_bytes()).await.unwrap();\n    }\n\n    #[tokio::test]\n    async fn discovers_ws_url_from_json_version() {\n        let listener = TcpListener::bind(\"127.0.0.1:0\").await.unwrap();\n        let port = listener.local_addr().unwrap().port();\n        let server = tokio::spawn(async move {\n            accept_http(\n                &listener,\n                &http_200(r#\"{\"webSocketDebuggerUrl\":\"ws://127.0.0.1:1234/\"}\"#),\n            )\n            .await;\n        });\n\n        let ws_url = discover_cdp_url(\"127.0.0.1\", port).await.unwrap();\n        assert_eq!(ws_url, format!(\"ws://127.0.0.1:{}/\", port));\n        server.await.unwrap();\n    }\n\n    #[tokio::test]\n    async fn returns_error_when_version_returns_invalid_json() {\n        let listener = TcpListener::bind(\"127.0.0.1:0\").await.unwrap();\n        let port = listener.local_addr().unwrap().port();\n        let server = tokio::spawn(async move {\n            accept_http(&listener, &http_200(\"not-json\")).await;\n            // /json/list and ws fallback both fail (server closes)\n        });\n\n        let err = discover_cdp_url(\"127.0.0.1\", port).await.unwrap_err();\n        assert!(err.contains(\"Invalid /json/version response\"));\n        server.await.unwrap();\n    }\n\n    #[tokio::test]\n    async fn falls_back_to_json_list_on_version_404() {\n        let listener = TcpListener::bind(\"127.0.0.1:0\").await.unwrap();\n        let port = listener.local_addr().unwrap().port();\n        let server = tokio::spawn(async move {\n            accept_http(&listener, HTTP_404).await;\n            accept_http(\n                &listener,\n                &http_200(r#\"[{\"type\":\"browser\",\"webSocketDebuggerUrl\":\"ws://127.0.0.1:1234/devtools/browser/abc\"}]\"#),\n            ).await;\n        });\n\n        let ws_url = discover_cdp_url(\"127.0.0.1\", port).await.unwrap();\n        assert!(ws_url.contains(\"/devtools/browser/abc\"));\n        assert!(ws_url.contains(&port.to_string()));\n        server.await.unwrap();\n    }\n\n    #[tokio::test]\n    async fn falls_back_to_ws_when_http_returns_404() {\n        let listener = TcpListener::bind(\"127.0.0.1:0\").await.unwrap();\n        let port = listener.local_addr().unwrap().port();\n        let server = tokio::spawn(async move {\n            // /json/version -> 404, /json/list -> 404\n            accept_http(&listener, HTTP_404).await;\n            accept_http(&listener, HTTP_404).await;\n\n            // WebSocket handshake + respond to Browser.getVersion\n            let (stream, _) = listener.accept().await.unwrap();\n            let mut ws = tokio_tungstenite::accept_async(stream).await.unwrap();\n            if let Some(Ok(Message::Text(text))) = ws.next().await {\n                let req: serde_json::Value = serde_json::from_str(&text).unwrap();\n                let id = req.get(\"id\").unwrap();\n                let reply = format!(\n                    r#\"{{\"id\":{},\"result\":{{\"protocolVersion\":\"1.3\",\"product\":\"Chrome/136\"}}}}\"#,\n                    id\n                );\n                ws.send(Message::Text(reply)).await.unwrap();\n            }\n            let _ = ws.close(None).await;\n        });\n\n        let ws_url = discover_cdp_url(\"127.0.0.1\", port).await.unwrap();\n        assert_eq!(ws_url, format!(\"ws://127.0.0.1:{}/devtools/browser\", port));\n        server.await.unwrap();\n    }\n\n    #[test]\n    fn rewrite_ws_host_replaces_host_and_port() {\n        let original = \"ws://127.0.0.1:9222/devtools/browser/abc\";\n        let rewritten = rewrite_ws_host(original, \"10.211.55.12\", 9223);\n        assert_eq!(rewritten, \"ws://10.211.55.12:9223/devtools/browser/abc\");\n    }\n\n    #[test]\n    fn rewrite_ws_host_handles_ipv6() {\n        let original = \"ws://127.0.0.1:9222/devtools/browser/abc\";\n        let rewritten = rewrite_ws_host(original, \"::1\", 9222);\n        assert_eq!(rewritten, \"ws://[::1]:9222/devtools/browser/abc\");\n    }\n}\n"
  },
  {
    "path": "cli/src/native/cdp/lightpanda.rs",
    "content": "use std::collections::VecDeque;\nuse std::io::{BufRead, BufReader};\nuse std::net::TcpListener;\nuse std::path::PathBuf;\nuse std::process::{Child, Command, Stdio};\nuse std::sync::{Arc, Mutex};\nuse std::time::Duration;\n\nuse super::discovery::discover_cdp_url_with_timeout;\n\nconst LIGHTPANDA_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);\nconst LIGHTPANDA_POLL_INTERVAL: Duration = Duration::from_millis(100);\nconst LIGHTPANDA_DISCOVERY_TIMEOUT: Duration = Duration::from_millis(500);\nconst LIGHTPANDA_SESSION_TIMEOUT_SECS: u64 = 604800; // 1 week, the documented maximum\nconst MAX_LOG_LINES: usize = 40;\n\npub struct LightpandaProcess {\n    child: Child,\n    pub ws_url: String,\n    _log_drainers: Vec<std::thread::JoinHandle<()>>,\n}\n\nimpl LightpandaProcess {\n    pub fn kill(&mut self) {\n        let _ = self.child.kill();\n        let _ = self.child.wait();\n    }\n}\n\nimpl Drop for LightpandaProcess {\n    fn drop(&mut self) {\n        self.kill();\n    }\n}\n\n#[derive(Default)]\npub struct LightpandaLaunchOptions {\n    pub executable_path: Option<String>,\n    pub proxy: Option<String>,\n    pub port: Option<u16>,\n}\n\nfn build_lightpanda_serve_args(port: u16, proxy: Option<&str>) -> Vec<String> {\n    let mut args = vec![\n        \"serve\".to_string(),\n        \"--host\".to_string(),\n        \"127.0.0.1\".to_string(),\n        \"--port\".to_string(),\n        port.to_string(),\n        \"--timeout\".to_string(),\n        LIGHTPANDA_SESSION_TIMEOUT_SECS.to_string(),\n    ];\n\n    if let Some(proxy) = proxy {\n        args.push(\"--http_proxy\".to_string());\n        args.push(proxy.to_string());\n    }\n\n    args\n}\n\n#[derive(Clone, Default)]\nstruct LaunchLogBuffer {\n    stdout: Arc<Mutex<VecDeque<String>>>,\n    stderr: Arc<Mutex<VecDeque<String>>>,\n}\n\nimpl LaunchLogBuffer {\n    fn push_stdout(&self, line: String) {\n        push_bounded(&self.stdout, line);\n    }\n\n    fn push_stderr(&self, line: String) {\n        push_bounded(&self.stderr, line);\n    }\n\n    fn snapshot_stdout(&self) -> Vec<String> {\n        self.stdout\n            .lock()\n            .expect(\"stdout log buffer poisoned\")\n            .iter()\n            .cloned()\n            .collect()\n    }\n\n    fn snapshot_stderr(&self) -> Vec<String> {\n        self.stderr\n            .lock()\n            .expect(\"stderr log buffer poisoned\")\n            .iter()\n            .cloned()\n            .collect()\n    }\n}\n\nfn push_bounded(buffer: &Mutex<VecDeque<String>>, line: String) {\n    let mut guard = buffer.lock().expect(\"log buffer poisoned\");\n    if guard.len() >= MAX_LOG_LINES {\n        guard.pop_front();\n    }\n    guard.push_back(line);\n}\n\npub fn find_lightpanda() -> Option<PathBuf> {\n    #[cfg(unix)]\n    {\n        if let Ok(output) = Command::new(\"which\").arg(\"lightpanda\").output() {\n            if output.status.success() {\n                let path = String::from_utf8_lossy(&output.stdout).trim().to_string();\n                if !path.is_empty() {\n                    return Some(PathBuf::from(path));\n                }\n            }\n        }\n    }\n\n    #[cfg(windows)]\n    {\n        if let Ok(output) = Command::new(\"where\").arg(\"lightpanda\").output() {\n            if output.status.success() {\n                let path = String::from_utf8_lossy(&output.stdout)\n                    .lines()\n                    .next()\n                    .unwrap_or(\"\")\n                    .trim()\n                    .to_string();\n                if !path.is_empty() {\n                    return Some(PathBuf::from(path));\n                }\n            }\n        }\n    }\n\n    if let Some(home) = dirs::home_dir() {\n        let candidates = [\n            home.join(\".lightpanda/lightpanda\"),\n            home.join(\".local/bin/lightpanda\"),\n        ];\n        for c in &candidates {\n            if c.exists() {\n                return Some(c.clone());\n            }\n        }\n    }\n\n    None\n}\n\npub async fn launch_lightpanda(\n    options: &LightpandaLaunchOptions,\n) -> Result<LightpandaProcess, String> {\n    let binary_path = match &options.executable_path {\n        Some(p) => PathBuf::from(p),\n        None => find_lightpanda().ok_or(\n            \"Lightpanda not found. Install it from https://lightpanda.io/docs/open-source/installation or use --executable-path.\",\n        )?,\n    };\n\n    let port = match options.port {\n        Some(p) => p,\n        None => TcpListener::bind(\"127.0.0.1:0\")\n            .and_then(|l| l.local_addr())\n            .map(|a| a.port())\n            .map_err(|e| format!(\"Failed to find an available port for Lightpanda: {}\", e))?,\n    };\n    let args = build_lightpanda_serve_args(port, options.proxy.as_deref());\n\n    let mut child = Command::new(&binary_path)\n        .args(&args)\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .spawn()\n        .map_err(|e| format!(\"Failed to launch Lightpanda at {:?}: {}\", binary_path, e))?;\n\n    let (log_buffer, log_drainers) = start_log_drainers(&mut child)?;\n\n    let ws_url =\n        match wait_for_lightpanda_ready(&mut child, port, &log_buffer, LIGHTPANDA_STARTUP_TIMEOUT)\n            .await\n        {\n            Ok(url) => url,\n            Err(e) => {\n                let _ = child.kill();\n                let _ = child.wait();\n                return Err(e);\n            }\n        };\n\n    Ok(LightpandaProcess {\n        child,\n        ws_url,\n        _log_drainers: log_drainers,\n    })\n}\n\nfn start_log_drainers(\n    child: &mut Child,\n) -> Result<(LaunchLogBuffer, Vec<std::thread::JoinHandle<()>>), String> {\n    let stdout = child.stdout.take().ok_or_else(|| {\n        let _ = child.kill();\n        \"Failed to capture Lightpanda stdout\".to_string()\n    })?;\n    let stderr = child.stderr.take().ok_or_else(|| {\n        let _ = child.kill();\n        \"Failed to capture Lightpanda stderr\".to_string()\n    })?;\n\n    let logs = LaunchLogBuffer::default();\n    let stdout_logs = logs.clone();\n    let stderr_logs = logs.clone();\n\n    let stdout_handle =\n        std::thread::spawn(move || drain_reader(stdout, move |line| stdout_logs.push_stdout(line)));\n    let stderr_handle =\n        std::thread::spawn(move || drain_reader(stderr, move |line| stderr_logs.push_stderr(line)));\n\n    Ok((logs, vec![stdout_handle, stderr_handle]))\n}\n\nfn drain_reader<R, F>(reader: R, mut push: F)\nwhere\n    R: std::io::Read,\n    F: FnMut(String),\n{\n    for line in BufReader::new(reader).lines() {\n        match line {\n            Ok(line) => push(line),\n            Err(_) => break,\n        }\n    }\n}\n\nasync fn wait_for_lightpanda_ready(\n    child: &mut Child,\n    port: u16,\n    logs: &LaunchLogBuffer,\n    startup_timeout: Duration,\n) -> Result<String, String> {\n    let deadline = std::time::Instant::now() + startup_timeout;\n    let mut last_probe_error = None;\n\n    loop {\n        if let Ok(Some(status)) = child.try_wait() {\n            // Give the drainer threads a brief window to flush the last log lines\n            // before we snapshot them.  This is best-effort: lines written just\n            // before exit may still be missing, but the most useful output (early\n            // startup errors) will already be in the buffer.\n            tokio::time::sleep(Duration::from_millis(25)).await;\n            return Err(lightpanda_launch_error(\n                &format!(\n                    \"Lightpanda exited before CDP became ready (status: {})\",\n                    status\n                ),\n                logs,\n                last_probe_error.as_deref(),\n            ));\n        }\n\n        match discover_cdp_url_with_timeout(\"127.0.0.1\", port, LIGHTPANDA_DISCOVERY_TIMEOUT).await {\n            Ok(ws_url) => return Ok(ws_url),\n            Err(err) => last_probe_error = Some(err),\n        }\n\n        if std::time::Instant::now() >= deadline {\n            return Err(lightpanda_launch_error(\n                &format!(\n                    \"Timed out after {}ms waiting for Lightpanda CDP endpoint on port {}\",\n                    startup_timeout.as_millis(),\n                    port\n                ),\n                logs,\n                last_probe_error.as_deref(),\n            ));\n        }\n\n        tokio::time::sleep(LIGHTPANDA_POLL_INTERVAL).await;\n    }\n}\n\nfn lightpanda_launch_error(\n    message: &str,\n    logs: &LaunchLogBuffer,\n    last_probe_error: Option<&str>,\n) -> String {\n    let stdout_lines = logs.snapshot_stdout();\n    let stderr_lines = logs.snapshot_stderr();\n    let mut details = Vec::new();\n\n    if let Some(err) = last_probe_error {\n        details.push(format!(\"Last probe error: {}\", err));\n    }\n\n    if !stderr_lines.is_empty() {\n        details.push(format!(\n            \"Lightpanda stderr (last {} lines):\\n  {}\",\n            stderr_lines.len(),\n            stderr_lines.join(\"\\n  \")\n        ));\n    }\n\n    if !stdout_lines.is_empty() {\n        details.push(format!(\n            \"Lightpanda stdout (last {} lines):\\n  {}\",\n            stdout_lines.len(),\n            stdout_lines.join(\"\\n  \")\n        ));\n    }\n\n    if details.is_empty() {\n        format!(\"{} (no stdout/stderr output from Lightpanda)\", message)\n    } else {\n        format!(\"{}\\n{}\", message, details.join(\"\\n\"))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tokio::io::{AsyncReadExt, AsyncWriteExt};\n    use tokio::net::TcpListener as TokioTcpListener;\n\n    fn unused_port() -> u16 {\n        std::net::TcpListener::bind(\"127.0.0.1:0\")\n            .unwrap()\n            .local_addr()\n            .unwrap()\n            .port()\n    }\n\n    async fn serve_json_version_once_after_delay(port: u16, delay_ms: u64, body: &'static str) {\n        tokio::time::sleep(Duration::from_millis(delay_ms)).await;\n        let listener = TokioTcpListener::bind((\"127.0.0.1\", port)).await.unwrap();\n        let (mut socket, _) = listener.accept().await.unwrap();\n        let mut buf = [0u8; 1024];\n        let _ = socket.read(&mut buf).await;\n        let response = format!(\n            \"HTTP/1.1 200 OK\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\nContent-Type: application/json\\r\\n\\r\\n{}\",\n            body.len(),\n            body\n        );\n        socket.write_all(response.as_bytes()).await.unwrap();\n    }\n\n    #[cfg(unix)]\n    #[tokio::test]\n    async fn waits_for_ready_without_logs() {\n        let port = unused_port();\n        tokio::spawn(serve_json_version_once_after_delay(\n            port,\n            150,\n            r#\"{\"webSocketDebuggerUrl\":\"ws://127.0.0.1:9222/\"}\"#,\n        ));\n\n        let mut child = Command::new(\"/bin/sh\")\n            .args([\"-c\", \"sleep 5\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .spawn()\n            .unwrap();\n\n        let (logs, _drainers) = start_log_drainers(&mut child).unwrap();\n        let ws_url = wait_for_lightpanda_ready(&mut child, port, &logs, LIGHTPANDA_STARTUP_TIMEOUT)\n            .await\n            .unwrap();\n\n        assert_eq!(ws_url, format!(\"ws://127.0.0.1:{}/\", port));\n        let _ = child.kill();\n        let _ = child.wait();\n    }\n\n    #[cfg(unix)]\n    #[tokio::test]\n    async fn child_exit_surfaces_logs() {\n        let port = unused_port();\n        let mut child = Command::new(\"/bin/sh\")\n            .args([\"-c\", \"echo boom >&2; sleep 0.1; exit 23\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .spawn()\n            .unwrap();\n\n        let (logs, _drainers) = start_log_drainers(&mut child).unwrap();\n        let err = wait_for_lightpanda_ready(&mut child, port, &logs, LIGHTPANDA_STARTUP_TIMEOUT)\n            .await\n            .unwrap_err();\n\n        assert!(err.contains(\"Lightpanda exited before CDP became ready\"));\n        assert!(err.contains(\"boom\"));\n    }\n\n    #[cfg(unix)]\n    #[tokio::test]\n    async fn timeout_reports_last_probe_error() {\n        let port = unused_port();\n        let mut child = Command::new(\"/bin/sh\")\n            .args([\"-c\", \"sleep 30\"])\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .spawn()\n            .unwrap();\n\n        let timeout = Duration::from_millis(300);\n        let (logs, _drainers) = start_log_drainers(&mut child).unwrap();\n        let err = tokio::time::timeout(\n            Duration::from_secs(2),\n            wait_for_lightpanda_ready(&mut child, port, &logs, timeout),\n        )\n        .await\n        .expect(\"ready wait should return before outer timeout\")\n        .unwrap_err();\n\n        assert!(err.contains(\"Timed out after 300ms waiting for Lightpanda CDP endpoint\"));\n        assert!(\n            err.contains(\"Failed to connect to CDP\") || err.contains(\"Timeout connecting to CDP\")\n        );\n\n        let _ = child.kill();\n        let _ = child.wait();\n    }\n\n    #[test]\n    fn test_find_lightpanda_returns_none_when_missing() {\n        let _ = find_lightpanda();\n    }\n\n    #[test]\n    fn test_lightpanda_launch_error_no_logs() {\n        let logs = LaunchLogBuffer::default();\n        let msg = lightpanda_launch_error(\"Lightpanda exited\", &logs, None);\n        assert!(msg.contains(\"no stdout/stderr output\"));\n    }\n\n    #[test]\n    fn test_lightpanda_launch_error_with_lines() {\n        let logs = LaunchLogBuffer::default();\n        logs.push_stdout(\"stdout line\".to_string());\n        logs.push_stderr(\"stderr line\".to_string());\n        let msg = lightpanda_launch_error(\"Lightpanda exited\", &logs, Some(\"connect failed\"));\n        assert!(msg.contains(\"stdout line\"));\n        assert!(msg.contains(\"stderr line\"));\n        assert!(msg.contains(\"Last probe error: connect failed\"));\n    }\n\n    #[test]\n    fn test_default_options() {\n        let opts = LightpandaLaunchOptions::default();\n        assert!(opts.executable_path.is_none());\n        assert!(opts.proxy.is_none());\n        assert!(opts.port.is_none());\n    }\n\n    #[test]\n    fn test_build_lightpanda_serve_args_sets_explicit_session_timeout() {\n        let args = build_lightpanda_serve_args(9222, None);\n\n        assert_eq!(\n            args,\n            vec![\n                \"serve\".to_string(),\n                \"--host\".to_string(),\n                \"127.0.0.1\".to_string(),\n                \"--port\".to_string(),\n                \"9222\".to_string(),\n                \"--timeout\".to_string(),\n                \"604800\".to_string(),\n            ]\n        );\n    }\n\n    #[test]\n    fn test_build_lightpanda_serve_args_with_proxy() {\n        let args = build_lightpanda_serve_args(9333, Some(\"http://127.0.0.1:8080\"));\n\n        assert_eq!(\n            args,\n            vec![\n                \"serve\".to_string(),\n                \"--host\".to_string(),\n                \"127.0.0.1\".to_string(),\n                \"--port\".to_string(),\n                \"9333\".to_string(),\n                \"--timeout\".to_string(),\n                \"604800\".to_string(),\n                \"--http_proxy\".to_string(),\n                \"http://127.0.0.1:8080\".to_string(),\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "cli/src/native/cdp/mod.rs",
    "content": "pub mod chrome;\npub mod client;\npub mod discovery;\npub mod lightpanda;\npub mod types;\n"
  },
  {
    "path": "cli/src/native/cdp/types.rs",
    "content": "use serde::{Deserialize, Deserializer, Serialize};\nuse serde_json::Value;\n\n/// Deserialize a value that may be either a string or an integer into a String.\n/// Lightpanda sends numeric nodeIds/childIds in AX tree responses, while Chrome\n/// sends strings. This accepts both.\nfn string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let v = Value::deserialize(deserializer)?;\n    match v {\n        Value::String(s) => Ok(s),\n        Value::Number(n) => Ok(n.to_string()),\n        other => Err(serde::de::Error::custom(format!(\n            \"expected string or integer, got {}\",\n            other\n        ))),\n    }\n}\n\n/// Deserialize an optional Vec where each element may be a string or integer.\nfn opt_vec_string_or_int<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let opt: Option<Vec<Value>> = Option::deserialize(deserializer)?;\n    match opt {\n        None => Ok(None),\n        Some(vec) => {\n            let mut result = Vec::with_capacity(vec.len());\n            for v in vec {\n                match v {\n                    Value::String(s) => result.push(s),\n                    Value::Number(n) => result.push(n.to_string()),\n                    other => {\n                        return Err(serde::de::Error::custom(format!(\n                            \"expected string or integer in array, got {}\",\n                            other\n                        )))\n                    }\n                }\n            }\n            Ok(Some(result))\n        }\n    }\n}\n\n// ---------------------------------------------------------------------------\n// CDP message envelope\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CdpCommand {\n    pub id: u64,\n    pub method: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub params: Option<Value>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub session_id: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CdpMessage {\n    pub id: Option<u64>,\n    pub result: Option<Value>,\n    pub error: Option<CdpError>,\n    pub method: Option<String>,\n    pub params: Option<Value>,\n    pub session_id: Option<String>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct CdpError {\n    pub code: Option<i64>,\n    pub message: String,\n    pub data: Option<String>,\n}\n\nimpl std::fmt::Display for CdpError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.message)\n    }\n}\n\n// ---------------------------------------------------------------------------\n// CDP events (broadcast to subscribers)\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Clone)]\npub struct CdpEvent {\n    pub method: String,\n    pub params: Value,\n    pub session_id: Option<String>,\n}\n\n// ---------------------------------------------------------------------------\n// Target domain\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct TargetInfo {\n    pub target_id: String,\n    #[serde(rename = \"type\")]\n    pub target_type: String,\n    pub title: String,\n    pub url: String,\n    pub attached: Option<bool>,\n    pub browser_context_id: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct GetTargetsResult {\n    pub target_infos: Vec<TargetInfo>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AttachToTargetParams {\n    pub target_id: String,\n    pub flatten: bool,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AttachToTargetResult {\n    pub session_id: String,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SetDiscoverTargetsParams {\n    pub discover: bool,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CreateTargetParams {\n    pub url: String,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CreateTargetResult {\n    pub target_id: String,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CloseTargetParams {\n    pub target_id: String,\n}\n\n// Target events\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct TargetCreatedEvent {\n    pub target_info: TargetInfo,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct TargetDestroyedEvent {\n    pub target_id: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct TargetInfoChangedEvent {\n    pub target_info: TargetInfo,\n}\n\n// ---------------------------------------------------------------------------\n// Page domain\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct PageNavigateParams {\n    pub url: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub referrer: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct PageNavigateResult {\n    pub frame_id: String,\n    pub loader_id: Option<String>,\n    pub error_text: Option<String>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct FrameNavigatedEvent {\n    pub frame: FrameInfo,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct FrameInfo {\n    pub id: String,\n    pub url: String,\n    pub parent_id: Option<String>,\n    pub name: Option<String>,\n}\n\n// Page.javascriptDialogOpening\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct JavascriptDialogOpeningEvent {\n    pub url: String,\n    pub message: String,\n    #[serde(rename = \"type\")]\n    pub dialog_type: String,\n    pub default_prompt: Option<String>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct HandleJavaScriptDialogParams {\n    pub accept: bool,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub prompt_text: Option<String>,\n}\n\n// ---------------------------------------------------------------------------\n// Runtime domain\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct EvaluateParams {\n    pub expression: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub return_by_value: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub await_promise: Option<bool>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct EvaluateResult {\n    pub result: RemoteObject,\n    pub exception_details: Option<ExceptionDetails>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct RemoteObject {\n    #[serde(rename = \"type\")]\n    pub object_type: String,\n    pub subtype: Option<String>,\n    pub value: Option<Value>,\n    pub description: Option<String>,\n    pub object_id: Option<String>,\n    pub class_name: Option<String>,\n    pub unserializable_value: Option<String>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ExceptionDetails {\n    pub text: String,\n    pub exception: Option<RemoteObject>,\n    pub line_number: Option<i64>,\n    pub column_number: Option<i64>,\n}\n\n// Runtime.consoleAPICalled\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ConsoleApiCalledEvent {\n    #[serde(rename = \"type\")]\n    pub call_type: String,\n    pub args: Vec<RemoteObject>,\n    pub timestamp: Option<f64>,\n}\n\n// Runtime.exceptionThrown\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ExceptionThrownEvent {\n    pub timestamp: f64,\n    pub exception_details: ExceptionDetails,\n}\n\n// ---------------------------------------------------------------------------\n// Accessibility domain\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct GetFullAXTreeResult {\n    pub nodes: Vec<AXNode>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AXNode {\n    #[serde(deserialize_with = \"string_or_int\")]\n    pub node_id: String,\n    pub role: Option<AXValue>,\n    pub name: Option<AXValue>,\n    pub value: Option<AXValue>,\n    pub description: Option<AXValue>,\n    pub properties: Option<Vec<AXProperty>>,\n    #[serde(default, deserialize_with = \"opt_vec_string_or_int\")]\n    pub child_ids: Option<Vec<String>>,\n    pub backend_d_o_m_node_id: Option<i64>,\n    pub ignored: Option<bool>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AXValue {\n    #[serde(rename = \"type\")]\n    pub value_type: String,\n    pub value: Option<Value>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AXProperty {\n    pub name: String,\n    pub value: AXValue,\n}\n\n// ---------------------------------------------------------------------------\n// Network domain (minimal for Phase 1)\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct RequestWillBeSentEvent {\n    pub request_id: String,\n    pub request: NetworkRequest,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NetworkRequest {\n    pub url: String,\n    pub method: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct LoadingFinishedEvent {\n    pub request_id: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct LoadingFailedEvent {\n    pub request_id: String,\n}\n\n// ---------------------------------------------------------------------------\n// DOM domain\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomResolveNodeParams {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub backend_node_id: Option<i64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub node_id: Option<i64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub object_group: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomResolveNodeResult {\n    pub object: RemoteObject,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomGetBoxModelParams {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub backend_node_id: Option<i64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub node_id: Option<i64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub object_id: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomGetBoxModelResult {\n    pub model: BoxModel,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct BoxModel {\n    pub content: Vec<f64>,\n    pub padding: Vec<f64>,\n    pub border: Vec<f64>,\n    pub margin: Vec<f64>,\n    pub width: i64,\n    pub height: i64,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomQuerySelectorParams {\n    pub node_id: i64,\n    pub selector: String,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomQuerySelectorResult {\n    pub node_id: i64,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomGetDocumentParams {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub depth: Option<i32>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomGetDocumentResult {\n    pub root: DomNode,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DomNode {\n    pub node_id: i64,\n    pub backend_node_id: Option<i64>,\n    pub node_type: Option<i64>,\n    pub node_name: Option<String>,\n    pub children: Option<Vec<DomNode>>,\n}\n\n// ---------------------------------------------------------------------------\n// Input domain\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DispatchMouseEventParams {\n    #[serde(rename = \"type\")]\n    pub event_type: String,\n    pub x: f64,\n    pub y: f64,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub button: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub buttons: Option<i32>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub click_count: Option<i32>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub delta_x: Option<f64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub delta_y: Option<f64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub modifiers: Option<i32>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct DispatchKeyEventParams {\n    #[serde(rename = \"type\")]\n    pub event_type: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub key: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub code: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub text: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub unmodified_text: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub windows_virtual_key_code: Option<i32>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub native_virtual_key_code: Option<i32>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub modifiers: Option<i32>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct InsertTextParams {\n    pub text: String,\n}\n\n// ---------------------------------------------------------------------------\n// Page.captureScreenshot\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CaptureScreenshotParams {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub format: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub quality: Option<i32>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub clip: Option<Viewport>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub from_surface: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub capture_beyond_viewport: Option<bool>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Viewport {\n    pub x: f64,\n    pub y: f64,\n    pub width: f64,\n    pub height: f64,\n    pub scale: f64,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CaptureScreenshotResult {\n    pub data: String,\n}\n\n// ---------------------------------------------------------------------------\n// Runtime.callFunctionOn\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CallFunctionOnParams {\n    pub function_declaration: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub object_id: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub arguments: Option<Vec<CallArgument>>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub return_by_value: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub await_promise: Option<bool>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CallArgument {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub value: Option<Value>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub object_id: Option<String>,\n}\n\n// ---------------------------------------------------------------------------\n// Version info (from /json/version)\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct BrowserVersionInfo {\n    #[serde(rename = \"webSocketDebuggerUrl\")]\n    pub web_socket_debugger_url: Option<String>,\n    #[serde(rename = \"Browser\")]\n    pub browser: Option<String>,\n}\n\n/// Auto-generated CDP types from protocol JSON files in `cdp-protocol/`.\n///\n/// To populate: download `browser_protocol.json` and `js_protocol.json` from\n/// <https://github.com/nicolo-ribaudo/nicolo-ribaudo.github.io/> (or any\n/// Chromium source) into `cli/cdp-protocol/` and rebuild.\n///\n/// Usage: `use super::cdp::types::generated::cdp_page::*;`\n#[allow(clippy::upper_case_acronyms)]\npub mod generated {\n    include!(concat!(env!(\"OUT_DIR\"), \"/cdp_generated.rs\"));\n}\n"
  },
  {
    "path": "cli/src/native/cookies.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse serde_json::{json, Value};\n\nuse super::cdp::client::CdpClient;\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Cookie {\n    pub name: String,\n    pub value: String,\n    pub domain: String,\n    pub path: String,\n    #[serde(default)]\n    pub expires: f64,\n    #[serde(default)]\n    pub size: i64,\n    #[serde(default)]\n    pub http_only: bool,\n    #[serde(default)]\n    pub secure: bool,\n    #[serde(default)]\n    pub session: bool,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub same_site: Option<String>,\n}\n\npub async fn get_cookies(\n    client: &CdpClient,\n    session_id: &str,\n    urls: Option<Vec<String>>,\n) -> Result<Vec<Cookie>, String> {\n    let params = match urls {\n        Some(ref u) if !u.is_empty() => json!({ \"urls\": u }),\n        _ => json!({}),\n    };\n\n    let result = client\n        .send_command(\"Network.getCookies\", Some(params), Some(session_id))\n        .await?;\n\n    let cookies: Vec<Cookie> = result\n        .get(\"cookies\")\n        .and_then(|v| serde_json::from_value(v.clone()).ok())\n        .unwrap_or_default();\n\n    Ok(cookies)\n}\n\npub async fn set_cookies(\n    client: &CdpClient,\n    session_id: &str,\n    cookies: Vec<Value>,\n    current_url: Option<&str>,\n) -> Result<(), String> {\n    let cookies: Vec<Value> = cookies\n        .into_iter()\n        .map(|mut c| {\n            // Auto-fill url if no domain/path/url provided\n            if c.get(\"url\").is_none() && c.get(\"domain\").is_none() && current_url.is_some() {\n                c.as_object_mut().map(|m| {\n                    m.insert(\n                        \"url\".to_string(),\n                        Value::String(current_url.unwrap().to_string()),\n                    )\n                });\n            }\n            c\n        })\n        .collect();\n\n    client\n        .send_command(\n            \"Network.setCookies\",\n            Some(json!({ \"cookies\": cookies })),\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn clear_cookies(client: &CdpClient, session_id: &str) -> Result<(), String> {\n    client\n        .send_command_no_params(\"Network.clearBrowserCookies\", Some(session_id))\n        .await?;\n    Ok(())\n}\n"
  },
  {
    "path": "cli/src/native/daemon.rs",
    "content": "use serde_json::Value;\nuse std::env;\nuse std::fs;\nuse std::io::Write;\nuse std::path::PathBuf;\nuse std::process;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};\nuse tokio::signal;\nuse tokio::sync::{mpsc, RwLock};\n\nuse super::actions::{execute_command, DaemonState};\nuse super::cdp::client::CdpClient;\nuse super::state;\nuse super::stream::StreamServer;\n\npub async fn run_daemon(session: &str) {\n    let socket_dir = get_daemon_socket_dir();\n    if !socket_dir.exists() {\n        let _ = fs::create_dir_all(&socket_dir);\n    }\n\n    let pid_path = socket_dir.join(format!(\"{}.pid\", session));\n    let _ = fs::write(&pid_path, process::id().to_string());\n\n    let socket_path = socket_dir.join(format!(\"{}.sock\", session));\n\n    if socket_path.exists() {\n        let _ = fs::remove_file(&socket_path);\n    }\n\n    if let Ok(days_str) = env::var(\"AGENT_BROWSER_STATE_EXPIRE_DAYS\") {\n        if let Ok(days) = days_str.parse::<u64>() {\n            if days > 0 {\n                let _ = state::state_clean(days);\n            }\n        }\n    }\n\n    let mut stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>> = None;\n    let mut stream_server_instance: Option<Arc<StreamServer>> = None;\n    if let Ok(port_str) = env::var(\"AGENT_BROWSER_STREAM_PORT\") {\n        if let Ok(port) = port_str.parse::<u16>() {\n            if port > 0 {\n                match StreamServer::start_without_client(port, session.to_string()).await {\n                    Ok((stream_server, client_slot)) => {\n                        stream_client = Some(client_slot.clone());\n                        let stream_path = socket_dir.join(format!(\"{}.stream\", session));\n                        if let Err(e) = fs::write(&stream_path, stream_server.port().to_string()) {\n                            let _ =\n                                writeln!(std::io::stderr(), \"Failed to write .stream file: {}\", e);\n                        }\n                        stream_server_instance = Some(Arc::new(stream_server));\n                    }\n                    Err(e) => {\n                        let _ = writeln!(std::io::stderr(), \"Stream server failed to start: {}\", e);\n                    }\n                }\n            }\n        }\n    }\n\n    // Auto-shutdown the daemon after this many ms of inactivity (no commands received).\n    // Disabled when unset or 0.\n    let idle_timeout_ms = env::var(\"AGENT_BROWSER_IDLE_TIMEOUT_MS\")\n        .ok()\n        .and_then(|s| s.parse::<u64>().ok())\n        .filter(|&ms| ms > 0);\n\n    let result = run_socket_server(\n        &socket_path,\n        session,\n        stream_client,\n        stream_server_instance,\n        idle_timeout_ms,\n    )\n    .await;\n\n    let _ = fs::remove_file(&socket_path);\n    let _ = fs::remove_file(&pid_path);\n    let stream_path = socket_dir.join(format!(\"{}.stream\", session));\n    let _ = fs::remove_file(&stream_path);\n\n    if let Err(e) = result {\n        let _ = writeln!(std::io::stderr(), \"Daemon error: {}\", e);\n        process::exit(1);\n    }\n}\n\n#[cfg(unix)]\nasync fn run_socket_server(\n    socket_path: &PathBuf,\n    _session: &str,\n    stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,\n    stream_server: Option<Arc<StreamServer>>,\n    idle_timeout_ms: Option<u64>,\n) -> Result<(), String> {\n    use tokio::net::UnixListener;\n\n    let listener =\n        UnixListener::bind(socket_path).map_err(|e| format!(\"Failed to bind socket: {}\", e))?;\n\n    let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> = std::sync::Arc::new(\n        tokio::sync::Mutex::new(DaemonState::new_with_stream(stream_client, stream_server)),\n    );\n\n    let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);\n    let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx));\n\n    loop {\n        let sleep_future = idle_timeout_ms.map(|ms| tokio::time::sleep(Duration::from_millis(ms)));\n        let mut sleep_pin = sleep_future.map(Box::pin);\n\n        tokio::select! {\n            accept_result = listener.accept() => {\n                match accept_result {\n                    Ok((stream, _)) => {\n                        let state = state.clone();\n                        let reset_tx = reset_tx.clone();\n                        tokio::spawn(async move {\n                            handle_connection(stream, state, reset_tx).await;\n                        });\n                    }\n                    Err(e) => {\n                        let _ = writeln!(std::io::stderr(), \"Accept error: {}\", e);\n                    }\n                }\n            }\n            _ = async {\n                if let Some(ref mut s) = sleep_pin {\n                    s.as_mut().await\n                } else {\n                    std::future::pending::<()>().await\n                }\n            }, if idle_timeout_ms.is_some() => {\n                let mut s = state.lock().await;\n                if let Some(ref mut mgr) = s.browser {\n                    let _ = mgr.close().await;\n                }\n                break;\n            }\n            _ = reset_rx.recv(), if idle_timeout_ms.is_some() => {\n                continue;\n            }\n            _ = shutdown_signal() => {\n                let mut s = state.lock().await;\n                if let Some(ref mut mgr) = s.browser {\n                    let _ = mgr.close().await;\n                }\n                break;\n            }\n        }\n    }\n\n    Ok(())\n}\n\n#[cfg(windows)]\nasync fn run_socket_server(\n    socket_path: &PathBuf,\n    session: &str,\n    stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,\n    stream_server: Option<Arc<StreamServer>>,\n    idle_timeout_ms: Option<u64>,\n) -> Result<(), String> {\n    use tokio::net::TcpListener;\n\n    let port = get_port_for_session(session);\n    let listener = TcpListener::bind(format!(\"127.0.0.1:{}\", port))\n        .await\n        .map_err(|e| format!(\"Failed to bind TCP: {}\", e))?;\n\n    let socket_dir = socket_path.parent().unwrap_or(std::path::Path::new(\".\"));\n    let port_path = socket_dir.join(format!(\"{}.port\", session));\n    let _ = fs::write(&port_path, port.to_string());\n\n    let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> = std::sync::Arc::new(\n        tokio::sync::Mutex::new(DaemonState::new_with_stream(stream_client, stream_server)),\n    );\n\n    let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);\n    let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx));\n\n    loop {\n        let sleep_future = idle_timeout_ms.map(|ms| tokio::time::sleep(Duration::from_millis(ms)));\n        let mut sleep_pin = sleep_future.map(Box::pin);\n\n        tokio::select! {\n            accept_result = listener.accept() => {\n                match accept_result {\n                    Ok((stream, _)) => {\n                        let state = state.clone();\n                        let reset_tx = reset_tx.clone();\n                        tokio::spawn(async move {\n                            handle_connection(stream, state, reset_tx).await;\n                        });\n                    }\n                    Err(e) => {\n                        let _ = writeln!(std::io::stderr(), \"Accept error: {}\", e);\n                    }\n                }\n            }\n            _ = async {\n                if let Some(ref mut s) = sleep_pin {\n                    s.as_mut().await\n                } else {\n                    std::future::pending::<()>().await\n                }\n            }, if idle_timeout_ms.is_some() => {\n                let mut s = state.lock().await;\n                if let Some(ref mut mgr) = s.browser {\n                    let _ = mgr.close().await;\n                }\n                let _ = fs::remove_file(&port_path);\n                break;\n            }\n            _ = reset_rx.recv(), if idle_timeout_ms.is_some() => {\n                continue;\n            }\n            _ = shutdown_signal() => {\n                let mut s = state.lock().await;\n                if let Some(ref mut mgr) = s.browser {\n                    let _ = mgr.close().await;\n                }\n                let _ = fs::remove_file(&port_path);\n                break;\n            }\n        }\n    }\n\n    Ok(())\n}\n\nasync fn handle_connection<S>(\n    stream: S,\n    state: std::sync::Arc<tokio::sync::Mutex<DaemonState>>,\n    idle_reset_tx: Option<Arc<mpsc::Sender<()>>>,\n) where\n    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,\n{\n    let (reader, mut writer) = tokio::io::split(stream);\n    let mut buf_reader = BufReader::new(reader);\n    let mut line = String::new();\n\n    loop {\n        line.clear();\n        match buf_reader.read_line(&mut line).await {\n            Ok(0) => break,\n            Ok(_) => {\n                let trimmed = line.trim();\n                if trimmed.is_empty() {\n                    continue;\n                }\n\n                if looks_like_http(trimmed) {\n                    break;\n                }\n\n                let cmd: Value = match serde_json::from_str(trimmed) {\n                    Ok(v) => v,\n                    Err(e) => {\n                        let err = serde_json::json!({\n                            \"success\": false,\n                            \"error\": format!(\"Invalid JSON: {}\", e),\n                        });\n                        let mut resp = serde_json::to_string(&err).unwrap_or_default();\n                        resp.push('\\n');\n                        let _ = writer.write_all(resp.as_bytes()).await;\n                        continue;\n                    }\n                };\n\n                if let Some(ref tx) = idle_reset_tx {\n                    let _ = tx.try_send(());\n                }\n\n                let is_close = cmd.get(\"action\").and_then(|v| v.as_str()) == Some(\"close\");\n\n                let response = {\n                    let mut s = state.lock().await;\n                    execute_command(&cmd, &mut s).await\n                };\n\n                let mut resp = serde_json::to_string(&response).unwrap_or_default();\n                resp.push('\\n');\n                if writer.write_all(resp.as_bytes()).await.is_err() {\n                    break;\n                }\n\n                if is_close {\n                    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;\n                    process::exit(0);\n                }\n            }\n            Err(_) => break,\n        }\n    }\n}\n\nfn looks_like_http(line: &str) -> bool {\n    let prefixes = [\n        \"GET \", \"POST \", \"PUT \", \"DELETE \", \"PATCH \", \"HEAD \", \"OPTIONS \", \"CONNECT \", \"TRACE \",\n    ];\n    prefixes.iter().any(|p| line.starts_with(p))\n}\n\nasync fn shutdown_signal() {\n    #[cfg(unix)]\n    {\n        let mut sigint = match signal::unix::signal(signal::unix::SignalKind::interrupt()) {\n            Ok(s) => s,\n            Err(e) => {\n                let _ = writeln!(std::io::stderr(), \"Failed to install SIGINT handler: {}\", e);\n                process::exit(1);\n            }\n        };\n        let mut sigterm = match signal::unix::signal(signal::unix::SignalKind::terminate()) {\n            Ok(s) => s,\n            Err(e) => {\n                let _ = writeln!(\n                    std::io::stderr(),\n                    \"Failed to install SIGTERM handler: {}\",\n                    e\n                );\n                process::exit(1);\n            }\n        };\n        let mut sighup = match signal::unix::signal(signal::unix::SignalKind::hangup()) {\n            Ok(s) => s,\n            Err(e) => {\n                let _ = writeln!(std::io::stderr(), \"Failed to install SIGHUP handler: {}\", e);\n                process::exit(1);\n            }\n        };\n\n        tokio::select! {\n            _ = sigint.recv() => {}\n            _ = sigterm.recv() => {}\n            _ = sighup.recv() => {}\n        }\n    }\n\n    #[cfg(windows)]\n    {\n        if let Err(e) = signal::ctrl_c().await {\n            let _ = writeln!(std::io::stderr(), \"Failed to install Ctrl+C handler: {}\", e);\n            process::exit(1);\n        }\n    }\n}\n\nfn get_daemon_socket_dir() -> PathBuf {\n    if let Ok(dir) = env::var(\"AGENT_BROWSER_SOCKET_DIR\") {\n        if !dir.is_empty() {\n            return PathBuf::from(dir);\n        }\n    }\n\n    if let Ok(xdg) = env::var(\"XDG_RUNTIME_DIR\") {\n        if !xdg.is_empty() {\n            return PathBuf::from(xdg).join(\"agent-browser\");\n        }\n    }\n\n    if let Some(home) = dirs::home_dir() {\n        return home.join(\".agent-browser\");\n    }\n\n    std::env::temp_dir().join(\"agent-browser\")\n}\n\n#[cfg(windows)]\nfn get_port_for_session(session: &str) -> u16 {\n    let mut hash: i32 = 0;\n    for c in session.chars() {\n        hash = ((hash << 5).wrapping_sub(hash)).wrapping_add(c as i32);\n    }\n    49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)\n}\n\n#[cfg(test)]\n#[cfg(windows)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_port_matches_client_algorithm() {\n        // These values are computed by the identical djb2 implementation in\n        // connection.rs. Both sides must agree on the port for the daemon to\n        // start successfully.\n        assert_eq!(get_port_for_session(\"default\"), 50838);\n        assert_eq!(get_port_for_session(\"my-session\"), 63105);\n        assert_eq!(get_port_for_session(\"work\"), 51184);\n        assert_eq!(get_port_for_session(\"\"), 49152);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/diff.rs",
    "content": "use serde_json::{json, Value};\nuse similar::{ChangeTag, TextDiff};\n\npub struct ScreenshotDiffResult {\n    pub total_pixels: u64,\n    pub different_pixels: u64,\n    pub mismatch_percentage: f64,\n    pub matched: bool,\n    pub diff_image: Option<Vec<u8>>,\n    pub dimension_mismatch: Option<Value>,\n}\n\npub struct SnapshotDiffResult {\n    pub diff: String,\n    pub additions: usize,\n    pub removals: usize,\n    pub unchanged: usize,\n    pub changed: bool,\n}\n\npub fn diff_screenshot(\n    baseline: &[u8],\n    current: &[u8],\n    threshold: f64,\n) -> Result<ScreenshotDiffResult, String> {\n    let img_a = image::load_from_memory(baseline)\n        .map_err(|e| format!(\"Failed to decode baseline image: {}\", e))?;\n    let img_b = image::load_from_memory(current)\n        .map_err(|e| format!(\"Failed to decode current image: {}\", e))?;\n\n    let (wa, ha) = (img_a.width(), img_a.height());\n    let (wb, hb) = (img_b.width(), img_b.height());\n\n    if wa != wb || ha != hb {\n        return Ok(ScreenshotDiffResult {\n            total_pixels: (wa as u64) * (ha as u64),\n            different_pixels: (wa as u64) * (ha as u64),\n            mismatch_percentage: 100.0,\n            matched: false,\n            diff_image: None,\n            dimension_mismatch: Some(json!({\n                \"expected\": { \"width\": wa, \"height\": ha },\n                \"actual\": { \"width\": wb, \"height\": hb },\n            })),\n        });\n    }\n\n    let rgba_a = img_a.to_rgba8();\n    let rgba_b = img_b.to_rgba8();\n    let total = (wa as u64) * (ha as u64);\n    let max_color_distance = threshold * 255.0 * (3.0_f64).sqrt();\n    let mut different = 0u64;\n\n    let mut diff_img = image::RgbaImage::new(wa, ha);\n\n    for y in 0..ha {\n        for x in 0..wa {\n            let pa = rgba_a.get_pixel(x, y);\n            let pb = rgba_b.get_pixel(x, y);\n            let dr = (pa[0] as f64) - (pb[0] as f64);\n            let dg = (pa[1] as f64) - (pb[1] as f64);\n            let db = (pa[2] as f64) - (pb[2] as f64);\n            let dist = (dr * dr + dg * dg + db * db).sqrt();\n\n            if dist > max_color_distance {\n                different += 1;\n                diff_img.put_pixel(x, y, image::Rgba([255, 0, 0, 255]));\n            } else {\n                let gray = ((pa[0] as u16 + pa[1] as u16 + pa[2] as u16) / 3) as u8;\n                let dimmed = (gray as f64 * 0.3) as u8;\n                diff_img.put_pixel(x, y, image::Rgba([dimmed, dimmed, dimmed, 255]));\n            }\n        }\n    }\n\n    let mismatch = if total > 0 {\n        (different as f64 / total as f64) * 100.0\n    } else {\n        0.0\n    };\n\n    let diff_bytes = if different > 0 {\n        let mut buf = std::io::Cursor::new(Vec::new());\n        diff_img\n            .write_to(&mut buf, image::ImageFormat::Png)\n            .map_err(|e| format!(\"Failed to encode diff image: {}\", e))?;\n        Some(buf.into_inner())\n    } else {\n        None\n    };\n\n    Ok(ScreenshotDiffResult {\n        total_pixels: total,\n        different_pixels: different,\n        mismatch_percentage: mismatch,\n        matched: different == 0,\n        diff_image: diff_bytes,\n        dimension_mismatch: None,\n    })\n}\n\n/// Compute a snapshot diff using the Myers algorithm via the `similar` crate.\npub fn diff_snapshots(before: &str, after: &str) -> SnapshotDiffResult {\n    // Fast path: identical inputs.\n    // This avoids constructing the `similar` TextDiff object and running the diff\n    // iteration when agents compare a snapshot to itself (common in retry/loop\n    // workloads).\n    if before == after {\n        let unchanged = before.lines().count();\n        return SnapshotDiffResult {\n            diff: String::new(),\n            additions: 0,\n            removals: 0,\n            unchanged,\n            changed: false,\n        };\n    }\n\n    let text_diff = TextDiff::from_lines(before, after);\n\n    let mut additions = 0usize;\n    let mut removals = 0usize;\n    let mut unchanged = 0usize;\n\n    for change in text_diff.iter_all_changes() {\n        match change.tag() {\n            ChangeTag::Insert => additions += 1,\n            ChangeTag::Delete => removals += 1,\n            ChangeTag::Equal => unchanged += 1,\n        }\n    }\n\n    let changed = additions > 0 || removals > 0;\n\n    let diff = text_diff\n        .unified_diff()\n        .context_radius(3)\n        .header(\"before\", \"after\")\n        .to_string();\n\n    SnapshotDiffResult {\n        diff,\n        additions,\n        removals,\n        unchanged,\n        changed,\n    }\n}\n\n/// Legacy JSON diff output for backwards compatibility.\npub fn diff_text(a: &str, b: &str) -> Value {\n    let result = diff_snapshots(a, b);\n    json!({\n        \"identical\": !result.changed,\n        \"additions\": result.additions,\n        \"removals\": result.removals,\n        \"deletions\": result.removals,\n        \"unchanged\": result.unchanged,\n        \"changed\": result.changed,\n    })\n}\n\npub fn diff_unified(a: &str, b: &str) -> String {\n    diff_snapshots(a, b).diff\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_diff_identical() {\n        let result = diff_text(\"hello\\nworld\", \"hello\\nworld\");\n        assert_eq!(result.get(\"identical\").unwrap(), true);\n        assert_eq!(result.get(\"changed\").unwrap(), false);\n        assert_eq!(result.get(\"unchanged\").unwrap(), 2);\n    }\n\n    #[test]\n    fn test_diff_additions() {\n        let result = diff_text(\"hello\\n\", \"hello\\nworld\\n\");\n        assert_eq!(result.get(\"identical\").unwrap(), false);\n        assert_eq!(result.get(\"changed\").unwrap(), true);\n        assert!(result.get(\"additions\").unwrap().as_i64().unwrap() > 0);\n    }\n\n    #[test]\n    fn test_diff_deletions() {\n        let result = diff_text(\"hello\\nworld\\n\", \"hello\\n\");\n        assert_eq!(result.get(\"identical\").unwrap(), false);\n        assert!(result.get(\"removals\").unwrap().as_i64().unwrap() > 0);\n    }\n\n    #[test]\n    fn test_diff_unified_output() {\n        let output = diff_unified(\"a\\nb\\nc\\n\", \"a\\nx\\nc\\n\");\n        assert!(output.contains(\"---\"));\n        assert!(output.contains(\"+++\"));\n    }\n\n    #[test]\n    fn test_snapshot_diff_struct() {\n        let result = diff_snapshots(\"line1\\nline2\\n\", \"line1\\nline3\\n\");\n        assert!(result.changed);\n        assert_eq!(result.additions, 1);\n        assert_eq!(result.removals, 1);\n        assert_eq!(result.unchanged, 1);\n        assert!(!result.diff.is_empty());\n    }\n\n    #[test]\n    fn test_diff_snapshots_identical_fast_path() {\n        let input = \"hello\\nworld\\n\";\n        let result = diff_snapshots(input, input);\n        assert!(!result.changed);\n        assert_eq!(result.additions, 0);\n        assert_eq!(result.removals, 0);\n        assert_eq!(result.unchanged, input.lines().count());\n        assert!(result.diff.is_empty());\n    }\n\n    #[test]\n    #[ignore]\n    fn bench_diff_snapshots_identical_and_changed() {\n        use std::hint::black_box;\n        use std::time::Instant;\n\n        let identical_a = (0..200)\n            .map(|i| format!(\"line {i}\"))\n            .collect::<Vec<_>>()\n            .join(\"\\n\");\n        let identical_b = identical_a.clone();\n\n        let changed_a = identical_a.clone();\n        let changed_b = (0..200)\n            .map(|i| {\n                if i == 123 {\n                    format!(\"line {i} changed\")\n                } else {\n                    format!(\"line {i}\")\n                }\n            })\n            .collect::<Vec<_>>()\n            .join(\"\\n\");\n\n        // Keep the iteration count high enough to measure, but low enough\n        // to avoid long CI times when someone runs `--ignored`.\n        let iters = 50_000usize;\n\n        let start = Instant::now();\n        let mut acc_changed = 0usize;\n        for _ in 0..iters {\n            let r = diff_snapshots(black_box(&identical_a), black_box(&identical_b));\n            acc_changed ^= r.unchanged;\n        }\n        let identical_ms = start.elapsed().as_secs_f64() * 1000.0;\n\n        let start = Instant::now();\n        let mut acc_changed2 = 0usize;\n        for _ in 0..iters {\n            let r = diff_snapshots(black_box(&changed_a), black_box(&changed_b));\n            acc_changed2 ^= r.additions;\n        }\n        let changed_ms = start.elapsed().as_secs_f64() * 1000.0;\n\n        // Prevent the compiler from optimizing everything away.\n        black_box(acc_changed);\n        black_box(acc_changed2);\n\n        println!(\n            \"bench_diff_snapshots_identical_and_changed: iters={iters} identical_ms={identical_ms:.2} changed_ms={changed_ms:.2}\"\n        );\n    }\n}\n"
  },
  {
    "path": "cli/src/native/e2e_tests.rs",
    "content": "//! End-to-end tests for the native daemon.\n//!\n//! These tests launch a real Chrome instance and exercise the full command\n//! pipeline. They require Chrome to be installed and are marked `#[ignore]`\n//! so they don't run during normal `cargo test`.\n//!\n//! Run serially to avoid Chrome instance contention:\n//!   cargo test e2e -- --ignored --test-threads=1\n\nuse base64::{engine::general_purpose::STANDARD, Engine};\nuse serde_json::{json, Value};\nuse tokio::io::{AsyncReadExt, AsyncWriteExt};\n\nuse super::actions::{execute_command, DaemonState};\n\nfn assert_success(resp: &Value) {\n    assert_eq!(\n        resp.get(\"success\").and_then(|v| v.as_bool()),\n        Some(true),\n        \"Expected success but got: {}\",\n        serde_json::to_string_pretty(resp).unwrap_or_default()\n    );\n}\n\nfn get_data(resp: &Value) -> &Value {\n    resp.get(\"data\").expect(\"Missing 'data' in response\")\n}\n\nfn native_test_fixture_html(name: &str) -> &'static str {\n    match name {\n        \"drag_probe\" => include_str!(\"test_fixtures/drag_probe.html\"),\n        \"html5_drag_probe\" => include_str!(\"test_fixtures/html5_drag_probe.html\"),\n        \"pointer_capture_probe\" => include_str!(\"test_fixtures/pointer_capture_probe.html\"),\n        _ => panic!(\"Unknown native test fixture: {}\", name),\n    }\n}\n\nfn native_test_fixture_url(name: &str) -> String {\n    format!(\n        \"data:text/html;base64,{}\",\n        STANDARD.encode(native_test_fixture_html(name))\n    )\n}\n\n// ---------------------------------------------------------------------------\n// Core: launch, navigate, evaluate, url, title, close\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_launch_navigate_evaluate_close() {\n    let mut state = DaemonState::new();\n\n    // Launch headless Chrome\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"launched\"], true);\n\n    // Navigate to example.com\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"url\"], \"https://example.com/\");\n    assert_eq!(get_data(&resp)[\"title\"], \"Example Domain\");\n\n    // Get URL\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"url\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"url\"], \"https://example.com/\");\n\n    // Get title\n    let resp = execute_command(&json!({ \"id\": \"4\", \"action\": \"title\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"title\"], \"Example Domain\");\n\n    // Evaluate JS\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"evaluate\", \"script\": \"1 + 2\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], 3);\n\n    // Evaluate document.title\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"evaluate\", \"script\": \"document.title\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"Example Domain\");\n\n    // Close\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"closed\"], true);\n}\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_lightpanda_launch_can_open_page() {\n    let lightpanda_bin = match std::env::var(\"LIGHTPANDA_BIN\") {\n        Ok(path) if !path.is_empty() => path,\n        _ => return,\n    };\n\n    let mut state = DaemonState::new();\n\n    let resp = tokio::time::timeout(\n        tokio::time::Duration::from_secs(20),\n        execute_command(\n            &json!({\n                \"id\": \"1\",\n                \"action\": \"launch\",\n                \"headless\": true,\n                \"engine\": \"lightpanda\",\n                \"executablePath\": lightpanda_bin,\n            }),\n            &mut state,\n        ),\n    )\n    .await\n    .expect(\"Lightpanda launch should not hang\");\n\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"launched\"], true);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"url\"], \"https://example.com/\");\n    assert_eq!(get_data(&resp)[\"title\"], \"Example Domain\");\n\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"closed\"], true);\n}\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_lightpanda_auto_launch_can_open_page() {\n    let lightpanda_bin = match std::env::var(\"LIGHTPANDA_BIN\") {\n        Ok(path) if !path.is_empty() => path,\n        _ => return,\n    };\n\n    let prev_engine = std::env::var(\"AGENT_BROWSER_ENGINE\").ok();\n    let prev_path = std::env::var(\"AGENT_BROWSER_EXECUTABLE_PATH\").ok();\n    std::env::set_var(\"AGENT_BROWSER_ENGINE\", \"lightpanda\");\n    std::env::set_var(\"AGENT_BROWSER_EXECUTABLE_PATH\", &lightpanda_bin);\n\n    let mut state = DaemonState::new();\n\n    let resp = tokio::time::timeout(\n        tokio::time::Duration::from_secs(20),\n        execute_command(\n            &json!({ \"id\": \"1\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n            &mut state,\n        ),\n    )\n    .await\n    .expect(\"Lightpanda auto-launch should not hang\");\n\n    match prev_engine {\n        Some(value) => std::env::set_var(\"AGENT_BROWSER_ENGINE\", value),\n        None => std::env::remove_var(\"AGENT_BROWSER_ENGINE\"),\n    }\n    match prev_path {\n        Some(value) => std::env::set_var(\"AGENT_BROWSER_EXECUTABLE_PATH\", value),\n        None => std::env::remove_var(\"AGENT_BROWSER_EXECUTABLE_PATH\"),\n    }\n\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"url\"], \"https://example.com/\");\n    assert_eq!(get_data(&resp)[\"title\"], \"Example Domain\");\n\n    let resp = execute_command(&json!({ \"id\": \"2\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"closed\"], true);\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot with refs and ref-based click\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_snapshot_and_click_ref() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Take snapshot\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"snapshot\" }), &mut state).await;\n    assert_success(&resp);\n    let snapshot = get_data(&resp)[\"snapshot\"].as_str().unwrap();\n    assert!(\n        snapshot.contains(\"Example Domain\"),\n        \"Snapshot should contain heading\"\n    );\n    assert!(snapshot.contains(\"ref=e1\"), \"Snapshot should have ref e1\");\n    assert!(snapshot.contains(\"ref=e2\"), \"Snapshot should have ref e2\");\n    assert!(\n        snapshot.contains(\"link\"),\n        \"Snapshot should have a link element\"\n    );\n\n    // Click the link by ref (e2 is the \"More information...\" link)\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"click\", \"selector\": \"e2\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Wait for navigation\n    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;\n\n    // Verify URL changed\n    let resp = execute_command(&json!({ \"id\": \"5\", \"action\": \"url\" }), &mut state).await;\n    assert_success(&resp);\n    let url = get_data(&resp)[\"url\"].as_str().unwrap();\n    assert!(\n        url.contains(\"iana.org\"),\n        \"Should have navigated to iana.org, got: {}\",\n        url\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Screenshot\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_screenshot() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Default screenshot\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"screenshot\" }), &mut state).await;\n    assert_success(&resp);\n    let path = get_data(&resp)[\"path\"].as_str().unwrap();\n    assert!(path.ends_with(\".png\"), \"Screenshot path should be .png\");\n    let metadata = std::fs::metadata(path).expect(\"Screenshot file should exist\");\n    assert!(\n        metadata.len() > 1000,\n        \"Screenshot should be non-trivial size\"\n    );\n\n    // Named screenshot\n    let tmp_path = std::env::temp_dir()\n        .join(\"agent-browser-e2e-test-screenshot.png\")\n        .to_string_lossy()\n        .to_string();\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"screenshot\", \"path\": tmp_path }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert!(std::path::Path::new(&tmp_path).exists());\n    let _ = std::fs::remove_file(&tmp_path);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"5\",\n            \"action\": \"setcontent\",\n            \"html\": r##\"\n                <html><body>\n                  <button onclick=\"document.getElementById('result').textContent = 'clicked'\">Submit</button>\n                  <a href=\"#\">Home</a>\n                  <div id=\"result\"></div>\n                </body></html>\n            \"##,\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"screenshot\", \"annotate\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let annotations = get_data(&resp)[\"annotations\"]\n        .as_array()\n        .expect(\"Annotated screenshot should return annotations\");\n    assert!(\n        !annotations.is_empty(),\n        \"Annotated screenshot should have at least one annotation\"\n    );\n\n    let submit_ref = annotations\n        .iter()\n        .find(|ann| ann.get(\"name\").and_then(|v| v.as_str()) == Some(\"Submit\"))\n        .and_then(|ann| ann.get(\"ref\").and_then(|v| v.as_str()))\n        .expect(\"Expected a Submit annotation\");\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"7\",\n            \"action\": \"evaluate\",\n            \"script\": \"document.getElementById('__agent_browser_annotations__') === null\"\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], true);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"8\", \"action\": \"click\", \"selector\": format!(\"@{}\", submit_ref) }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"9\",\n            \"action\": \"evaluate\",\n            \"script\": \"document.getElementById('result').textContent\"\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"clicked\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Form interaction: fill, type, select, check\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_form_interaction() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let html = concat!(\n        \"data:text/html,<html><body>\",\n        \"<input id='name' type='text' placeholder='Name'>\",\n        \"<input id='email' type='email'>\",\n        \"<select id='color'><option value='red'>Red</option><option value='blue'>Blue</option></select>\",\n        \"<input id='agree' type='checkbox'>\",\n        \"<textarea id='bio'></textarea>\",\n        \"<button id='submit'>Submit</button>\",\n        \"</body></html>\"\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Fill name\n    let resp = execute_command(\n        &json!({ \"id\": \"10\", \"action\": \"fill\", \"selector\": \"#name\", \"value\": \"John Doe\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Verify fill\n    let resp = execute_command(\n        &json!({ \"id\": \"11\", \"action\": \"evaluate\", \"script\": \"document.getElementById('name').value\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"John Doe\");\n\n    // Type email – the type action now correctly handles punctuation like '.'\n    let resp = execute_command(\n        &json!({ \"id\": \"12\", \"action\": \"type\", \"selector\": \"#email\", \"text\": \"john@example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"13\", \"action\": \"evaluate\", \"script\": \"document.getElementById('email').value\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"john@example.com\");\n\n    // Select option\n    let resp = execute_command(\n        &json!({ \"id\": \"14\", \"action\": \"select\", \"selector\": \"#color\", \"values\": [\"blue\"] }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"15\", \"action\": \"evaluate\", \"script\": \"document.getElementById('color').value\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"blue\");\n\n    // Check checkbox\n    let resp = execute_command(\n        &json!({ \"id\": \"16\", \"action\": \"check\", \"selector\": \"#agree\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"17\", \"action\": \"ischecked\", \"selector\": \"#agree\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"checked\"], true);\n\n    // Uncheck\n    let resp = execute_command(\n        &json!({ \"id\": \"18\", \"action\": \"uncheck\", \"selector\": \"#agree\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"19\", \"action\": \"ischecked\", \"selector\": \"#agree\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"checked\"], false);\n\n    // Snapshot should show form state\n    let resp = execute_command(&json!({ \"id\": \"20\", \"action\": \"snapshot\" }), &mut state).await;\n    assert_success(&resp);\n    let snap = get_data(&resp)[\"snapshot\"].as_str().unwrap();\n    assert!(\n        snap.contains(\"John Doe\"),\n        \"Snapshot should show filled value\"\n    );\n    assert!(snap.contains(\"textbox\"), \"Snapshot should show textbox\");\n    assert!(snap.contains(\"button\"), \"Snapshot should show button\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Navigation: back, forward, reload\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_navigation_history() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to page 1\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Page 1</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to page 2\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Page 2</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Back\n    let resp = execute_command(&json!({ \"id\": \"4\", \"action\": \"back\" }), &mut state).await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"evaluate\", \"script\": \"document.querySelector('h1').textContent\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"Page 1\");\n\n    // Forward\n    let resp = execute_command(&json!({ \"id\": \"6\", \"action\": \"forward\" }), &mut state).await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"7\", \"action\": \"evaluate\", \"script\": \"document.querySelector('h1').textContent\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"Page 2\");\n\n    // Reload\n    let resp = execute_command(&json!({ \"id\": \"8\", \"action\": \"reload\" }), &mut state).await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"9\", \"action\": \"evaluate\", \"script\": \"document.querySelector('h1').textContent\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"Page 2\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Cookies\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_cookies() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set cookie\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\",\n            \"action\": \"cookies_set\",\n            \"name\": \"test_cookie\",\n            \"value\": \"hello123\"\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Get cookies\n    let resp = execute_command(&json!({ \"id\": \"4\", \"action\": \"cookies_get\" }), &mut state).await;\n    assert_success(&resp);\n    let cookies = get_data(&resp)[\"cookies\"].as_array().unwrap();\n    let found = cookies\n        .iter()\n        .any(|c| c[\"name\"] == \"test_cookie\" && c[\"value\"] == \"hello123\");\n    assert!(found, \"Should find the set cookie\");\n\n    // Clear cookies\n    let resp = execute_command(&json!({ \"id\": \"5\", \"action\": \"cookies_clear\" }), &mut state).await;\n    assert_success(&resp);\n\n    // Verify cleared\n    let resp = execute_command(&json!({ \"id\": \"6\", \"action\": \"cookies_get\" }), &mut state).await;\n    assert_success(&resp);\n    let cookies = get_data(&resp)[\"cookies\"].as_array().unwrap();\n    let found = cookies.iter().any(|c| c[\"name\"] == \"test_cookie\");\n    assert!(!found, \"Cookie should be cleared\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// localStorage / sessionStorage\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_storage() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set local storage\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"storage_set\", \"type\": \"local\", \"key\": \"mykey\", \"value\": \"myvalue\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Get local storage key\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"storage_get\", \"type\": \"local\", \"key\": \"mykey\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"value\"], \"myvalue\");\n\n    // Get all local storage\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"storage_get\", \"type\": \"local\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"data\"][\"mykey\"], \"myvalue\");\n\n    // Clear\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"storage_clear\", \"type\": \"local\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Verify cleared\n    let resp = execute_command(\n        &json!({ \"id\": \"7\", \"action\": \"storage_get\", \"type\": \"local\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let data = &get_data(&resp)[\"data\"];\n    assert!(\n        data.as_object().map(|m| m.is_empty()).unwrap_or(true),\n        \"Storage should be empty after clear\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Tab management\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_tabs() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Tab 1</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Tab list should show 1 tab\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"tab_list\" }), &mut state).await;\n    assert_success(&resp);\n    let tabs = get_data(&resp)[\"tabs\"].as_array().unwrap();\n    assert_eq!(tabs.len(), 1);\n    assert_eq!(tabs[0][\"active\"], true);\n\n    // Open new tab\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"tab_new\", \"url\": \"data:text/html,<h1>Tab 2</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"index\"], 1);\n\n    // Tab list should show 2 tabs\n    let resp = execute_command(&json!({ \"id\": \"5\", \"action\": \"tab_list\" }), &mut state).await;\n    assert_success(&resp);\n    let tabs = get_data(&resp)[\"tabs\"].as_array().unwrap();\n    assert_eq!(tabs.len(), 2);\n    assert_eq!(tabs[1][\"active\"], true);\n\n    // Switch to first tab\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"tab_switch\", \"index\": 0 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"7\", \"action\": \"evaluate\", \"script\": \"document.querySelector('h1').textContent\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"Tab 1\");\n\n    // Close second tab\n    let resp = execute_command(\n        &json!({ \"id\": \"8\", \"action\": \"tab_close\", \"index\": 1 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Should have 1 tab left\n    let resp = execute_command(&json!({ \"id\": \"9\", \"action\": \"tab_list\" }), &mut state).await;\n    assert_success(&resp);\n    let tabs = get_data(&resp)[\"tabs\"].as_array().unwrap();\n    assert_eq!(tabs.len(), 1);\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Element queries: isvisible, isenabled, gettext, getattribute\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_element_queries() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let html = concat!(\n        \"data:text/html,<html><body>\",\n        \"<p id='visible'>Hello World</p>\",\n        \"<p id='hidden' style='display:none'>Hidden</p>\",\n        \"<input id='enabled' value='test'>\",\n        \"<input id='disabled' disabled value='nope'>\",\n        \"<a id='link' href='https://example.com' data-testid='my-link'>Click me</a>\",\n        \"</body></html>\"\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // isvisible\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"isvisible\", \"selector\": \"#visible\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"visible\"], true);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"isvisible\", \"selector\": \"#hidden\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"visible\"], false);\n\n    // isenabled\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"isenabled\", \"selector\": \"#enabled\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"enabled\"], true);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"isenabled\", \"selector\": \"#disabled\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"enabled\"], false);\n\n    // gettext\n    let resp = execute_command(\n        &json!({ \"id\": \"7\", \"action\": \"gettext\", \"selector\": \"#visible\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"text\"], \"Hello World\");\n\n    // getattribute\n    let resp = execute_command(\n        &json!({ \"id\": \"8\", \"action\": \"getattribute\", \"selector\": \"#link\", \"attribute\": \"href\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"value\"], \"https://example.com\");\n\n    let resp = execute_command(\n        &json!({ \"id\": \"9\", \"action\": \"getattribute\", \"selector\": \"#link\", \"attribute\": \"data-testid\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"value\"], \"my-link\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Wait command\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_wait() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let html = concat!(\n        \"data:text/html,<html><body>\",\n        \"<div id='target' style='display:none'>Appeared!</div>\",\n        \"<script>setTimeout(() => document.getElementById('target').style.display='block', 500)</script>\",\n        \"</body></html>\"\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Wait for selector to become visible\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"wait\", \"selector\": \"#target\", \"state\": \"visible\", \"timeout\": 5000 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Wait for text\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"wait\", \"text\": \"Appeared!\", \"timeout\": 5000 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Timeout wait\n    let start = std::time::Instant::now();\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"wait\", \"timeout\": 200 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert!(\n        start.elapsed().as_millis() >= 150,\n        \"Timeout wait should sleep at least 150ms\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Viewport with deviceScaleFactor (retina)\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_viewport_scale_factor() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"about:blank\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Default devicePixelRatio should be 1\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"evaluate\", \"script\": \"window.devicePixelRatio\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let default_dpr = get_data(&resp)[\"result\"].as_f64().unwrap();\n    assert_eq!(default_dpr, 1.0, \"Default devicePixelRatio should be 1\");\n\n    // Set viewport with 2x scale factor\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"viewport\", \"width\": 1920, \"height\": 1080, \"deviceScaleFactor\": 2.0 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"width\"], 1920);\n    assert_eq!(get_data(&resp)[\"height\"], 1080);\n    assert_eq!(get_data(&resp)[\"deviceScaleFactor\"], 2.0);\n\n    // devicePixelRatio should now be 2\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"evaluate\", \"script\": \"window.devicePixelRatio\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let new_dpr = get_data(&resp)[\"result\"].as_f64().unwrap();\n    assert_eq!(\n        new_dpr, 2.0,\n        \"devicePixelRatio should be 2 after setting scale factor\"\n    );\n\n    // CSS viewport width should still be 1920 (not 3840)\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"evaluate\", \"script\": \"window.innerWidth\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let css_width = get_data(&resp)[\"result\"].as_i64().unwrap();\n    assert_eq!(css_width, 1920, \"CSS width should remain 1920 at 2x scale\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Viewport and emulation\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_viewport_emulation() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Viewport</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Get initial width\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"evaluate\", \"script\": \"window.innerWidth\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let initial_width = get_data(&resp)[\"result\"].as_i64().unwrap();\n\n    // Set viewport to a different size\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"viewport\", \"width\": 375, \"height\": 812, \"deviceScaleFactor\": 3.0, \"mobile\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"width\"], 375);\n    assert_eq!(get_data(&resp)[\"height\"], 812);\n    assert_eq!(get_data(&resp)[\"mobile\"], true);\n\n    // Reload to apply viewport change\n    let resp = execute_command(&json!({ \"id\": \"5\", \"action\": \"reload\" }), &mut state).await;\n    assert_success(&resp);\n\n    // Width should differ from default (setDeviceMetricsOverride applied)\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"evaluate\", \"script\": \"window.innerWidth\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let new_width = get_data(&resp)[\"result\"].as_i64().unwrap();\n    assert!(\n        new_width != initial_width || new_width == 375,\n        \"Viewport should change from {} after setDeviceMetricsOverride (got {})\",\n        initial_width,\n        new_width\n    );\n\n    // Set user agent\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"user_agent\", \"userAgent\": \"TestBot/1.0\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"evaluate\", \"script\": \"navigator.userAgent\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"TestBot/1.0\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Hover, scroll, press\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_hover_scroll_press() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let html = concat!(\n        \"data:text/html,<html><body style='height:3000px'>\",\n        \"<button id='btn' onmouseover=\\\"this.textContent='hovered'\\\">Hover me</button>\",\n        \"<input id='input' onkeydown=\\\"this.dataset.key=event.key\\\">\",\n        \"</body></html>\"\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Hover\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"hover\", \"selector\": \"#btn\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Scroll\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"scroll\", \"y\": 500 }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"evaluate\", \"script\": \"window.scrollY\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let scroll_y = get_data(&resp)[\"result\"].as_f64().unwrap();\n    assert!(scroll_y > 0.0, \"Should have scrolled down\");\n\n    // Press key\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"press\", \"key\": \"Enter\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"pressed\"], \"Enter\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Raw mouse regressions\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_mouse_down_move_up_preserves_drag_state() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\",\n            \"action\": \"navigate\",\n            \"url\": native_test_fixture_url(\"drag_probe\")\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\",\n            \"action\": \"evaluate\",\n            \"script\": r#\"(() => {\n                const rect = document.getElementById('target').getBoundingClientRect();\n                return {\n                    left: Math.round(rect.left),\n                    top: Math.round(rect.top),\n                    x: Math.round(rect.left + rect.width / 2),\n                    y: Math.round(rect.top + rect.height / 2)\n                };\n            })()\"#\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let start = &get_data(&resp)[\"result\"];\n    let initial_left = start[\"left\"]\n        .as_i64()\n        .expect(\"target left should be numeric\");\n    let initial_top = start[\"top\"].as_i64().expect(\"target top should be numeric\");\n    let start_x = start[\"x\"].as_i64().expect(\"target x should be numeric\");\n    let start_y = start[\"y\"].as_i64().expect(\"target y should be numeric\");\n    let end_x = start_x + 80;\n    let end_y = start_y + 60;\n\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"mousemove\", \"x\": start_x, \"y\": start_y }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"mousedown\", \"button\": \"left\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"mousemove\", \"x\": end_x, \"y\": end_y }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"7\", \"action\": \"mouseup\", \"button\": \"left\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"8\", \"action\": \"evaluate\", \"script\": \"window.__dragProbe\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let probe = &get_data(&resp)[\"result\"];\n    assert_eq!(probe[\"finalLeft\"].as_i64(), Some(initial_left + 80));\n    assert_eq!(probe[\"finalTop\"].as_i64(), Some(initial_top + 60));\n\n    let events = probe[\"events\"]\n        .as_array()\n        .expect(\"drag probe should expose events\");\n    assert!(\n        events.iter().any(|event| {\n            event[\"type\"] == \"mousedown\"\n                && event[\"x\"].as_f64() == Some(start_x as f64)\n                && event[\"y\"].as_f64() == Some(start_y as f64)\n                && event[\"buttons\"].as_i64() == Some(1)\n        }),\n        \"Expected a non-zero mousedown event in drag probe\"\n    );\n    assert!(\n        events.iter().any(|event| {\n            event[\"type\"] == \"mousemove\"\n                && event[\"x\"].as_f64() == Some(end_x as f64)\n                && event[\"y\"].as_f64() == Some(end_y as f64)\n                && event[\"buttons\"].as_i64() == Some(1)\n        }),\n        \"Expected a drag mousemove with the button still pressed\"\n    );\n    assert!(\n        events.iter().any(|event| {\n            event[\"type\"] == \"mouseup\"\n                && event[\"x\"].as_f64() == Some(end_x as f64)\n                && event[\"y\"].as_f64() == Some(end_y as f64)\n                && event[\"buttons\"].as_i64() == Some(0)\n        }),\n        \"Expected mouseup at the last drag position\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_mouse_drag_reaches_pointer_capture_target() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\",\n            \"action\": \"navigate\",\n            \"url\": native_test_fixture_url(\"pointer_capture_probe\")\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\",\n            \"action\": \"evaluate\",\n            \"script\": r#\"(() => {\n                const rect = document.getElementById('handle').getBoundingClientRect();\n                return {\n                    x: Math.round(rect.left + rect.width / 2),\n                    y: Math.round(rect.top + rect.height / 2)\n                };\n            })()\"#\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let start = &get_data(&resp)[\"result\"];\n    let start_x = start[\"x\"].as_i64().expect(\"handle x should be numeric\");\n    let start_y = start[\"y\"].as_i64().expect(\"handle y should be numeric\");\n    let end_x = start_x + 80;\n    let end_y = start_y + 60;\n\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"mousemove\", \"x\": start_x, \"y\": start_y }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"mousedown\", \"button\": \"left\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"mousemove\", \"x\": end_x, \"y\": end_y }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"7\", \"action\": \"mouseup\", \"button\": \"left\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"8\", \"action\": \"evaluate\", \"script\": \"window.__pointerCaptureProbe\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let probe = &get_data(&resp)[\"result\"];\n    assert_eq!(probe[\"moved\"].as_bool(), Some(true));\n\n    let events = probe[\"events\"]\n        .as_array()\n        .expect(\"pointer capture probe should expose events\");\n    assert!(\n        events.iter().any(|event| {\n            event[\"type\"] == \"pointermove\"\n                && event[\"phase\"] == \"drag\"\n                && event[\"hasCapture\"].as_bool() == Some(true)\n                && event[\"x\"].as_f64() == Some(end_x as f64)\n                && event[\"y\"].as_f64() == Some(end_y as f64)\n        }),\n        \"Expected pointermove with capture during the drag\"\n    );\n    assert!(\n        events.iter().any(|event| {\n            event[\"type\"] == \"pointerup\"\n                && event[\"phase\"] == \"up\"\n                && event[\"hadCapture\"].as_bool() == Some(true)\n        }),\n        \"Expected pointerup to observe an active pointer capture\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// State save/load, state management\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_state_management() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set some storage\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"storage_set\", \"type\": \"local\", \"key\": \"persist_key\", \"value\": \"persist_val\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Save state\n    let tmp_state = std::env::temp_dir()\n        .join(\"agent-browser-e2e-state.json\")\n        .to_string_lossy()\n        .to_string();\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"state_save\", \"path\": &tmp_state }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert!(std::path::Path::new(&tmp_state).exists());\n\n    // State show\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"state_show\", \"path\": &tmp_state }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let state_data = get_data(&resp);\n    assert!(state_data.get(\"state\").is_some());\n\n    // State list\n    let resp = execute_command(&json!({ \"id\": \"6\", \"action\": \"state_list\" }), &mut state).await;\n    assert_success(&resp);\n    assert!(get_data(&resp)[\"files\"].is_array());\n\n    // Clean up\n    let _ = std::fs::remove_file(&tmp_state);\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Domain filter\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_domain_filter() {\n    let mut state = DaemonState::new();\n\n    // Set domain filter BEFORE launch so Fetch.enable is called during\n    // launch and the background fetch handler intercepts from the start.\n    {\n        let mut df = state.domain_filter.write().await;\n        *df = Some(super::network::DomainFilter::new(\"example.com\"));\n    }\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Allowed domain\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Blocked domain\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"navigate\", \"url\": \"https://blocked.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_eq!(resp[\"success\"], false);\n    let error = resp[\"error\"].as_str().unwrap();\n    assert!(\n        error.contains(\"blocked\") || error.contains(\"not allowed\"),\n        \"Should reject blocked domain, got: {}\",\n        error\n    );\n\n    // Verify that in-page fetch to a blocked domain is also blocked by\n    // the Fetch interception layer (not just the navigate-level check).\n    // First navigate to the allowed domain.\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Attempt a cross-origin fetch to a blocked domain from the page.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"5\", \"action\": \"evaluate\",\n            \"script\": \"fetch('https://blocked.com/data').then(() => 'ok').catch(e => 'blocked:' + e.message)\",\n            \"await\": true,\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = get_data(&resp)[\"result\"].as_str().unwrap_or(\"\");\n    assert!(\n        result.starts_with(\"blocked:\"),\n        \"Fetch to blocked domain should fail, got: {}\",\n        result,\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Diff engine\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_diff_snapshot() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Hello</h1><p>World</p>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Take a snapshot and use it as baseline for diff\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"snapshot\" }), &mut state).await;\n    assert_success(&resp);\n    let baseline = get_data(&resp)[\"snapshot\"].as_str().unwrap().to_string();\n\n    // Modify the page\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"evaluate\", \"script\": \"document.querySelector('h1').textContent = 'Changed'\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Diff against baseline\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"diff_snapshot\", \"baseline\": baseline }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let data = get_data(&resp);\n    assert_eq!(data[\"changed\"], true, \"Diff should detect the h1 change\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 8 commands: focus, clear, count, boundingbox, innertext, setvalue\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_phase8_commands() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let html = concat!(\n        \"data:text/html,<html><body>\",\n        \"<input id='a' value='original'>\",\n        \"<input id='b' value='other'>\",\n        \"<p class='item'>One</p>\",\n        \"<p class='item'>Two</p>\",\n        \"<p class='item'>Three</p>\",\n        \"<div id='box' style='width:200px;height:100px;background:red'>Box</div>\",\n        \"</body></html>\"\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Focus\n    let resp = execute_command(\n        &json!({ \"id\": \"10\", \"action\": \"focus\", \"selector\": \"#a\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Clear\n    let resp = execute_command(\n        &json!({ \"id\": \"11\", \"action\": \"clear\", \"selector\": \"#a\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"12\", \"action\": \"evaluate\", \"script\": \"document.getElementById('a').value\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"\");\n\n    // Set value\n    let resp = execute_command(\n        &json!({ \"id\": \"13\", \"action\": \"setvalue\", \"selector\": \"#b\", \"value\": \"new-value\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"14\", \"action\": \"inputvalue\", \"selector\": \"#b\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"value\"], \"new-value\");\n\n    // Count\n    let resp = execute_command(\n        &json!({ \"id\": \"15\", \"action\": \"count\", \"selector\": \".item\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"count\"], 3);\n\n    // Bounding box\n    let resp = execute_command(\n        &json!({ \"id\": \"16\", \"action\": \"boundingbox\", \"selector\": \"#box\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let bbox = get_data(&resp);\n    assert_eq!(bbox[\"width\"], 200.0);\n    assert_eq!(bbox[\"height\"], 100.0);\n    assert!(bbox[\"x\"].as_f64().is_some());\n    assert!(bbox[\"y\"].as_f64().is_some());\n\n    // Inner text\n    let resp = execute_command(\n        &json!({ \"id\": \"17\", \"action\": \"innertext\", \"selector\": \"#box\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"text\"], \"Box\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Auto-launch (tests that commands auto-launch when no browser exists)\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_auto_launch() {\n    let mut state = DaemonState::new();\n\n    // Navigate without explicit launch -- should auto-launch\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Auto</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert!(state.browser.is_some(), \"Browser should be auto-launched\");\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"evaluate\", \"script\": \"document.querySelector('h1').textContent\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"], \"Auto\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Error handling\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_error_handling() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"data:text/html,<h1>Errors</h1>\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Unknown action\n    let resp = execute_command(\n        &json!({ \"id\": \"10\", \"action\": \"nonexistent_action\" }),\n        &mut state,\n    )\n    .await;\n    assert_eq!(resp[\"success\"], false);\n    assert!(resp[\"error\"]\n        .as_str()\n        .unwrap()\n        .contains(\"Not yet implemented\"));\n\n    // Missing required parameter\n    let resp = execute_command(\n        &json!({ \"id\": \"11\", \"action\": \"fill\", \"selector\": \"#x\" }),\n        &mut state,\n    )\n    .await;\n    assert_eq!(resp[\"success\"], false);\n    assert!(resp[\"error\"].as_str().unwrap().contains(\"value\"));\n\n    // Click on non-existent element\n    let resp = execute_command(\n        &json!({ \"id\": \"12\", \"action\": \"click\", \"selector\": \"#does-not-exist\" }),\n        &mut state,\n    )\n    .await;\n    assert_eq!(resp[\"success\"], false);\n\n    // Evaluate syntax error\n    let resp = execute_command(\n        &json!({ \"id\": \"13\", \"action\": \"evaluate\", \"script\": \"}{invalid\" }),\n        &mut state,\n    )\n    .await;\n    assert_eq!(resp[\"success\"], false);\n    assert!(resp[\"error\"].as_str().unwrap().contains(\"error\"));\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Profile cookie persistence across restarts\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_profile_cookie_persistence() {\n    let profile_dir = std::env::temp_dir().join(format!(\n        \"agent-browser-e2e-profile-{}\",\n        uuid::Uuid::new_v4()\n    ));\n\n    // Session 1: launch with profile, set a cookie, close\n    {\n        let mut state = DaemonState::new();\n\n        let resp = execute_command(\n            &json!({\n                \"id\": \"1\",\n                \"action\": \"launch\",\n                \"headless\": true,\n                \"profile\": profile_dir.to_str().unwrap()\n            }),\n            &mut state,\n        )\n        .await;\n        assert_success(&resp);\n\n        let resp = execute_command(\n            &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n            &mut state,\n        )\n        .await;\n        assert_success(&resp);\n\n        let resp = execute_command(\n            &json!({\n                \"id\": \"3\",\n                \"action\": \"cookies_set\",\n                \"name\": \"persist_test\",\n                \"value\": \"should_survive_restart\",\n                \"domain\": \".example.com\",\n                \"path\": \"/\",\n                \"expires\": 2000000000\n            }),\n            &mut state,\n        )\n        .await;\n        assert_success(&resp);\n\n        // Verify cookie is set\n        let resp =\n            execute_command(&json!({ \"id\": \"4\", \"action\": \"cookies_get\" }), &mut state).await;\n        assert_success(&resp);\n        let cookies = get_data(&resp)[\"cookies\"].as_array().unwrap();\n        let found = cookies\n            .iter()\n            .any(|c| c[\"name\"] == \"persist_test\" && c[\"value\"] == \"should_survive_restart\");\n        assert!(found, \"Cookie should exist before close\");\n\n        let resp = execute_command(&json!({ \"id\": \"5\", \"action\": \"close\" }), &mut state).await;\n        assert_success(&resp);\n    }\n\n    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;\n\n    // Session 2: reopen with the same profile, verify cookie persisted\n    {\n        let mut state = DaemonState::new();\n\n        let resp = execute_command(\n            &json!({\n                \"id\": \"10\",\n                \"action\": \"launch\",\n                \"headless\": true,\n                \"profile\": profile_dir.to_str().unwrap()\n            }),\n            &mut state,\n        )\n        .await;\n        assert_success(&resp);\n\n        let resp = execute_command(\n            &json!({ \"id\": \"11\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n            &mut state,\n        )\n        .await;\n        assert_success(&resp);\n\n        let resp =\n            execute_command(&json!({ \"id\": \"12\", \"action\": \"cookies_get\" }), &mut state).await;\n        assert_success(&resp);\n        let cookies = get_data(&resp)[\"cookies\"].as_array().unwrap();\n        let found = cookies\n            .iter()\n            .any(|c| c[\"name\"] == \"persist_test\" && c[\"value\"] == \"should_survive_restart\");\n        assert!(\n            found,\n            \"Cookie should persist across restart with --profile. Cookies found: {:?}\",\n            cookies\n                .iter()\n                .map(|c| c[\"name\"].as_str().unwrap_or(\"?\"))\n                .collect::<Vec<_>>()\n        );\n\n        let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n        assert_success(&resp);\n    }\n\n    let _ = std::fs::remove_dir_all(&profile_dir);\n}\n\n// ---------------------------------------------------------------------------\n// Inspect / CDP URL\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_get_cdp_url() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(&json!({ \"id\": \"2\", \"action\": \"cdp_url\" }), &mut state).await;\n    assert_success(&resp);\n    let cdp_url = get_data(&resp)[\"cdpUrl\"]\n        .as_str()\n        .expect(\"cdpUrl should be a string\");\n    assert!(\n        cdp_url.starts_with(\"ws://\"),\n        \"CDP URL should start with ws://, got: {}\",\n        cdp_url\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_inspect() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": \"https://example.com\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"inspect\" }), &mut state).await;\n    assert_success(&resp);\n    let data = get_data(&resp);\n    assert_eq!(data[\"opened\"], true);\n    let url = data[\"url\"]\n        .as_str()\n        .expect(\"inspect url should be a string\");\n    assert!(\n        url.starts_with(\"http://127.0.0.1:\"),\n        \"Inspect URL should be http://127.0.0.1:<port>, got: {}\",\n        url\n    );\n\n    // Verify the HTTP redirect serves a 302 to the DevTools frontend\n    let http_resp = reqwest::get(url).await;\n    match http_resp {\n        Ok(r) => {\n            let final_url = r.url().to_string();\n            assert!(\n                final_url.contains(\"devtools/devtools_app.html\"),\n                \"Redirect should point to DevTools frontend, got: {}\",\n                final_url\n            );\n        }\n        Err(e) => {\n            panic!(\"HTTP GET to inspect URL failed: {}\", e);\n        }\n    }\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Stale ref fallback (#805): clicking a ref after the DOM has been replaced\n// should fall back to role/name lookup instead of failing.\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_click_stale_ref_falls_back_to_role_name() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to a page with a button that replaces the DOM when clicked.\n    let html = r#\"data:text/html,<body>\n        <div id=\"c\">\n            <button onclick=\"\n                var c = document.getElementById('c');\n                c.innerHTML = '';\n                var b = document.createElement('button');\n                b.textContent = 'Target';\n                b.onclick = function() { document.title = 'clicked'; };\n                c.appendChild(b);\n                document.title = 'replaced';\n            \">Replace</button>\n            <button>Target</button>\n        </div>\n    </body>\"#;\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Snapshot to populate the ref_map with backend_node_ids.\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"snapshot\" }), &mut state).await;\n    assert_success(&resp);\n    let snapshot = get_data(&resp)[\"snapshot\"].as_str().unwrap();\n    assert!(\n        snapshot.contains(\"Replace\"),\n        \"Snapshot should contain Replace button\"\n    );\n    assert!(\n        snapshot.contains(\"Target\"),\n        \"Snapshot should contain Target button\"\n    );\n\n    // Click \"Replace\" — this removes all DOM nodes and recreates them,\n    // making the backend_node_id for \"Target\" stale.\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"click\", \"selector\": \"e1\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;\n\n    // Verify the DOM was actually replaced.\n    let resp = execute_command(&json!({ \"id\": \"5\", \"action\": \"title\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"title\"], \"replaced\");\n\n    // Now click the stale \"Target\" ref. Before the fix this returned:\n    //   \"CDP error (DOM.getBoxModel): Could not compute box model.\"\n    // After the fix it falls back to role/name lookup and succeeds.\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"click\", \"selector\": \"e2\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;\n\n    // Verify the fallback click hit the right (recreated) button.\n    let resp = execute_command(&json!({ \"id\": \"7\", \"action\": \"title\" }), &mut state).await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"title\"],\n        \"clicked\",\n        \"Stale ref should have been resolved via role/name fallback\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Regression: Material Design checkbox/radio (#832)\n//\n// Material Design controls hide the native <input> off-screen and place\n// overlay elements (ripple, touch-target) on top.  Coordinate-based CDP\n// clicks may therefore miss the actual input.  The check/uncheck actions\n// must detect this and fall back to a JS .click() — matching the behaviour\n// that Playwright provided in v0.19.0.\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\n#[ignore]\nasync fn e2e_material_checkbox_check_uncheck() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Inline HTML that reproduces the Material Design DOM pattern:\n    // - Native <input> is visually hidden (position:absolute, opacity:0, off-screen)\n    // - A ripple overlay sits on top with pointer-events:all, intercepting coordinate clicks\n    // - An ARIA-only checkbox uses role=\"checkbox\" + aria-checked (no native input)\n    let html = concat!(\n        \"data:text/html,<html><body>\",\n        // -- Native baseline --\n        \"<input id='native' type='checkbox'>\",\n        // -- Material-style hidden-input checkbox --\n        \"<div id='mat' style='position:relative;padding:12px'>\",\n          \"<input id='mat-input' type='checkbox' style='position:absolute;opacity:0;width:1px;height:1px;top:-9999px;left:-9999px;pointer-events:none'>\",\n          \"<div style='position:absolute;top:0;left:0;width:48px;height:48px;pointer-events:all;z-index:10'></div>\",\n          \"<span>Material CB</span>\",\n        \"</div>\",\n        // -- ARIA-only checkbox (no native input) --\n        \"<div id='aria' role='checkbox' aria-checked='false' tabindex='0'>ARIA CB</div>\",\n        \"<script>\",\n          \"document.getElementById('aria').addEventListener('click',function(){\",\n            \"var c=this.getAttribute('aria-checked')==='true';\",\n            \"this.setAttribute('aria-checked',String(!c));\",\n          \"});\",\n        \"</script>\",\n        \"</body></html>\"\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // ---- Native checkbox (sanity baseline) ----\n    let resp = execute_command(\n        &json!({ \"id\": \"10\", \"action\": \"ischecked\", \"selector\": \"#native\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"checked\"], false);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"11\", \"action\": \"check\", \"selector\": \"#native\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"12\", \"action\": \"ischecked\", \"selector\": \"#native\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"checked\"], true, \"native check failed\");\n\n    // ---- Material checkbox (hidden input + overlay) ----\n    // ischecked on the wrapper should detect the nested hidden input's state\n    let resp = execute_command(\n        &json!({ \"id\": \"20\", \"action\": \"ischecked\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"checked\"], false);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"21\", \"action\": \"check\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"22\", \"action\": \"ischecked\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"checked\"],\n        true,\n        \"Material checkbox should be checked after check action (#832)\"\n    );\n\n    // Idempotency: check again should be a no-op\n    let resp = execute_command(\n        &json!({ \"id\": \"23\", \"action\": \"check\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"24\", \"action\": \"ischecked\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"checked\"],\n        true,\n        \"Material checkbox should stay checked on redundant check\"\n    );\n\n    // Uncheck\n    let resp = execute_command(\n        &json!({ \"id\": \"25\", \"action\": \"uncheck\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"26\", \"action\": \"ischecked\", \"selector\": \"#mat\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"checked\"],\n        false,\n        \"Material checkbox should be unchecked after uncheck action\"\n    );\n\n    // ---- ARIA-only checkbox ----\n    let resp = execute_command(\n        &json!({ \"id\": \"30\", \"action\": \"ischecked\", \"selector\": \"#aria\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"checked\"], false);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"31\", \"action\": \"check\", \"selector\": \"#aria\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"32\", \"action\": \"ischecked\", \"selector\": \"#aria\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"checked\"],\n        true,\n        \"ARIA checkbox should be checked after check action\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Issue #841 – snapshot -C and screenshot --annotate must not hang over WSS\n// ---------------------------------------------------------------------------\n\n/// Verifies that `snapshot -C` (cursor-interactive mode) detects elements with\n/// cursor:pointer / onclick / tabindex, produces the correct v0.19.0-compatible\n/// output format, deduplicates against the ARIA tree, and completes in bounded\n/// time (no sequential CDP round-trip explosion).\n#[tokio::test]\n#[ignore]\nasync fn e2e_snapshot_cursor_interactive() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Page with:\n    //  - <button> and <a> (standard interactive – ARIA tree, NOT in cursor section)\n    //  - <div cursor:pointer onclick> (clickable – cursor section)\n    //  - <div tabindex=0> (focusable – cursor section)\n    //  - <span cursor:pointer> (clickable – cursor section)\n    //  - <span cursor:pointer> child of <div cursor:pointer> (inherited – skip)\n    let html = concat!(\n        \"<html><body>\",\n        \"<a href='#'>Link</a>\",\n        \"<button>Btn</button>\",\n        \"<div style='cursor:pointer' onclick='x()'>ClickDiv</div>\",\n        \"<div tabindex='0'>FocusDiv</div>\",\n        \"<span style='cursor:pointer'>PointerSpan</span>\",\n        \"<div style='cursor:pointer'><span>InheritChild</span></div>\",\n        \"</body></html>\",\n    );\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"setcontent\", \"html\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // snapshot -i -C: interactive tree + cursor section\n    let start = std::time::Instant::now();\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"snapshot\", \"interactive\": true, \"cursor\": true }),\n        &mut state,\n    )\n    .await;\n    let elapsed = start.elapsed();\n    assert_success(&resp);\n\n    let snapshot = get_data(&resp)[\"snapshot\"].as_str().unwrap();\n\n    // v0.19.0 output format: role + hints\n    assert!(\n        snapshot.contains(\"clickable\") && snapshot.contains(\"[cursor:pointer\"),\n        \"Expected v0.19.0-format cursor output with hints:\\n{}\",\n        snapshot,\n    );\n\n    // Role differentiation: tabindex-only → focusable\n    assert!(\n        snapshot.contains(\"focusable\") && snapshot.contains(\"[tabindex]\"),\n        \"Expected focusable role for tabindex-only element:\\n{}\",\n        snapshot,\n    );\n\n    // Text dedup: \"Link\" and \"Btn\" are in the ARIA tree, so must NOT suffix\n    // with cursor-interactive info. Verify line by line.\n    for line in snapshot.lines() {\n        assert!(\n            !(line.contains(\"\\\"Link\\\"\")\n                && (line.contains(\"clickable\")\n                    || line.contains(\"focusable\")\n                    || line.contains(\"editable\"))),\n            \"Standard <a> element should not have cursor-interactive info:\\n{}\",\n            line\n        );\n        assert!(\n            !(line.contains(\"\\\"Btn\\\"\")\n                && (line.contains(\"clickable\")\n                    || line.contains(\"focusable\")\n                    || line.contains(\"editable\"))),\n            \"Standard <button> element should not have cursor-interactive info:\\n{}\",\n            line\n        );\n    }\n\n    // Must complete quickly (< 5s), not hit the 30s CDP timeout\n    assert!(\n        elapsed.as_secs() < 5,\n        \"snapshot -C took {:?}, expected < 5s (Issue #841 regression)\",\n        elapsed,\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Verifies that `screenshot --annotate` completes in bounded time even with\n/// many interactive elements. Guards against the sequential CDP round-trip\n/// regression that caused hangs over high-latency WSS (Issue #841).\n#[tokio::test]\n#[ignore]\nasync fn e2e_screenshot_annotate_many_elements() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // 50 buttons: old sequential code would do 50×2×200ms ≈ 20s over WSS.\n    let mut html = String::from(\"<html><body>\");\n    for i in 1..=50 {\n        html.push_str(&format!(\"<button>Button {}</button>\", i));\n    }\n    html.push_str(\"</body></html>\");\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"setcontent\", \"html\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let start = std::time::Instant::now();\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"screenshot\", \"annotate\": true }),\n        &mut state,\n    )\n    .await;\n    let elapsed = start.elapsed();\n    assert_success(&resp);\n\n    let annotations = get_data(&resp)[\"annotations\"]\n        .as_array()\n        .expect(\"Annotated screenshot should return annotations\");\n\n    assert!(\n        annotations.len() >= 50,\n        \"Expected at least 50 annotations, got {}\",\n        annotations.len(),\n    );\n\n    // Must complete quickly (< 10s), not hit the 30s CDP timeout\n    assert!(\n        elapsed.as_secs() < 10,\n        \"screenshot --annotate with 50 elements took {:?}, expected < 10s (Issue #841)\",\n        elapsed,\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Verifies `snapshot -C` with many cursor-interactive elements completes in\n/// bounded time. Direct regression test for Issue #841's root cause: N×2\n/// sequential CDP round-trips per cursor-interactive element.\n#[tokio::test]\n#[ignore]\nasync fn e2e_snapshot_cursor_many_elements() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // 100 cursor-interactive divs: old code = 200 sequential CDP calls,\n    // at 200ms WSS latency = 40s timeout. New code must finish in seconds.\n    let mut html = String::from(\"<html><body>\");\n    for i in 1..=100 {\n        html.push_str(&format!(\n            \"<div style='cursor:pointer' onclick='x()'>Item {}</div>\",\n            i,\n        ));\n    }\n    html.push_str(\"</body></html>\");\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"setcontent\", \"html\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let start = std::time::Instant::now();\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"snapshot\", \"interactive\": true, \"cursor\": true }),\n        &mut state,\n    )\n    .await;\n    let elapsed = start.elapsed();\n    assert_success(&resp);\n\n    let snapshot = get_data(&resp)[\"snapshot\"].as_str().unwrap();\n\n    // All 100 items should appear\n    assert!(\n        snapshot.contains(\"Item 1\") && snapshot.contains(\"Item 100\"),\n        \"Expected all 100 cursor-interactive items in output\",\n    );\n\n    // All should have v0.19.0-format hints\n    assert!(\n        snapshot.contains(\"[cursor:pointer, onclick]\"),\n        \"Expected v0.19.0-format hints\",\n    );\n\n    // Must complete quickly\n    assert!(\n        elapsed.as_secs() < 10,\n        \"snapshot -C with 100 cursor elements took {:?}, expected < 10s (Issue #841)\",\n        elapsed,\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Test that InlineTextBox nodes are filtered from snapshot output while preserving\n/// the actual text content from parent elements.\n#[tokio::test]\n#[ignore]\nasync fn e2e_snapshot_inline_text_box_filtered() {\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Simple HTML with text content that would generate InlineTextBox nodes\n    let html =\n        \"data:text/html,<html><body><div><span>Hello</span> <span>World</span></div></body></html>\";\n\n    let resp = execute_command(\n        &json!({ \"id\": \"2\", \"action\": \"navigate\", \"url\": html }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Take snapshot to capture full output and verify InlineTextBox filtering\n    let start = std::time::Instant::now();\n    let resp = execute_command(&json!({ \"id\": \"3\", \"action\": \"snapshot\" }), &mut state).await;\n    assert_success(&resp);\n    let elapsed = start.elapsed();\n\n    let snapshot_output = get_data(&resp)[\"snapshot\"].as_str().unwrap();\n\n    // Verify that InlineTextBox does not appear in the output\n    assert!(\n        !snapshot_output.contains(\"InlineTextBox\"),\n        \"Snapshot output should not contain InlineTextBox: {}\",\n        snapshot_output\n    );\n\n    // Verify that the actual text content is preserved\n    assert!(\n        snapshot_output.contains(\"Hello\"),\n        \"Snapshot should contain 'Hello': {}\",\n        snapshot_output\n    );\n    assert!(\n        snapshot_output.contains(\"World\"),\n        \"Snapshot should contain 'World': {}\",\n        snapshot_output\n    );\n\n    // Must complete quickly\n    assert!(\n        elapsed.as_secs() < 5,\n        \"snapshot with InlineTextBox filtering took {:?}, expected < 5s\",\n        elapsed,\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n// ---------------------------------------------------------------------------\n// Helper: tiny HTTP server that echoes request headers as JSON\n// ---------------------------------------------------------------------------\n\n/// Starts a TCP listener on localhost:0 and spawns a task that accepts\n/// connections, reads the HTTP request, and responds with a JSON body\n/// containing all received request headers. Returns the server's base URL.\nasync fn start_echo_server() -> (String, tokio::task::JoinHandle<()>) {\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:0\").await.unwrap();\n    let port = listener.local_addr().unwrap().port();\n    let base_url = format!(\"http://127.0.0.1:{}\", port);\n\n    let handle = tokio::spawn(async move {\n        // Serve up to 20 requests then exit (enough for all tests).\n        for _ in 0..20 {\n            let Ok((mut stream, _)) = listener.accept().await else {\n                break;\n            };\n            tokio::spawn(async move {\n                let mut buf = vec![0u8; 8192];\n                let n = stream.read(&mut buf).await.unwrap_or(0);\n                let request = String::from_utf8_lossy(&buf[..n]);\n\n                // Parse headers from the HTTP request.\n                let mut headers = serde_json::Map::new();\n                for line in request.lines().skip(1) {\n                    if line.is_empty() {\n                        break;\n                    }\n                    if let Some((key, value)) = line.split_once(\": \") {\n                        headers.insert(key.to_string(), Value::String(value.to_string()));\n                    }\n                }\n\n                let body = serde_json::to_string(&json!({ \"headers\": headers })).unwrap();\n                let response = format!(\n                    \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\n\\\n                     Access-Control-Allow-Origin: *\\r\\nContent-Length: {}\\r\\n\\\n                     Connection: close\\r\\n\\r\\n{}\",\n                    body.len(),\n                    body,\n                );\n                let _ = stream.write_all(response.as_bytes()).await;\n                let _ = stream.flush().await;\n            });\n        }\n    });\n\n    (base_url, handle)\n}\n\n// ---------------------------------------------------------------------------\n// Origin-scoped --headers tests\n// ---------------------------------------------------------------------------\n\n/// Headers passed via --headers on open persist for subsequent same-origin\n/// navigations (the core regression from the Rust rewrite).\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_persist_same_origin_navigation() {\n    let (base_url, _server) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate with --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/first\", base_url),\n            \"headers\": { \"X-Test\": \"scoped\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to the same origin WITHOUT --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/second\", base_url),\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // The page body is the echo JSON. Read it via evaluate.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"4\", \"action\": \"evaluate\",\n            \"script\": \"JSON.parse(document.body.innerText)\",\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = &get_data(&resp)[\"result\"];\n    assert_eq!(\n        result[\"headers\"][\"X-Test\"], \"scoped\",\n        \"X-Test header should persist on same-origin navigation without --headers\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Headers passed via --headers on open persist for in-page fetch/XHR to\n/// the same origin.\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_persist_same_origin_fetch() {\n    let (base_url, _server) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate with --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", base_url),\n            \"headers\": { \"X-Test\": \"fetched\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // In-page fetch to the same origin (relative URL).\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"evaluate\",\n            \"script\": \"fetch('/echo').then(r => r.json())\",\n            \"await\": true,\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = &get_data(&resp)[\"result\"];\n    assert_eq!(\n        result[\"headers\"][\"X-Test\"], \"fetched\",\n        \"X-Test header should be present on in-page fetch to same origin\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Headers set via --headers do NOT leak to a different origin.\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_do_not_leak_cross_origin() {\n    let (server_a, _ha) = start_echo_server().await;\n    let (server_b, _hb) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to server A with --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", server_a),\n            \"headers\": { \"X-Secret\": \"a-only\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to server B (different origin) without --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", server_b),\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"4\", \"action\": \"evaluate\",\n            \"script\": \"JSON.parse(document.body.innerText)\",\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = &get_data(&resp)[\"result\"];\n    assert!(\n        result[\"headers\"].get(\"X-Secret\").is_none(),\n        \"X-Secret header must NOT leak to a different origin, got: {}\",\n        result[\"headers\"],\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// In-page fetch to a cross-origin URL must NOT include the origin-scoped\n/// headers (sub-resource isolation).\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_do_not_leak_cross_origin_fetch() {\n    let (server_a, _ha) = start_echo_server().await;\n    let (server_b, _hb) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate to server A with --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", server_a),\n            \"headers\": { \"X-Secret\": \"a-only\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Fetch from the page to server B (cross-origin sub-resource).\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"evaluate\",\n            \"script\": format!(\"fetch('{}/echo').then(r => r.json())\", server_b),\n            \"await\": true,\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = &get_data(&resp)[\"result\"];\n    assert!(\n        result[\"headers\"].get(\"X-Secret\").is_none(),\n        \"X-Secret header must NOT leak to cross-origin fetch, got: {}\",\n        result[\"headers\"],\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// `set headers` (global headers via the headers action) must not be\n/// regressed — they should persist across navigations without being\n/// cleared by the origin-scoped header logic.\n#[tokio::test]\n#[ignore]\nasync fn e2e_set_headers_not_regressed() {\n    let (base_url, _server) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set global headers via the `headers` action (not --headers on navigate).\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"headers\",\n            \"headers\": { \"X-Global\": \"everywhere\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate — global headers should be present.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", base_url),\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"4\", \"action\": \"evaluate\",\n            \"script\": \"JSON.parse(document.body.innerText)\",\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = &get_data(&resp)[\"result\"];\n    assert_eq!(\n        result[\"headers\"][\"X-Global\"], \"everywhere\",\n        \"Global headers set via `set headers` must persist across navigations\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Multiple origins each get their own independent headers.\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_multiple_origins_independent() {\n    let (server_a, _ha) = start_echo_server().await;\n    let (server_b, _hb) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set headers for origin A.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", server_a),\n            \"headers\": { \"X-From\": \"alpha\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set different headers for origin B.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", server_b),\n            \"headers\": { \"X-From\": \"beta\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Verify B got its own header.\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"evaluate\", \"script\": \"JSON.parse(document.body.innerText)\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"][\"headers\"][\"X-From\"], \"beta\");\n\n    // Navigate back to A — should get A's header, not B's.\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"navigate\", \"url\": format!(\"{}/check\", server_a) }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"evaluate\", \"script\": \"JSON.parse(document.body.innerText)\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"][\"headers\"][\"X-From\"], \"alpha\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Headers persist when navigating away to a different origin and back.\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_persist_after_roundtrip() {\n    let (server_a, _ha) = start_echo_server().await;\n    let (server_b, _hb) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set headers for origin A.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", server_a),\n            \"headers\": { \"X-Persist\": \"roundtrip\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate away to B (no headers).\n    let resp = execute_command(\n        &json!({ \"id\": \"3\", \"action\": \"navigate\", \"url\": format!(\"{}/page\", server_b) }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Navigate back to A without --headers.\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"navigate\", \"url\": format!(\"{}/back\", server_a) }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"evaluate\", \"script\": \"JSON.parse(document.body.innerText)\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"result\"][\"headers\"][\"X-Persist\"],\n        \"roundtrip\",\n        \"Headers should persist after navigating away and back to the same origin\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Passing --headers a second time to the same origin replaces the previous headers.\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_override_same_origin() {\n    let (base_url, _server) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set initial headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/first\", base_url),\n            \"headers\": { \"X-Version\": \"v1\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Override with new headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/second\", base_url),\n            \"headers\": { \"X-Version\": \"v2\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"evaluate\", \"script\": \"JSON.parse(document.body.innerText)\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(\n        get_data(&resp)[\"result\"][\"headers\"][\"X-Version\"],\n        \"v2\",\n        \"Second --headers should replace the first for the same origin\"\n    );\n\n    // Subsequent navigation without --headers should use v2.\n    let resp = execute_command(\n        &json!({ \"id\": \"5\", \"action\": \"navigate\", \"url\": format!(\"{}/third\", base_url) }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"6\", \"action\": \"evaluate\", \"script\": \"JSON.parse(document.body.innerText)\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    assert_eq!(get_data(&resp)[\"result\"][\"headers\"][\"X-Version\"], \"v2\");\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// `set headers` (global) and `--headers` (origin-scoped) stack together.\n#[tokio::test]\n#[ignore]\nasync fn e2e_global_and_scoped_headers_stack() {\n    let (base_url, _server) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set global headers via `set headers`.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"headers\",\n            \"headers\": { \"X-Global\": \"everywhere\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Set origin-scoped headers via --headers.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", base_url),\n            \"headers\": { \"X-Scoped\": \"this-origin\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({ \"id\": \"4\", \"action\": \"evaluate\", \"script\": \"JSON.parse(document.body.innerText)\" }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let headers = &get_data(&resp)[\"result\"][\"headers\"];\n    assert_eq!(\n        headers[\"X-Global\"], \"everywhere\",\n        \"Global header should be present alongside scoped header\"\n    );\n    assert_eq!(\n        headers[\"X-Scoped\"], \"this-origin\",\n        \"Scoped header should be present alongside global header\"\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n\n/// Origin-scoped headers with different casing than the browser's original\n/// request headers must not produce duplicates (HTTP headers are\n/// case-insensitive per RFC 7230).\n#[tokio::test]\n#[ignore]\nasync fn e2e_headers_case_insensitive_no_duplicates() {\n    let (base_url, _server) = start_echo_server().await;\n    let mut state = DaemonState::new();\n\n    let resp = execute_command(\n        &json!({ \"id\": \"1\", \"action\": \"launch\", \"headless\": true }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    // Chrome sends \"Accept: ...\" by default on navigations. Pass \"accept\"\n    // (lowercase) via --headers to verify the merge is case-insensitive\n    // and doesn't produce a duplicate Accept header.\n    let resp = execute_command(\n        &json!({\n            \"id\": \"2\", \"action\": \"navigate\",\n            \"url\": format!(\"{}/page\", base_url),\n            \"headers\": { \"accept\": \"application/test\" },\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n\n    let resp = execute_command(\n        &json!({\n            \"id\": \"3\", \"action\": \"evaluate\",\n            \"script\": \"JSON.parse(document.body.innerText)\",\n        }),\n        &mut state,\n    )\n    .await;\n    assert_success(&resp);\n    let result = &get_data(&resp)[\"result\"][\"headers\"];\n\n    // The echo server stores headers keyed by name as received on the wire.\n    // If deduplication works, only our custom \"accept\" value should appear\n    // (Chrome's original \"Accept: text/html,...\" should be suppressed).\n    let accept_val = result\n        .get(\"accept\")\n        .or_else(|| result.get(\"Accept\"))\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"\");\n    assert_eq!(\n        accept_val, \"application/test\",\n        \"Case-insensitive merge should replace Chrome's Accept header, got headers: {}\",\n        result,\n    );\n\n    let resp = execute_command(&json!({ \"id\": \"99\", \"action\": \"close\" }), &mut state).await;\n    assert_success(&resp);\n}\n"
  },
  {
    "path": "cli/src/native/element.rs",
    "content": "use std::collections::HashMap;\n\nuse serde_json::Value;\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::*;\n\n#[derive(Debug, Clone)]\npub struct RefEntry {\n    pub backend_node_id: Option<i64>,\n    pub role: String,\n    pub name: String,\n    pub nth: Option<usize>,\n    pub selector: Option<String>,\n    pub frame_id: Option<String>,\n}\n\npub struct RefMap {\n    map: HashMap<String, RefEntry>,\n    next_ref: usize,\n}\n\nimpl RefMap {\n    pub fn new() -> Self {\n        Self {\n            map: HashMap::new(),\n            next_ref: 1,\n        }\n    }\n\n    pub fn add(\n        &mut self,\n        ref_id: String,\n        backend_node_id: Option<i64>,\n        role: &str,\n        name: &str,\n        nth: Option<usize>,\n    ) {\n        self.add_with_frame(ref_id, backend_node_id, role, name, nth, None);\n    }\n\n    pub fn add_with_frame(\n        &mut self,\n        ref_id: String,\n        backend_node_id: Option<i64>,\n        role: &str,\n        name: &str,\n        nth: Option<usize>,\n        frame_id: Option<&str>,\n    ) {\n        self.map.insert(\n            ref_id,\n            RefEntry {\n                backend_node_id,\n                role: role.to_string(),\n                name: name.to_string(),\n                nth,\n                selector: None,\n                frame_id: frame_id.map(|s| s.to_string()),\n            },\n        );\n    }\n\n    pub fn add_selector(\n        &mut self,\n        ref_id: String,\n        selector: String,\n        role: &str,\n        name: &str,\n        nth: Option<usize>,\n    ) {\n        self.map.insert(\n            ref_id,\n            RefEntry {\n                backend_node_id: None,\n                role: role.to_string(),\n                name: name.to_string(),\n                nth,\n                selector: Some(selector),\n                frame_id: None,\n            },\n        );\n    }\n\n    pub fn get(&self, ref_id: &str) -> Option<&RefEntry> {\n        self.map.get(ref_id)\n    }\n\n    pub fn entries_sorted(&self) -> Vec<(String, RefEntry)> {\n        let mut entries = self\n            .map\n            .iter()\n            .map(|(ref_id, entry)| (ref_id.clone(), entry.clone()))\n            .collect::<Vec<_>>();\n\n        entries.sort_by_key(|(ref_id, _)| {\n            ref_id\n                .strip_prefix('e')\n                .and_then(|n| n.parse::<usize>().ok())\n                .unwrap_or(usize::MAX)\n        });\n\n        entries\n    }\n\n    pub fn clear(&mut self) {\n        self.map.clear();\n        self.next_ref = 1;\n    }\n\n    pub fn next_ref_num(&self) -> usize {\n        self.next_ref\n    }\n\n    pub fn set_next_ref_num(&mut self, n: usize) {\n        self.next_ref = n;\n    }\n}\n\npub fn parse_ref(input: &str) -> Option<String> {\n    let trimmed = input.trim();\n\n    if let Some(stripped) = trimmed.strip_prefix('@') {\n        if stripped.starts_with('e') && stripped[1..].chars().all(|c| c.is_ascii_digit()) {\n            return Some(stripped.to_string());\n        }\n    }\n\n    if let Some(stripped) = trimmed.strip_prefix(\"ref=\") {\n        if stripped.starts_with('e') && stripped[1..].chars().all(|c| c.is_ascii_digit()) {\n            return Some(stripped.to_string());\n        }\n    }\n\n    if trimmed.starts_with('e')\n        && trimmed.len() > 1\n        && trimmed[1..].chars().all(|c| c.is_ascii_digit())\n    {\n        return Some(trimmed.to_string());\n    }\n\n    None\n}\n\npub async fn resolve_element_center(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(f64, f64), String> {\n    if let Some(ref_id) = parse_ref(selector_or_ref) {\n        let entry = ref_map\n            .get(&ref_id)\n            .ok_or_else(|| format!(\"Unknown ref: {}\", ref_id))?;\n\n        // Try cached backend_node_id first (fast path)\n        if let Some(backend_node_id) = entry.backend_node_id {\n            let result: Result<DomGetBoxModelResult, String> = client\n                .send_command_typed(\n                    \"DOM.getBoxModel\",\n                    &DomGetBoxModelParams {\n                        backend_node_id: Some(backend_node_id),\n                        node_id: None,\n                        object_id: None,\n                    },\n                    Some(session_id),\n                )\n                .await;\n\n            if let Ok(r) = result {\n                return Ok(box_model_center(&r.model));\n            }\n            // backend_node_id is stale; re-query the accessibility tree below\n        }\n\n        // Fallback: re-query the accessibility tree to find a fresh node by role/name\n        let ref_frame_id = entry.frame_id.clone();\n        let fresh_id = find_node_id_by_role_name(\n            client,\n            session_id,\n            &entry.role,\n            &entry.name,\n            entry.nth,\n            ref_frame_id.as_deref(),\n        )\n        .await?;\n        let result: DomGetBoxModelResult = client\n            .send_command_typed(\n                \"DOM.getBoxModel\",\n                &DomGetBoxModelParams {\n                    backend_node_id: Some(fresh_id),\n                    node_id: None,\n                    object_id: None,\n                },\n                Some(session_id),\n            )\n            .await?;\n        return Ok(box_model_center(&result.model));\n    }\n\n    // CSS selector\n    resolve_by_selector(client, session_id, selector_or_ref).await\n}\n\npub async fn resolve_element_object_id(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<String, String> {\n    if let Some(ref_id) = parse_ref(selector_or_ref) {\n        let entry = ref_map\n            .get(&ref_id)\n            .ok_or_else(|| format!(\"Unknown ref: {}\", ref_id))?;\n\n        // Try cached backend_node_id first (fast path)\n        if let Some(backend_node_id) = entry.backend_node_id {\n            let result: Result<DomResolveNodeResult, String> = client\n                .send_command_typed(\n                    \"DOM.resolveNode\",\n                    &DomResolveNodeParams {\n                        backend_node_id: Some(backend_node_id),\n                        node_id: None,\n                        object_group: Some(\"agent-browser\".to_string()),\n                    },\n                    Some(session_id),\n                )\n                .await;\n\n            if let Ok(r) = result {\n                if let Some(oid) = r.object.object_id {\n                    return Ok(oid);\n                }\n            }\n            // backend_node_id is stale; re-query the accessibility tree below\n        }\n\n        // Fallback: re-query the accessibility tree to find a fresh node by role/name\n        let ref_frame_id = entry.frame_id.clone();\n        let fresh_id = find_node_id_by_role_name(\n            client,\n            session_id,\n            &entry.role,\n            &entry.name,\n            entry.nth,\n            ref_frame_id.as_deref(),\n        )\n        .await?;\n        let result: DomResolveNodeResult = client\n            .send_command_typed(\n                \"DOM.resolveNode\",\n                &DomResolveNodeParams {\n                    backend_node_id: Some(fresh_id),\n                    node_id: None,\n                    object_group: Some(\"agent-browser\".to_string()),\n                },\n                Some(session_id),\n            )\n            .await?;\n        return result\n            .object\n            .object_id\n            .ok_or_else(|| format!(\"No objectId for ref {}\", ref_id));\n    }\n\n    // Selector fallback (CSS or XPath)\n    let js = build_find_element_js(selector_or_ref);\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: js,\n                return_by_value: Some(false),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    result\n        .result\n        .object_id\n        .ok_or_else(|| format!(\"Element not found: {}\", selector_or_ref))\n}\n\n/// Re-query the accessibility tree to find a node matching role+name+nth,\n/// returning its fresh backendDOMNodeId. This uses the same data source\n/// (Accessibility.getFullAXTree) that built the ref map during snapshot,\n/// so role/name matching is guaranteed to be consistent.\nasync fn find_node_id_by_role_name(\n    client: &CdpClient,\n    session_id: &str,\n    role: &str,\n    name: &str,\n    nth: Option<usize>,\n    frame_id: Option<&str>,\n) -> Result<i64, String> {\n    let ax_params = if let Some(fid) = frame_id {\n        serde_json::json!({ \"frameId\": fid })\n    } else {\n        serde_json::json!({})\n    };\n    let ax_tree: GetFullAXTreeResult = client\n        .send_command_typed(\"Accessibility.getFullAXTree\", &ax_params, Some(session_id))\n        .await?;\n\n    let nth_index = nth.unwrap_or(0);\n    let mut match_count: usize = 0;\n\n    for node in &ax_tree.nodes {\n        if node.ignored.unwrap_or(false) {\n            continue;\n        }\n        let node_role = extract_ax_string(&node.role);\n        let node_name = extract_ax_string(&node.name);\n        if node_role == role && node_name == name {\n            if match_count == nth_index {\n                return node.backend_d_o_m_node_id.ok_or_else(|| {\n                    format!(\n                        \"AX node has no backendDOMNodeId for role={} name={}\",\n                        role, name\n                    )\n                });\n            }\n            match_count += 1;\n        }\n    }\n\n    Err(format!(\n        \"Could not locate element with role={} name={}\",\n        role, name\n    ))\n}\n\nfn extract_ax_string(value: &Option<AXValue>) -> String {\n    match value {\n        Some(v) => match &v.value {\n            Some(Value::String(s)) => s.clone(),\n            Some(Value::Number(n)) => n.to_string(),\n            Some(Value::Bool(b)) => b.to_string(),\n            _ => String::new(),\n        },\n        None => String::new(),\n    }\n}\n\n/// Build a JS expression that finds a DOM element by CSS selector or XPath.\nfn build_find_element_js(selector: &str) -> String {\n    if let Some(xpath) = selector.strip_prefix(\"xpath=\") {\n        format!(\n            \"document.evaluate({}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\",\n            serde_json::to_string(xpath).unwrap_or_default()\n        )\n    } else {\n        format!(\n            \"document.querySelector({})\",\n            serde_json::to_string(selector).unwrap_or_default()\n        )\n    }\n}\n\n/// Build a JS expression that counts matching DOM elements by CSS selector or XPath.\nfn build_count_elements_js(selector: &str) -> String {\n    if let Some(xpath) = selector.strip_prefix(\"xpath=\") {\n        format!(\n            \"document.evaluate({}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength\",\n            serde_json::to_string(xpath).unwrap_or_default()\n        )\n    } else {\n        format!(\n            \"document.querySelectorAll({}).length\",\n            serde_json::to_string(selector).unwrap_or_default()\n        )\n    }\n}\n\nfn build_selector_js(selector: &str) -> String {\n    let find_expr = build_find_element_js(selector);\n    format!(\n        r#\"(() => {{\n            const el = {find_expr};\n            if (!el) return null;\n            const rect = el.getBoundingClientRect();\n            return {{ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }};\n        }})()\"#,\n    )\n}\n\nasync fn resolve_by_selector(\n    client: &CdpClient,\n    session_id: &str,\n    selector: &str,\n) -> Result<(f64, f64), String> {\n    let js = build_selector_js(selector);\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: js,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    let val = result.result.value.unwrap_or(Value::Null);\n    let x = val.get(\"x\").and_then(|v| v.as_f64());\n    let y = val.get(\"y\").and_then(|v| v.as_f64());\n\n    match (x, y) {\n        (Some(x), Some(y)) => Ok((x, y)),\n        _ => Err(format!(\"Element not found: {}\", selector)),\n    }\n}\n\nfn box_model_center(model: &BoxModel) -> (f64, f64) {\n    // content quad: [x1,y1, x2,y2, x3,y3, x4,y4]\n    if model.content.len() >= 8 {\n        let x = (model.content[0] + model.content[2] + model.content[4] + model.content[6]) / 4.0;\n        let y = (model.content[1] + model.content[3] + model.content[5] + model.content[7]) / 4.0;\n        (x, y)\n    } else {\n        (0.0, 0.0)\n    }\n}\n\npub async fn get_element_text(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<String, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration:\n                    \"function() { return this.innerText || this.textContent || ''; }\".to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_str().map(|s| s.to_string()))\n        .unwrap_or_default())\n}\n\npub async fn get_element_attribute(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    attribute: &str,\n) -> Result<Value, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: format!(\n                    \"function() {{ return this.getAttribute({}); }}\",\n                    serde_json::to_string(attribute).unwrap_or_default()\n                ),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result.result.value.unwrap_or(Value::Null))\n}\n\npub async fn is_element_visible(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<bool, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    const rect = this.getBoundingClientRect();\n                    const style = window.getComputedStyle(this);\n                    return rect.width > 0 && rect.height > 0 &&\n                           style.visibility !== 'hidden' &&\n                           style.display !== 'none' &&\n                           parseFloat(style.opacity) > 0;\n                }\"#\n                .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false))\n}\n\npub async fn is_element_enabled(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<bool, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: \"function() { return !this.disabled; }\".to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_bool())\n        .unwrap_or(true))\n}\n\npub async fn is_element_checked(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<bool, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    // Mirrors Playwright's getChecked() with follow-label retargeting:\n    // 1. If element is a native checkbox/radio input, return .checked\n    // 2. If element has an ARIA checked role, return aria-checked\n    // 3. Follow label → input association (label.control)\n    // 4. Check for nested checkbox/radio input as last resort\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    var el = this;\n                    // Native checkbox/radio input\n                    var tag = el.tagName && el.tagName.toUpperCase();\n                    if (tag === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')) {\n                        return el.checked;\n                    }\n                    // ARIA role-based checked state\n                    var role = el.getAttribute && el.getAttribute('role');\n                    var ariaCheckedRoles = ['checkbox','radio','switch','menuitemcheckbox','menuitemradio','option','treeitem'];\n                    if (role && ariaCheckedRoles.indexOf(role) !== -1) {\n                        return el.getAttribute('aria-checked') === 'true';\n                    }\n                    // Follow label association (Playwright follow-label retarget)\n                    var label = el;\n                    if (tag !== 'LABEL') {\n                        label = el.closest && el.closest('label');\n                    }\n                    if (label && label.tagName && label.tagName.toUpperCase() === 'LABEL' && label.control) {\n                        var ctrl = label.control;\n                        if (ctrl.type === 'checkbox' || ctrl.type === 'radio') {\n                            return ctrl.checked;\n                        }\n                    }\n                    // Check for nested native input\n                    var input = el.querySelector && el.querySelector('input[type=\"checkbox\"], input[type=\"radio\"]');\n                    if (input) return input.checked;\n                    return false;\n                }\"#.to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false))\n}\n\npub async fn get_element_inner_text(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<String, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: \"function() { return this.innerText || ''; }\".to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_str().map(|s| s.to_string()))\n        .unwrap_or_default())\n}\n\npub async fn get_element_inner_html(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<String, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: \"function() { return this.innerHTML || ''; }\".to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_str().map(|s| s.to_string()))\n        .unwrap_or_default())\n}\n\npub async fn get_element_input_value(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<String, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration:\n                    \"function() { return typeof this.value === 'string' ? this.value : ''; }\"\n                        .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result\n        .result\n        .value\n        .and_then(|v| v.as_str().map(|s| s.to_string()))\n        .unwrap_or_default())\n}\n\npub async fn set_element_value(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    value: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let js = format!(\n        \"function() {{ this.value = {}; this.dispatchEvent(new Event('input', {{bubbles: true}})); this.dispatchEvent(new Event('change', {{bubbles: true}})); }}\",\n        serde_json::to_string(value).unwrap_or_default()\n    );\n\n    client\n        .send_command_typed::<_, EvaluateResult>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: js,\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn get_element_bounding_box(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<Value, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    const r = this.getBoundingClientRect();\n                    return { x: r.x, y: r.y, width: r.width, height: r.height };\n                }\"#\n                .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    result\n        .result\n        .value\n        .ok_or_else(|| format!(\"Could not get bounding box for: {}\", selector_or_ref))\n}\n\npub async fn get_element_count(\n    client: &CdpClient,\n    session_id: &str,\n    selector: &str,\n) -> Result<i64, String> {\n    let js = build_count_elements_js(selector);\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: js,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result.result.value.and_then(|v| v.as_i64()).unwrap_or(0))\n}\n\npub async fn get_element_styles(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    properties: Option<Vec<String>>,\n) -> Result<Value, String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let js = match properties {\n        Some(props) => {\n            let props_json = serde_json::to_string(&props).unwrap_or(\"[]\".to_string());\n            format!(\n                r#\"function() {{\n                    const s = window.getComputedStyle(this);\n                    const props = {};\n                    const result = {{}};\n                    for (const p of props) result[p] = s.getPropertyValue(p);\n                    return result;\n                }}\"#,\n                props_json\n            )\n        }\n        None => r#\"function() {\n                    const s = window.getComputedStyle(this);\n                    const result = {};\n                    for (let i = 0; i < s.length; i++) {\n                        const p = s[i];\n                        result[p] = s.getPropertyValue(p);\n                    }\n                    return result;\n                }\"#\n        .to_string(),\n    };\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: js,\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result.result.value.unwrap_or(Value::Null))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse_ref_at_prefix() {\n        assert_eq!(parse_ref(\"@e1\"), Some(\"e1\".to_string()));\n        assert_eq!(parse_ref(\"@e123\"), Some(\"e123\".to_string()));\n    }\n\n    #[test]\n    fn test_parse_ref_equals_prefix() {\n        assert_eq!(parse_ref(\"ref=e1\"), Some(\"e1\".to_string()));\n    }\n\n    #[test]\n    fn test_parse_ref_bare() {\n        assert_eq!(parse_ref(\"e1\"), Some(\"e1\".to_string()));\n        assert_eq!(parse_ref(\"e42\"), Some(\"e42\".to_string()));\n    }\n\n    #[test]\n    fn test_parse_ref_invalid() {\n        assert_eq!(parse_ref(\"button\"), None);\n        assert_eq!(parse_ref(\"e\"), None);\n        assert_eq!(parse_ref(\"1\"), None);\n        assert_eq!(parse_ref(\"\"), None);\n    }\n\n    #[test]\n    fn test_ref_map_basic() {\n        let mut map = RefMap::new();\n        map.add(\"e1\".to_string(), Some(42), \"button\", \"Submit\", None);\n        assert!(map.get(\"e1\").is_some());\n        assert_eq!(map.get(\"e1\").unwrap().role, \"button\");\n        assert!(map.get(\"e2\").is_none());\n    }\n\n    #[test]\n    fn test_build_selector_js_css() {\n        let js = build_selector_js(\"#submit-btn\");\n        assert!(js.contains(\"document.querySelector(\\\"#submit-btn\\\")\"));\n        assert!(!js.contains(\"document.evaluate\"));\n    }\n\n    #[test]\n    fn test_build_selector_js_xpath() {\n        let js = build_selector_js(\"xpath=//button[@id='ok']\");\n        assert!(js.contains(\"document.evaluate(\\\"//button[@id='ok']\\\", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)\"));\n        assert!(!js.contains(\"document.querySelector\"));\n    }\n\n    #[test]\n    fn test_build_selector_js_xpath_empty() {\n        let js = build_selector_js(\"xpath=\");\n        assert!(js.contains(\"document.evaluate\"));\n    }\n\n    #[test]\n    fn test_build_selector_js_not_xpath_prefix() {\n        // \"xpath\" without \"=\" should be treated as CSS selector\n        let js = build_selector_js(\"xpath//div\");\n        assert!(js.contains(\"document.querySelector\"));\n    }\n\n    #[test]\n    fn test_build_count_elements_js_css() {\n        let js = build_count_elements_js(\".item\");\n        assert!(js.contains(\"document.querySelectorAll(\\\".item\\\").length\"));\n        assert!(!js.contains(\"document.evaluate\"));\n    }\n\n    #[test]\n    fn test_build_count_elements_js_xpath() {\n        let js = build_count_elements_js(\"xpath=//li\");\n        assert!(js.contains(\"document.evaluate(\\\"//li\\\", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength\"));\n        assert!(!js.contains(\"querySelectorAll\"));\n    }\n\n    #[test]\n    fn test_box_model_center() {\n        let model = BoxModel {\n            content: vec![10.0, 20.0, 110.0, 20.0, 110.0, 60.0, 10.0, 60.0],\n            padding: vec![],\n            border: vec![],\n            margin: vec![],\n            width: 100,\n            height: 40,\n        };\n        let (x, y) = box_model_center(&model);\n        assert!((x - 60.0).abs() < 0.01);\n        assert!((y - 40.0).abs() < 0.01);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/inspect_server.rs",
    "content": "use std::io::Write;\nuse std::sync::atomic::{AtomicI64, Ordering};\nuse std::sync::Arc;\n\nuse futures_util::{SinkExt, StreamExt};\nuse tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};\nuse tokio::net::TcpListener;\nuse tokio::sync::Mutex;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::cdp::client::InspectProxyHandle;\n\n/// Counter for unique attach IDs so concurrent connections don't collide.\nstatic ATTACH_ID: AtomicI64 = AtomicI64::new(-1000);\n\n/// Lightweight HTTP + WebSocket server for `agent-browser inspect`.\n///\n/// Serves two purposes:\n/// - `GET /` redirects to Chrome's built-in DevTools frontend with `ws=` pointing to this server\n/// - WebSocket connections create a dedicated CDP session via `Target.attachToTarget` and proxy\n///   CDP messages through the daemon's existing browser-level connection, injecting/stripping\n///   `sessionId` so the DevTools frontend sees a page-level view\npub struct InspectServer {\n    port: u16,\n    _handle: tokio::task::JoinHandle<()>,\n}\n\nimpl InspectServer {\n    /// Start the inspect proxy server.\n    ///\n    /// - `proxy_handle`: lightweight handle for sending/receiving raw CDP messages\n    /// - `target_id`: the CDP target ID of the page to inspect\n    /// - `chrome_host_port`: the Chrome debug server address (e.g. \"127.0.0.1:9222\")\n    pub async fn start(\n        proxy_handle: InspectProxyHandle,\n        target_id: String,\n        chrome_host_port: String,\n    ) -> Result<Self, String> {\n        let listener = TcpListener::bind(\"127.0.0.1:0\")\n            .await\n            .map_err(|e| format!(\"Failed to bind inspect server: {}\", e))?;\n        let port = listener\n            .local_addr()\n            .map_err(|e| format!(\"Failed to get local addr: {}\", e))?\n            .port();\n\n        let proxy = Arc::new(proxy_handle);\n\n        let handle = tokio::spawn(accept_loop(\n            listener,\n            proxy,\n            target_id,\n            chrome_host_port,\n            port,\n        ));\n\n        Ok(Self {\n            port,\n            _handle: handle,\n        })\n    }\n\n    pub fn port(&self) -> u16 {\n        self.port\n    }\n\n    pub fn shutdown(self) {\n        self._handle.abort();\n    }\n}\n\nasync fn accept_loop(\n    listener: TcpListener,\n    proxy: Arc<InspectProxyHandle>,\n    target_id: String,\n    chrome_host_port: String,\n    proxy_port: u16,\n) {\n    loop {\n        let (stream, _) = match listener.accept().await {\n            Ok(s) => s,\n            Err(_) => continue,\n        };\n\n        let proxy = proxy.clone();\n        let tid = target_id.clone();\n        let chp = chrome_host_port.clone();\n\n        tokio::spawn(async move {\n            if let Err(e) = handle_connection(stream, proxy, tid, chp, proxy_port).await {\n                let _ = writeln!(std::io::stderr(), \"[inspect] connection error: {}\", e);\n            }\n        });\n    }\n}\n\nasync fn handle_connection(\n    stream: tokio::net::TcpStream,\n    proxy: Arc<InspectProxyHandle>,\n    target_id: String,\n    chrome_host_port: String,\n    proxy_port: u16,\n) -> Result<(), String> {\n    // Peek at the request line to determine routing WITHOUT consuming bytes.\n    // This is critical: tokio_tungstenite::accept_async needs to read the full\n    // HTTP upgrade request itself, so we must not consume anything for WS paths.\n    let mut peek_buf = [0u8; 32];\n    let n = stream\n        .peek(&mut peek_buf)\n        .await\n        .map_err(|e| e.to_string())?;\n    let peek = String::from_utf8_lossy(&peek_buf[..n]);\n\n    if peek.starts_with(\"GET /ws\") {\n        return handle_ws_proxy(stream, proxy, target_id).await;\n    }\n\n    if peek.starts_with(\"GET / \") {\n        let buf_reader = BufReader::new(stream);\n        return handle_http_redirect(buf_reader, chrome_host_port, proxy_port).await;\n    }\n\n    // Unknown request -- consume and respond 404\n    let mut stream = stream;\n    let mut discard = [0u8; 4096];\n    let _ = stream.read(&mut discard).await;\n    let resp = \"HTTP/1.1 404 Not Found\\r\\nContent-Length: 0\\r\\nConnection: close\\r\\n\\r\\n\";\n    stream\n        .write_all(resp.as_bytes())\n        .await\n        .map_err(|e| e.to_string())?;\n    Ok(())\n}\n\nconst MAX_HEADER_BYTES: usize = 8192;\n\nasync fn handle_http_redirect(\n    buf_reader: BufReader<tokio::net::TcpStream>,\n    chrome_host_port: String,\n    proxy_port: u16,\n) -> Result<(), String> {\n    let mut br = buf_reader;\n    let mut total_bytes = 0usize;\n    loop {\n        let mut line = String::new();\n        let n = br.read_line(&mut line).await.map_err(|e| e.to_string())?;\n        total_bytes += n;\n        if line == \"\\r\\n\" || line == \"\\n\" || line.is_empty() || total_bytes > MAX_HEADER_BYTES {\n            break;\n        }\n    }\n\n    let location = format!(\n        \"http://{}/devtools/devtools_app.html?ws=127.0.0.1:{}/ws\",\n        chrome_host_port, proxy_port\n    );\n    let body = format!(\n        \"<html><body>Redirecting to <a href=\\\"{url}\\\">{url}</a></body></html>\",\n        url = location\n    );\n    let resp = format!(\n        \"HTTP/1.1 302 Found\\r\\nLocation: {}\\r\\nContent-Type: text/html\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n        location,\n        body.len(),\n        body\n    );\n    let mut stream = br.into_inner();\n    stream\n        .write_all(resp.as_bytes())\n        .await\n        .map_err(|e| e.to_string())?;\n    Ok(())\n}\n\nasync fn handle_ws_proxy(\n    stream: tokio::net::TcpStream,\n    proxy: Arc<InspectProxyHandle>,\n    target_id: String,\n) -> Result<(), String> {\n    let ws_stream = tokio_tungstenite::accept_async(stream)\n        .await\n        .map_err(|e| format!(\"WebSocket handshake failed: {}\", e))?;\n\n    // Create a dedicated CDP session for this DevTools connection.\n    // Each connection gets its own session so domain enablements (DOM.enable, etc.)\n    // always trigger fresh initial state dumps from Chrome.\n    let attach_id = ATTACH_ID.fetch_sub(1, Ordering::SeqCst);\n    let attach_cmd = format!(\n        r#\"{{\"id\":{},\"method\":\"Target.attachToTarget\",\"params\":{{\"targetId\":\"{}\",\"flatten\":true}}}}\"#,\n        attach_id, target_id\n    );\n\n    // Subscribe BEFORE sending so we don't miss the response (tokio broadcast\n    // receivers only deliver messages to receivers that already exist).\n    let mut raw_rx = proxy.subscribe_raw();\n\n    proxy\n        .send_raw(attach_cmd)\n        .await\n        .map_err(|e| format!(\"Failed to send attachToTarget: {}\", e))?;\n\n    // Wait for the attachToTarget response to extract the session ID\n    let session_id = tokio::time::timeout(std::time::Duration::from_secs(5), async {\n        while let Ok(raw_msg) = raw_rx.recv().await {\n            if let Ok(val) = serde_json::from_str::<serde_json::Value>(&raw_msg.text) {\n                if val.get(\"id\").and_then(|v| v.as_i64()) == Some(attach_id) {\n                    if let Some(sid) = val\n                        .get(\"result\")\n                        .and_then(|r| r.get(\"sessionId\"))\n                        .and_then(|s| s.as_str())\n                    {\n                        return Ok(sid.to_string());\n                    }\n                    return Err(\"attachToTarget failed\".to_string());\n                }\n            }\n        }\n        Err(\"raw message channel closed\".to_string())\n    })\n    .await\n    .map_err(|_| \"Timed out waiting for attachToTarget response\".to_string())?\n    .map_err(|e| format!(\"Failed to create DevTools session: {}\", e))?;\n\n    let (ws_tx, mut ws_rx) = ws_stream.split();\n    let ws_tx = Arc::new(Mutex::new(ws_tx));\n\n    let mut raw_rx = proxy.subscribe_raw();\n    let ws_tx_clone = ws_tx.clone();\n    let session_id_clone = session_id.clone();\n\n    // Chrome -> DevTools: forward messages matching our session, strip sessionId\n    let mut chrome_to_devtools = tokio::spawn(async move {\n        loop {\n            let raw_msg = match raw_rx.recv().await {\n                Ok(msg) => msg,\n                Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {\n                    let _ = writeln!(\n                        std::io::stderr(),\n                        \"[inspect] warning: dropped {} CDP messages (channel lag)\",\n                        n\n                    );\n                    continue;\n                }\n                Err(_) => break,\n            };\n\n            if raw_msg.session_id.as_deref() != Some(&session_id_clone) {\n                continue;\n            }\n\n            let stripped = strip_session_id(&raw_msg.text);\n\n            let mut tx = ws_tx_clone.lock().await;\n            if tx.send(Message::Text(stripped)).await.is_err() {\n                break;\n            }\n        }\n    });\n\n    // DevTools -> Chrome: inject sessionId and forward\n    let proxy_for_send = proxy.clone();\n    let session_id_for_send = session_id.clone();\n    let mut devtools_to_chrome = tokio::spawn(async move {\n        while let Some(Ok(msg)) = ws_rx.next().await {\n            let text = match msg {\n                Message::Text(t) => t,\n                Message::Close(_) => break,\n                _ => continue,\n            };\n\n            let injected = inject_session_id(&text, &session_id_for_send);\n            if proxy_for_send.send_raw(injected).await.is_err() {\n                break;\n            }\n        }\n    });\n\n    tokio::select! {\n        _ = &mut chrome_to_devtools => {\n            devtools_to_chrome.abort();\n        },\n        _ = &mut devtools_to_chrome => {\n            chrome_to_devtools.abort();\n        },\n    }\n\n    // Clean up the CDP session so Chrome doesn't leak attached targets\n    let detach_cmd = format!(\n        r#\"{{\"id\":{},\"method\":\"Target.detachFromTarget\",\"params\":{{\"sessionId\":\"{}\"}}}}\"#,\n        ATTACH_ID.fetch_sub(1, Ordering::SeqCst),\n        session_id\n    );\n    let _ = proxy.send_raw(detach_cmd).await;\n\n    Ok(())\n}\n\nfn inject_session_id(json: &str, session_id: &str) -> String {\n    if let Ok(mut val) = serde_json::from_str::<serde_json::Value>(json) {\n        if let Some(obj) = val.as_object_mut() {\n            obj.insert(\n                \"sessionId\".to_string(),\n                serde_json::Value::String(session_id.to_string()),\n            );\n        }\n        serde_json::to_string(&val).unwrap_or_else(|_| json.to_string())\n    } else {\n        json.to_string()\n    }\n}\n\nfn strip_session_id(json: &str) -> String {\n    if let Ok(mut val) = serde_json::from_str::<serde_json::Value>(json) {\n        if let Some(obj) = val.as_object_mut() {\n            obj.remove(\"sessionId\");\n        }\n        serde_json::to_string(&val).unwrap_or_else(|_| json.to_string())\n    } else {\n        json.to_string()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_inject_session_id() {\n        let input = r#\"{\"id\":1,\"method\":\"DOM.getDocument\"}\"#;\n        let result = inject_session_id(input, \"abc123\");\n        let parsed: serde_json::Value = serde_json::from_str(&result).expect(\"valid JSON\");\n        assert_eq!(parsed[\"sessionId\"], \"abc123\");\n        assert_eq!(parsed[\"method\"], \"DOM.getDocument\");\n        assert_eq!(parsed[\"id\"], 1);\n    }\n\n    #[test]\n    fn test_inject_session_id_empty_object() {\n        let result = inject_session_id(\"{}\", \"abc\");\n        let parsed: serde_json::Value = serde_json::from_str(&result).expect(\"valid JSON\");\n        assert_eq!(parsed[\"sessionId\"], \"abc\");\n    }\n\n    #[test]\n    fn test_strip_session_id() {\n        let input = r#\"{\"id\":1,\"result\":{},\"sessionId\":\"abc123\"}\"#;\n        let result = strip_session_id(input);\n        let parsed: serde_json::Value = serde_json::from_str(&result).expect(\"valid JSON\");\n        assert!(parsed.get(\"sessionId\").is_none());\n        assert_eq!(parsed[\"id\"], 1);\n    }\n\n    #[test]\n    fn test_inject_then_strip_roundtrip() {\n        let input = r#\"{\"id\":42,\"method\":\"Runtime.evaluate\"}\"#;\n        let injected = inject_session_id(input, \"sess1\");\n        let stripped = strip_session_id(&injected);\n        let original: serde_json::Value = serde_json::from_str(input).unwrap();\n        let result: serde_json::Value = serde_json::from_str(&stripped).unwrap();\n        assert_eq!(original, result);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/interaction.rs",
    "content": "use serde_json::Value;\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::*;\nuse super::element::{resolve_element_center, resolve_element_object_id, RefMap};\n\npub async fn click(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    button: &str,\n    click_count: i32,\n) -> Result<(), String> {\n    let (x, y) = resolve_element_center(client, session_id, ref_map, selector_or_ref).await?;\n    dispatch_click(client, session_id, x, y, button, click_count).await\n}\n\npub async fn dblclick(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    click(client, session_id, ref_map, selector_or_ref, \"left\", 2).await\n}\n\npub async fn hover(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let (x, y) = resolve_element_center(client, session_id, ref_map, selector_or_ref).await?;\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.dispatchMouseEvent\",\n            &DispatchMouseEventParams {\n                event_type: \"mouseMoved\".to_string(),\n                x,\n                y,\n                button: None,\n                buttons: None,\n                click_count: None,\n                delta_x: None,\n                delta_y: None,\n                modifiers: None,\n            },\n            Some(session_id),\n        )\n        .await?;\n    Ok(())\n}\n\npub async fn fill(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    value: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    // Focus the element\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: \"function() { this.focus(); }\".to_string(),\n                object_id: Some(object_id.clone()),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    // Select all + delete to clear\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    this.select && this.select();\n                    this.value = '';\n                    this.dispatchEvent(new Event('input', { bubbles: true }));\n                }\"#\n                .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    // Insert text\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.insertText\",\n            &InsertTextParams {\n                text: value.to_string(),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\n#[allow(clippy::too_many_arguments)]\npub async fn type_text(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    text: &str,\n    clear: bool,\n    delay_ms: Option<u64>,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    // Focus\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: \"function() { this.focus(); }\".to_string(),\n                object_id: Some(object_id.clone()),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    if clear {\n        client\n            .send_command_typed::<_, Value>(\n                \"Runtime.callFunctionOn\",\n                &CallFunctionOnParams {\n                    function_declaration: r#\"function() {\n                        this.select && this.select();\n                        this.value = '';\n                        this.dispatchEvent(new Event('input', { bubbles: true }));\n                    }\"#\n                    .to_string(),\n                    object_id: Some(object_id),\n                    arguments: None,\n                    return_by_value: Some(true),\n                    await_promise: Some(false),\n                },\n                Some(session_id),\n            )\n            .await?;\n    }\n\n    let delay = delay_ms.unwrap_or(0);\n\n    for ch in text.chars() {\n        let text_str = ch.to_string();\n        let (key, code, key_code) = char_to_key_info(ch);\n\n        // Characters that have no US-keyboard mapping (key_code == 0 and empty\n        // code) are inserted via `Input.insertText`, matching Playwright's\n        // keyboard.type() fallback behaviour.  This handles emoji, CJK, and\n        // other characters that don't correspond to a physical key.\n        if key_code == 0 && code.is_empty() {\n            client\n                .send_command_typed::<_, Value>(\n                    \"Input.insertText\",\n                    &InsertTextParams { text: text_str },\n                    Some(session_id),\n                )\n                .await?;\n        } else {\n            client\n                .send_command_typed::<_, Value>(\n                    \"Input.dispatchKeyEvent\",\n                    &DispatchKeyEventParams {\n                        event_type: \"keyDown\".to_string(),\n                        key: Some(key.clone()),\n                        code: Some(code.clone()),\n                        text: Some(text_str.clone()),\n                        unmodified_text: Some(text_str.clone()),\n                        windows_virtual_key_code: Some(key_code),\n                        native_virtual_key_code: Some(key_code),\n                        modifiers: None,\n                    },\n                    Some(session_id),\n                )\n                .await?;\n\n            client\n                .send_command_typed::<_, Value>(\n                    \"Input.dispatchKeyEvent\",\n                    &DispatchKeyEventParams {\n                        event_type: \"keyUp\".to_string(),\n                        key: Some(key),\n                        code: Some(code),\n                        text: None,\n                        unmodified_text: None,\n                        windows_virtual_key_code: Some(key_code),\n                        native_virtual_key_code: Some(key_code),\n                        modifiers: None,\n                    },\n                    Some(session_id),\n                )\n                .await?;\n        }\n\n        if delay > 0 {\n            tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;\n        }\n    }\n\n    Ok(())\n}\n\npub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Result<(), String> {\n    press_key_with_modifiers(client, session_id, key, None).await\n}\n\n/// Dispatch a keyDown+keyUp sequence for `key` with an optional CDP modifier bitmask.\n///\n/// Modifier values follow the CDP `Input.dispatchKeyEvent` spec:\n/// 1 = Alt, 2 = Control, 4 = Meta (Cmd), 8 = Shift.\n///\n/// Callers that need a platform-appropriate modifier (e.g. Cmd on macOS,\n/// Ctrl elsewhere) must choose the value themselves -- see `cfg!(target_os)`.\npub async fn press_key_with_modifiers(\n    client: &CdpClient,\n    session_id: &str,\n    key: &str,\n    modifiers: Option<i32>,\n) -> Result<(), String> {\n    let (key_name, code, key_code) = named_key_info(key);\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.dispatchKeyEvent\",\n            &DispatchKeyEventParams {\n                event_type: \"keyDown\".to_string(),\n                key: Some(key_name.clone()),\n                code: Some(code.clone()),\n                text: None,\n                unmodified_text: None,\n                windows_virtual_key_code: Some(key_code),\n                native_virtual_key_code: Some(key_code),\n                modifiers,\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.dispatchKeyEvent\",\n            &DispatchKeyEventParams {\n                event_type: \"keyUp\".to_string(),\n                key: Some(key_name),\n                code: Some(code),\n                text: None,\n                unmodified_text: None,\n                windows_virtual_key_code: Some(key_code),\n                native_virtual_key_code: Some(key_code),\n                modifiers,\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn scroll(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: Option<&str>,\n    delta_x: f64,\n    delta_y: f64,\n) -> Result<(), String> {\n    if let Some(sel) = selector_or_ref {\n        let object_id = resolve_element_object_id(client, session_id, ref_map, sel).await?;\n        let js = \"function(dx, dy) { this.scrollBy(dx, dy); }\".to_string();\n        client\n            .send_command_typed::<_, Value>(\n                \"Runtime.callFunctionOn\",\n                &CallFunctionOnParams {\n                    function_declaration: js,\n                    object_id: Some(object_id),\n                    arguments: Some(vec![\n                        CallArgument {\n                            value: Some(serde_json::json!(delta_x)),\n                            object_id: None,\n                        },\n                        CallArgument {\n                            value: Some(serde_json::json!(delta_y)),\n                            object_id: None,\n                        },\n                    ]),\n                    return_by_value: Some(true),\n                    await_promise: Some(false),\n                },\n                Some(session_id),\n            )\n            .await?;\n    } else {\n        let js = format!(\"window.scrollBy({}, {})\", delta_x, delta_y);\n        client\n            .send_command_typed::<_, Value>(\n                \"Runtime.evaluate\",\n                &EvaluateParams {\n                    expression: js,\n                    return_by_value: Some(true),\n                    await_promise: Some(false),\n                },\n                Some(session_id),\n            )\n            .await?;\n    }\n    Ok(())\n}\n\npub async fn select_option(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    values: &[String],\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let js = r#\"function(vals) {\n            const options = Array.from(this.options);\n            for (const opt of options) {\n                opt.selected = vals.includes(opt.value) || vals.includes(opt.textContent.trim());\n            }\n            this.dispatchEvent(new Event('change', { bubbles: true }));\n        }\"#\n    .to_string();\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: js,\n                object_id: Some(object_id),\n                arguments: Some(vec![CallArgument {\n                    value: Some(serde_json::json!(values)),\n                    object_id: None,\n                }]),\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn check(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let is_checked =\n        super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await?;\n    if !is_checked {\n        click(client, session_id, ref_map, selector_or_ref, \"left\", 1).await?;\n\n        // Verify the click changed the state (Playwright parity: _setChecked re-checks).\n        // If the coordinate-based click missed (e.g. hidden input, overlay), retry\n        // with a JS .click() on the element and its associated input.\n        if !super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await?\n        {\n            js_click_checkbox(client, session_id, ref_map, selector_or_ref).await?;\n        }\n    }\n    Ok(())\n}\n\npub async fn uncheck(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let is_checked =\n        super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await?;\n    if is_checked {\n        click(client, session_id, ref_map, selector_or_ref, \"left\", 1).await?;\n\n        // Same verify-and-retry as check().\n        if super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await? {\n            js_click_checkbox(client, session_id, ref_map, selector_or_ref).await?;\n        }\n    }\n    Ok(())\n}\n\n/// Fallback for when the coordinate-based CDP click did not toggle the\n/// checkbox/radio state. This mirrors how Playwright dispatches clicks\n/// through the DOM rather than via raw Input.dispatchMouseEvent coordinates.\n///\n/// Uses the same follow-label resolution as `is_element_checked`:\n/// 1. If the element is a native input → `.click()` it directly.\n/// 2. If the element is inside a `<label>` → `.click()` the label's `.control`.\n/// 3. If the element has a nested `<input>` → `.click()` that input.\n/// 4. Otherwise → `.click()` the element itself (handles ARIA role controls).\nasync fn js_click_checkbox(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let js = r#\"function() {\n            var el = this;\n            var tag = el.tagName && el.tagName.toUpperCase();\n            // 1. Native input — click it directly\n            if (tag === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')) {\n                el.click();\n                return;\n            }\n            // 2. Follow label → control association\n            var label = tag === 'LABEL' ? el : (el.closest && el.closest('label'));\n            if (label && label.tagName && label.tagName.toUpperCase() === 'LABEL' && label.control) {\n                label.control.click();\n                return;\n            }\n            // 3. Nested native input\n            var input = el.querySelector && el.querySelector('input[type=\"checkbox\"], input[type=\"radio\"]');\n            if (input) {\n                input.click();\n                return;\n            }\n            // 4. ARIA role control — click the element itself\n            el.click();\n        }\"#;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: js.to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn focus(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: \"function() { this.focus(); }\".to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn clear(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    this.focus();\n                    this.value = '';\n                    this.dispatchEvent(new Event('input', { bubbles: true }));\n                    this.dispatchEvent(new Event('change', { bubbles: true }));\n                }\"#\n                .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn select_all(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    this.focus();\n                    if (typeof this.select === 'function') {\n                        this.select();\n                    } else {\n                        const range = document.createRange();\n                        range.selectNodeContents(this);\n                        const sel = window.getSelection();\n                        sel.removeAllRanges();\n                        sel.addRange(range);\n                    }\n                }\"#\n                .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn scroll_into_view(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration:\n                    \"function() { this.scrollIntoView({ block: 'center', inline: 'center' }); }\"\n                        .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn dispatch_event(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n    event_type: &str,\n    event_init: Option<&Value>,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    let init_json = event_init\n        .map(|v| serde_json::to_string(v).unwrap_or(\"{}\".to_string()))\n        .unwrap_or_else(|| \"{ bubbles: true }\".to_string());\n\n    let js = format!(\n        \"function() {{ this.dispatchEvent(new Event({}, {})); }}\",\n        serde_json::to_string(event_type).unwrap_or_default(),\n        init_json\n    );\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: js,\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn highlight(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?;\n\n    client\n        .send_command_typed::<_, Value>(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    this.style.outline = '2px solid red';\n                    this.style.outlineOffset = '2px';\n                    const el = this;\n                    setTimeout(() => {\n                        el.style.outline = '';\n                        el.style.outlineOffset = '';\n                    }, 3000);\n                }\"#\n                .to_string(),\n                object_id: Some(object_id),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn tap_touch(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector_or_ref: &str,\n) -> Result<(), String> {\n    let (x, y) = resolve_element_center(client, session_id, ref_map, selector_or_ref).await?;\n\n    client\n        .send_command(\n            \"Input.dispatchTouchEvent\",\n            Some(serde_json::json!({\n                \"type\": \"touchStart\",\n                \"touchPoints\": [{ \"x\": x, \"y\": y }],\n            })),\n            Some(session_id),\n        )\n        .await?;\n\n    client\n        .send_command(\n            \"Input.dispatchTouchEvent\",\n            Some(serde_json::json!({\n                \"type\": \"touchEnd\",\n                \"touchPoints\": [],\n            })),\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\nasync fn dispatch_click(\n    client: &CdpClient,\n    session_id: &str,\n    x: f64,\n    y: f64,\n    button: &str,\n    click_count: i32,\n) -> Result<(), String> {\n    // Move\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.dispatchMouseEvent\",\n            &DispatchMouseEventParams {\n                event_type: \"mouseMoved\".to_string(),\n                x,\n                y,\n                button: None,\n                buttons: None,\n                click_count: None,\n                delta_x: None,\n                delta_y: None,\n                modifiers: None,\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    let button_value = match button {\n        \"right\" => 2,\n        \"middle\" => 4,\n        _ => 1,\n    };\n\n    // Press\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.dispatchMouseEvent\",\n            &DispatchMouseEventParams {\n                event_type: \"mousePressed\".to_string(),\n                x,\n                y,\n                button: Some(button.to_string()),\n                buttons: Some(button_value),\n                click_count: Some(click_count),\n                delta_x: None,\n                delta_y: None,\n                modifiers: None,\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    // Release\n    client\n        .send_command_typed::<_, Value>(\n            \"Input.dispatchMouseEvent\",\n            &DispatchMouseEventParams {\n                event_type: \"mouseReleased\".to_string(),\n                x,\n                y,\n                button: Some(button.to_string()),\n                buttons: Some(0),\n                click_count: Some(click_count),\n                delta_x: None,\n                delta_y: None,\n                modifiers: None,\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\nfn char_to_key_info(ch: char) -> (String, String, i32) {\n    match ch {\n        '\\n' | '\\r' => (\"Enter\".to_string(), \"Enter\".to_string(), 13),\n        '\\t' => (\"Tab\".to_string(), \"Tab\".to_string(), 9),\n        ' ' => (\" \".to_string(), \"Space\".to_string(), 32),\n        _ => {\n            let key = ch.to_string();\n            if ch.is_ascii_alphabetic() {\n                // For letters the Windows VK code equals the uppercase ASCII value.\n                let upper = ch.to_ascii_uppercase();\n                let code = format!(\"Key{}\", upper);\n                let key_code = upper as i32;\n                (key, code, key_code)\n            } else if ch.is_ascii_digit() {\n                let code = format!(\"Digit{}\", ch);\n                let key_code = ch as i32;\n                (key, code, key_code)\n            } else {\n                let (code, key_code) = punctuation_key_info(ch);\n                (key, code.to_string(), key_code)\n            }\n        }\n    }\n}\n\n/// Return the DOM `KeyboardEvent.code` value and Windows virtual-key code for\n/// a punctuation / symbol character assuming a US keyboard layout.\n///\n/// The Windows virtual-key codes (VK_OEM_*) differ from ASCII values for\n/// punctuation.  Using the raw ASCII code would misidentify characters – e.g.\n/// '.' (ASCII 46) collides with VK_DELETE (0x2E = 46), causing the period to\n/// be swallowed.\nfn punctuation_key_info(ch: char) -> (&'static str, i32) {\n    match ch {\n        // VK_OEM_1 (0xBA = 186) — \";:\" key on US layout\n        ';' | ':' => (\"Semicolon\", 186),\n        // VK_OEM_PLUS (0xBB = 187) — \"=+\" key\n        '=' | '+' => (\"Equal\", 187),\n        // VK_OEM_COMMA (0xBC = 188) — \",<\" key\n        ',' | '<' => (\"Comma\", 188),\n        // VK_OEM_MINUS (0xBD = 189) — \"-_\" key\n        '-' | '_' => (\"Minus\", 189),\n        // VK_OEM_PERIOD (0xBE = 190) — \".>\" key\n        '.' | '>' => (\"Period\", 190),\n        // VK_OEM_2 (0xBF = 191) — \"/?\" key\n        '/' | '?' => (\"Slash\", 191),\n        // VK_OEM_3 (0xC0 = 192) — \"`~\" key\n        '`' | '~' => (\"Backquote\", 192),\n        // VK_OEM_4 (0xDB = 219) — \"[{\" key\n        '[' | '{' => (\"BracketLeft\", 219),\n        // VK_OEM_5 (0xDC = 220) — \"\\\\|\" key\n        '\\\\' | '|' => (\"Backslash\", 220),\n        // VK_OEM_6 (0xDD = 221) — \"]}\" key\n        ']' | '}' => (\"BracketRight\", 221),\n        // VK_OEM_7 (0xDE = 222) — \"'\\\"\"\" key\n        '\\'' | '\"' => (\"Quote\", 222),\n        _ => (\"\", 0),\n    }\n}\n\nfn named_key_info(key: &str) -> (String, String, i32) {\n    match key.to_lowercase().as_str() {\n        \"enter\" | \"return\" => (\"Enter\".to_string(), \"Enter\".to_string(), 13),\n        \"tab\" => (\"Tab\".to_string(), \"Tab\".to_string(), 9),\n        \"escape\" | \"esc\" => (\"Escape\".to_string(), \"Escape\".to_string(), 27),\n        \"backspace\" => (\"Backspace\".to_string(), \"Backspace\".to_string(), 8),\n        \"delete\" => (\"Delete\".to_string(), \"Delete\".to_string(), 46),\n        \"arrowup\" | \"up\" => (\"ArrowUp\".to_string(), \"ArrowUp\".to_string(), 38),\n        \"arrowdown\" | \"down\" => (\"ArrowDown\".to_string(), \"ArrowDown\".to_string(), 40),\n        \"arrowleft\" | \"left\" => (\"ArrowLeft\".to_string(), \"ArrowLeft\".to_string(), 37),\n        \"arrowright\" | \"right\" => (\"ArrowRight\".to_string(), \"ArrowRight\".to_string(), 39),\n        \"home\" => (\"Home\".to_string(), \"Home\".to_string(), 36),\n        \"end\" => (\"End\".to_string(), \"End\".to_string(), 35),\n        \"pageup\" => (\"PageUp\".to_string(), \"PageUp\".to_string(), 33),\n        \"pagedown\" => (\"PageDown\".to_string(), \"PageDown\".to_string(), 34),\n        \"space\" | \" \" => (\" \".to_string(), \"Space\".to_string(), 32),\n        _ => {\n            if key.len() == 1 {\n                let ch = key.chars().next().unwrap();\n                char_to_key_info(ch)\n            } else {\n                (key.to_string(), key.to_string(), 0)\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    /// Verify that `char_to_key_info` returns the correct (key, code,\n    /// windowsVirtualKeyCode) triple for every character in Playwright's\n    /// USKeyboardLayout.  The expected values below are taken verbatim from\n    /// playwright-core/lib/server/usKeyboardLayout.js so that any drift from\n    /// Playwright's behaviour is caught immediately.\n    #[test]\n    fn test_char_to_key_info_matches_playwright_layout() {\n        // (character, expected_code, expected_vk_code)\n        let cases: &[(char, &str, i32)] = &[\n            // Letters – VK code must equal the uppercase ASCII value.\n            ('a', \"KeyA\", 65),\n            ('z', \"KeyZ\", 90),\n            ('A', \"KeyA\", 65),\n            // Digits\n            ('0', \"Digit0\", 48),\n            ('9', \"Digit9\", 57),\n            // Punctuation – these are the values from Playwright's layout.\n            // The bug that prompted this test sent '.' as VK 46 (= VK_DELETE).\n            ('.', \"Period\", 190),\n            (',', \"Comma\", 188),\n            ('/', \"Slash\", 191),\n            (';', \"Semicolon\", 186),\n            ('\\'', \"Quote\", 222),\n            ('[', \"BracketLeft\", 219),\n            (']', \"BracketRight\", 221),\n            ('\\\\', \"Backslash\", 220),\n            ('`', \"Backquote\", 192),\n            ('-', \"Minus\", 189),\n            ('=', \"Equal\", 187),\n            // Shifted variants produced by the same physical keys.\n            ('>', \"Period\", 190),\n            ('<', \"Comma\", 188),\n            ('?', \"Slash\", 191),\n            (':', \"Semicolon\", 186),\n            ('\"', \"Quote\", 222),\n            ('{', \"BracketLeft\", 219),\n            ('}', \"BracketRight\", 221),\n            ('|', \"Backslash\", 220),\n            ('~', \"Backquote\", 192),\n            ('_', \"Minus\", 189),\n            ('+', \"Equal\", 187),\n            // Whitespace / control\n            (' ', \"Space\", 32),\n            ('\\n', \"Enter\", 13),\n            ('\\t', \"Tab\", 9),\n        ];\n\n        for &(ch, expected_code, expected_vk) in cases {\n            let (key, code, vk) = char_to_key_info(ch);\n            assert_eq!(\n                code, expected_code,\n                \"char {:?}: expected code {:?}, got {:?}\",\n                ch, expected_code, code\n            );\n            assert_eq!(\n                vk, expected_vk,\n                \"char {:?}: expected VK {}, got {} (ASCII would be {})\",\n                ch, expected_vk, vk, ch as i32\n            );\n            // key should be the character itself (except control chars).\n            if !ch.is_control() {\n                assert_eq!(key, ch.to_string(), \"char {:?}: key mismatch\", ch);\n            }\n        }\n    }\n\n    /// Regression test: period must NEVER map to VK 46 (VK_DELETE).\n    #[test]\n    fn test_period_is_not_vk_delete() {\n        let (_, _, vk) = char_to_key_info('.');\n        assert_ne!(\n            vk, 46,\n            \"Period must not use VK code 46 (VK_DELETE); expected 190 (VK_OEM_PERIOD)\"\n        );\n        assert_eq!(vk, 190);\n    }\n\n    /// Characters outside the US keyboard layout should return (key, \"\", 0)\n    /// so that `type_text` falls back to `Input.insertText`.\n    #[test]\n    fn test_unmapped_chars_return_zero_keycode() {\n        for ch in ['@', '#', '$', '%', '^', '&', '*', '(', ')', '€', '£', '你'] {\n            let (key, code, vk) = char_to_key_info(ch);\n            assert_eq!(\n                code, \"\",\n                \"char {:?}: unmapped char should have empty code, got {:?}\",\n                ch, code\n            );\n            assert_eq!(\n                vk, 0,\n                \"char {:?}: unmapped char should have VK 0, got {}\",\n                ch, vk\n            );\n            assert_eq!(key, ch.to_string());\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/native/mod.rs",
    "content": "#[allow(dead_code)]\npub mod actions;\n#[allow(dead_code)]\npub mod auth;\n#[allow(dead_code)]\npub mod browser;\n#[allow(dead_code)]\npub mod cdp;\n#[allow(dead_code)]\npub mod cookies;\n#[allow(dead_code)]\npub mod daemon;\n#[allow(dead_code)]\npub mod diff;\n#[allow(dead_code)]\npub mod element;\n#[allow(dead_code)]\npub mod inspect_server;\n#[allow(dead_code)]\npub mod interaction;\n#[allow(dead_code)]\npub mod network;\n#[allow(dead_code)]\npub mod policy;\n#[allow(dead_code)]\npub mod providers;\n#[allow(dead_code)]\npub mod recording;\n#[allow(dead_code)]\npub mod screenshot;\n#[allow(dead_code)]\npub mod snapshot;\n#[allow(dead_code)]\npub mod state;\n#[allow(dead_code)]\npub mod storage;\n#[allow(dead_code)]\npub mod stream;\n#[allow(dead_code)]\npub mod tracing;\n#[allow(dead_code)]\npub mod webdriver;\n\n#[cfg(test)]\nmod e2e_tests;\n#[cfg(test)]\nmod parity_tests;\n"
  },
  {
    "path": "cli/src/native/network.rs",
    "content": "use serde_json::{json, Value};\nuse std::collections::HashMap;\n\nuse super::cdp::client::CdpClient;\n\npub async fn set_extra_headers(\n    client: &CdpClient,\n    session_id: &str,\n    headers: &HashMap<String, String>,\n) -> Result<(), String> {\n    let headers_value: Value = headers\n        .iter()\n        .map(|(k, v)| (k.clone(), Value::String(v.clone())))\n        .collect::<serde_json::Map<String, Value>>()\n        .into();\n\n    client\n        .send_command(\n            \"Network.setExtraHTTPHeaders\",\n            Some(json!({ \"headers\": headers_value })),\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\npub async fn set_offline(\n    client: &CdpClient,\n    session_id: &str,\n    offline: bool,\n) -> Result<(), String> {\n    client\n        .send_command(\n            \"Network.emulateNetworkConditions\",\n            Some(json!({\n                \"offline\": offline,\n                \"latency\": 0,\n                \"downloadThroughput\": -1,\n                \"uploadThroughput\": -1,\n            })),\n            Some(session_id),\n        )\n        .await?;\n    Ok(())\n}\n\npub async fn set_content(client: &CdpClient, session_id: &str, html: &str) -> Result<(), String> {\n    // Get current frame ID\n    let tree_result = client\n        .send_command_no_params(\"Page.getFrameTree\", Some(session_id))\n        .await?;\n\n    let frame_id = tree_result\n        .get(\"frameTree\")\n        .and_then(|t| t.get(\"frame\"))\n        .and_then(|f| f.get(\"id\"))\n        .and_then(|id| id.as_str())\n        .ok_or(\"Could not determine frame ID\")?;\n\n    client\n        .send_command(\n            \"Page.setDocumentContent\",\n            Some(json!({\n                \"frameId\": frame_id,\n                \"html\": html,\n            })),\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\n// ---------------------------------------------------------------------------\n// Domain filter\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Clone)]\npub struct DomainFilter {\n    pub allowed_domains: Vec<String>,\n}\n\nimpl DomainFilter {\n    pub fn new(domains: &str) -> Self {\n        let allowed = parse_domain_list(domains);\n        Self {\n            allowed_domains: allowed,\n        }\n    }\n\n    pub fn is_allowed(&self, hostname: &str) -> bool {\n        if self.allowed_domains.is_empty() {\n            return true;\n        }\n        let hostname = hostname.to_lowercase();\n        for pattern in &self.allowed_domains {\n            if let Some(suffix) = pattern.strip_prefix(\"*.\") {\n                if hostname == suffix || hostname.ends_with(&format!(\".{}\", suffix)) {\n                    return true;\n                }\n            } else if hostname == *pattern {\n                return true;\n            }\n        }\n        false\n    }\n\n    pub fn check_url(&self, url: &str) -> Result<(), String> {\n        if self.allowed_domains.is_empty() {\n            return Ok(());\n        }\n        let parsed = url::Url::parse(url).map_err(|_| format!(\"Invalid URL: {}\", url))?;\n        let hostname = parsed\n            .host_str()\n            .ok_or_else(|| format!(\"No hostname in URL: {}\", url))?;\n        if self.is_allowed(hostname) {\n            Ok(())\n        } else {\n            Err(format!(\n                \"Domain '{}' is not in the allowed domains list\",\n                hostname\n            ))\n        }\n    }\n}\n\nfn parse_domain_list(input: &str) -> Vec<String> {\n    input\n        .split(',')\n        .map(|s| s.trim().to_lowercase())\n        .filter(|s| !s.is_empty())\n        .collect()\n}\n\npub async fn sanitize_existing_pages(\n    client: &CdpClient,\n    pages: &[super::browser::PageInfo],\n    filter: &DomainFilter,\n) {\n    for page in pages {\n        if page.url.is_empty() || page.url == \"about:blank\" {\n            continue;\n        }\n        if let Ok(parsed) = url::Url::parse(&page.url) {\n            if let Some(hostname) = parsed.host_str() {\n                if !filter.is_allowed(hostname) {\n                    let _ = client\n                        .send_command(\n                            \"Page.navigate\",\n                            Some(json!({ \"url\": \"about:blank\" })),\n                            Some(&page.session_id),\n                        )\n                        .await;\n                }\n            }\n        }\n    }\n}\n\npub async fn install_domain_filter_script(\n    client: &CdpClient,\n    session_id: &str,\n    allowed_domains: &[String],\n) -> Result<(), String> {\n    if allowed_domains.is_empty() {\n        return Ok(());\n    }\n\n    let domains_json = serde_json::to_string(allowed_domains).unwrap_or(\"[]\".to_string());\n    let script = format!(\n        r#\"(() => {{\n            const _allowed = {};\n            function _isDomainAllowed(hostname) {{\n                hostname = hostname.toLowerCase();\n                for (const p of _allowed) {{\n                    if (p.startsWith('*.')) {{\n                        const suffix = p.slice(2);\n                        if (hostname === suffix || hostname.endsWith('.' + suffix)) return true;\n                    }} else if (hostname === p) return true;\n                }}\n                return false;\n            }}\n            const OrigWS = window.WebSocket;\n            window.WebSocket = function(url, protocols) {{\n                try {{\n                    const u = new URL(url, location.href);\n                    if (!_isDomainAllowed(u.hostname)) throw new DOMException('WebSocket blocked: ' + u.hostname, 'SecurityError');\n                }} catch(e) {{ if (e instanceof DOMException) throw e; }}\n                return new OrigWS(url, protocols);\n            }};\n            window.WebSocket.prototype = OrigWS.prototype;\n            const OrigES = window.EventSource;\n            if (OrigES) {{\n                window.EventSource = function(url, opts) {{\n                    try {{\n                        const u = new URL(url, location.href);\n                        if (!_isDomainAllowed(u.hostname)) throw new DOMException('EventSource blocked: ' + u.hostname, 'SecurityError');\n                    }} catch(e) {{ if (e instanceof DOMException) throw e; }}\n                    return new OrigES(url, opts);\n                }};\n                window.EventSource.prototype = OrigES.prototype;\n            }}\n            const origBeacon = navigator.sendBeacon;\n            if (origBeacon) {{\n                navigator.sendBeacon = function(url, data) {{\n                    try {{\n                        const u = new URL(url, location.href);\n                        if (!_isDomainAllowed(u.hostname)) return false;\n                    }} catch(e) {{ return false; }}\n                    return origBeacon.call(navigator, url, data);\n                }};\n            }}\n        }})()\"#,\n        domains_json,\n    );\n\n    client\n        .send_command(\n            \"Page.addScriptToEvaluateOnNewDocument\",\n            Some(json!({ \"source\": script })),\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\n/// Enable Fetch-based network interception for domain filtering.\n/// This intercepts all requests and checks them against the allowed domains list.\n/// The actual handling of `Fetch.requestPaused` events happens in\n/// `resolve_fetch_paused` in the actions module.\npub async fn install_domain_filter_fetch(\n    client: &CdpClient,\n    session_id: &str,\n) -> Result<(), String> {\n    client\n        .send_command(\n            \"Fetch.enable\",\n            Some(json!({\n                \"patterns\": [{ \"urlPattern\": \"*\" }]\n            })),\n            Some(session_id),\n        )\n        .await?;\n    Ok(())\n}\n\n/// Install both layers of domain filtering on a session:\n/// 1. JS patching (WebSocket, EventSource, sendBeacon)\n/// 2. Fetch-based network interception\npub async fn install_domain_filter(\n    client: &CdpClient,\n    session_id: &str,\n    allowed_domains: &[String],\n) -> Result<(), String> {\n    install_domain_filter_script(client, session_id, allowed_domains).await?;\n    install_domain_filter_fetch(client, session_id).await?;\n    Ok(())\n}\n\n// ---------------------------------------------------------------------------\n// Console and error tracking\n// ---------------------------------------------------------------------------\n\n#[derive(Debug, Clone)]\npub struct ConsoleEntry {\n    pub level: String,\n    pub text: String,\n}\n\n#[derive(Debug, Clone)]\npub struct ErrorEntry {\n    pub text: String,\n    pub url: Option<String>,\n    pub line: Option<i64>,\n    pub column: Option<i64>,\n}\n\npub struct EventTracker {\n    pub console_entries: Vec<ConsoleEntry>,\n    pub error_entries: Vec<ErrorEntry>,\n    pub max_entries: usize,\n}\n\nimpl EventTracker {\n    pub fn new() -> Self {\n        Self {\n            console_entries: Vec::new(),\n            error_entries: Vec::new(),\n            max_entries: 1000,\n        }\n    }\n\n    pub fn add_console(&mut self, level: &str, text: &str) {\n        if self.console_entries.len() >= self.max_entries {\n            self.console_entries.remove(0);\n        }\n        self.console_entries.push(ConsoleEntry {\n            level: level.to_string(),\n            text: text.to_string(),\n        });\n    }\n\n    pub fn add_error(\n        &mut self,\n        text: &str,\n        url: Option<&str>,\n        line: Option<i64>,\n        col: Option<i64>,\n    ) {\n        if self.error_entries.len() >= self.max_entries {\n            self.error_entries.remove(0);\n        }\n        self.error_entries.push(ErrorEntry {\n            text: text.to_string(),\n            url: url.map(String::from),\n            line,\n            column: col,\n        });\n    }\n\n    pub fn get_console_json(&self) -> Value {\n        let entries: Vec<Value> = self\n            .console_entries\n            .iter()\n            .map(|e| json!({ \"level\": e.level, \"text\": e.text }))\n            .collect();\n        json!({ \"entries\": entries })\n    }\n\n    pub fn get_errors_json(&self) -> Value {\n        let entries: Vec<Value> = self\n            .error_entries\n            .iter()\n            .map(|e| {\n                json!({\n                    \"text\": e.text,\n                    \"url\": e.url,\n                    \"line\": e.line,\n                    \"column\": e.column,\n                })\n            })\n            .collect();\n        json!({ \"errors\": entries })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_domain_filter_exact() {\n        let filter = DomainFilter::new(\"example.com\");\n        assert!(filter.is_allowed(\"example.com\"));\n        assert!(!filter.is_allowed(\"other.com\"));\n    }\n\n    #[test]\n    fn test_domain_filter_wildcard() {\n        let filter = DomainFilter::new(\"*.example.com\");\n        assert!(filter.is_allowed(\"example.com\"));\n        assert!(filter.is_allowed(\"api.example.com\"));\n        assert!(filter.is_allowed(\"sub.api.example.com\"));\n        assert!(!filter.is_allowed(\"other.com\"));\n    }\n\n    #[test]\n    fn test_domain_filter_empty() {\n        let filter = DomainFilter::new(\"\");\n        assert!(filter.is_allowed(\"anything.com\"));\n    }\n\n    #[test]\n    fn test_domain_filter_multiple() {\n        let filter = DomainFilter::new(\"example.com, *.api.io\");\n        assert!(filter.is_allowed(\"example.com\"));\n        assert!(filter.is_allowed(\"api.io\"));\n        assert!(filter.is_allowed(\"v1.api.io\"));\n        assert!(!filter.is_allowed(\"other.com\"));\n    }\n\n    #[test]\n    fn test_parse_domain_list() {\n        let domains = parse_domain_list(\"A.com, B.com , *.C.com\");\n        assert_eq!(domains, vec![\"a.com\", \"b.com\", \"*.c.com\"]);\n    }\n\n    #[test]\n    fn test_event_tracker() {\n        let mut tracker = EventTracker::new();\n        tracker.add_console(\"log\", \"hello\");\n        tracker.add_error(\"oops\", Some(\"test.js\"), Some(1), Some(5));\n\n        assert_eq!(tracker.console_entries.len(), 1);\n        assert_eq!(tracker.error_entries.len(), 1);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/parity_tests.rs",
    "content": "//! Parity tests for the native daemon's command interface.\n//!\n//! These unit tests verify:\n//! - All documented actions are handled (not returning \"Not yet implemented\")\n//! - Response format consistency (success/error structure)\n//! - Credential and state actions work without a browser\n\nuse serde_json::{json, Value};\n\nuse super::actions::{execute_command, DaemonState};\n\nconst ENCRYPTION_KEY_ENV: &str = \"AGENT_BROWSER_ENCRYPTION_KEY\";\n\nstruct TestKeyGuard {\n    _lock: std::sync::MutexGuard<'static, ()>,\n    original: Option<String>,\n}\n\nimpl TestKeyGuard {\n    fn new() -> Self {\n        let lock = super::auth::AUTH_TEST_MUTEX\n            .lock()\n            .unwrap_or_else(|e| e.into_inner());\n        let original = std::env::var(ENCRYPTION_KEY_ENV).ok();\n        // SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.\n        unsafe { std::env::set_var(ENCRYPTION_KEY_ENV, \"a\".repeat(64)) };\n        Self {\n            _lock: lock,\n            original,\n        }\n    }\n}\n\nimpl Drop for TestKeyGuard {\n    fn drop(&mut self) {\n        // SAFETY: AUTH_TEST_MUTEX is held via _lock.\n        match &self.original {\n            Some(val) => unsafe { std::env::set_var(ENCRYPTION_KEY_ENV, val) },\n            None => unsafe { std::env::remove_var(ENCRYPTION_KEY_ENV) },\n        }\n    }\n}\n\n/// All documented action names that should be implemented.\nconst DOCUMENTED_ACTIONS: &[&str] = &[\n    \"launch\",\n    \"navigate\",\n    \"url\",\n    \"title\",\n    \"content\",\n    \"evaluate\",\n    \"close\",\n    \"snapshot\",\n    \"screenshot\",\n    \"click\",\n    \"dblclick\",\n    \"fill\",\n    \"type\",\n    \"press\",\n    \"hover\",\n    \"scroll\",\n    \"select\",\n    \"check\",\n    \"uncheck\",\n    \"wait\",\n    \"gettext\",\n    \"getattribute\",\n    \"isvisible\",\n    \"isenabled\",\n    \"ischecked\",\n    \"back\",\n    \"forward\",\n    \"reload\",\n    \"cookies_get\",\n    \"cookies_set\",\n    \"cookies_clear\",\n    \"storage_get\",\n    \"storage_set\",\n    \"storage_clear\",\n    \"setcontent\",\n    \"headers\",\n    \"offline\",\n    \"console\",\n    \"errors\",\n    \"state_save\",\n    \"state_load\",\n    \"state_list\",\n    \"state_show\",\n    \"state_clear\",\n    \"state_clean\",\n    \"state_rename\",\n    \"trace_start\",\n    \"trace_stop\",\n    \"profiler_start\",\n    \"profiler_stop\",\n    \"recording_start\",\n    \"recording_stop\",\n    \"recording_restart\",\n    \"pdf\",\n    \"tab_list\",\n    \"tab_new\",\n    \"tab_switch\",\n    \"tab_close\",\n    \"viewport\",\n    \"user_agent\",\n    \"set_media\",\n    \"download\",\n    \"diff_snapshot\",\n    \"diff_url\",\n    \"credentials_set\",\n    \"credentials_get\",\n    \"credentials_delete\",\n    \"credentials_list\",\n    \"mouse\",\n    \"keyboard\",\n    \"focus\",\n    \"clear\",\n    \"selectall\",\n    \"scrollintoview\",\n    \"dispatch\",\n    \"highlight\",\n    \"tap\",\n    \"boundingbox\",\n    \"innertext\",\n    \"innerhtml\",\n    \"inputvalue\",\n    \"setvalue\",\n    \"count\",\n    \"styles\",\n    \"bringtofront\",\n    \"timezone\",\n    \"locale\",\n    \"geolocation\",\n    \"permissions\",\n    \"dialog\",\n    \"upload\",\n    \"addscript\",\n    \"addinitscript\",\n    \"addstyle\",\n    \"clipboard\",\n    \"wheel\",\n    \"device\",\n    \"screencast_start\",\n    \"screencast_stop\",\n    \"waitforurl\",\n    \"waitforloadstate\",\n    \"waitforfunction\",\n    \"frame\",\n    \"mainframe\",\n    \"getbyrole\",\n    \"getbytext\",\n    \"getbylabel\",\n    \"getbyplaceholder\",\n    \"getbyalttext\",\n    \"getbytitle\",\n    \"getbytestid\",\n    \"nth\",\n    \"find\",\n    \"evalhandle\",\n    \"drag\",\n    \"expose\",\n    \"pause\",\n    \"multiselect\",\n    \"responsebody\",\n    \"waitfordownload\",\n    \"window_new\",\n    \"diff_screenshot\",\n    \"video_start\",\n    \"video_stop\",\n    \"har_start\",\n    \"har_stop\",\n    \"route\",\n    \"unroute\",\n    \"requests\",\n    \"credentials\",\n    \"auth_save\",\n    \"auth_login\",\n    \"auth_list\",\n    \"auth_delete\",\n    \"auth_show\",\n    \"confirm\",\n    \"deny\",\n    \"swipe\",\n    \"device_list\",\n    \"input_mouse\",\n    \"input_keyboard\",\n    \"input_touch\",\n    \"keydown\",\n    \"keyup\",\n    \"inserttext\",\n    \"mousemove\",\n    \"mousedown\",\n    \"mouseup\",\n];\n\nfn minimal_command(action: &str, id: &str) -> Value {\n    let mut cmd = json!({ \"action\": action, \"id\": id });\n    let obj = cmd.as_object_mut().unwrap();\n\n    match action {\n        \"navigate\" | \"diff_url\" | \"waitforurl\" => {\n            obj.insert(\"url\".to_string(), json!(\"https://example.com\"));\n        }\n        \"evaluate\" | \"expose\" => {\n            obj.insert(\"script\".to_string(), json!(\"1\"));\n        }\n        \"click\" | \"dblclick\" | \"fill\" | \"type\" | \"press\" | \"hover\" | \"scroll\" | \"select\"\n        | \"check\" | \"uncheck\" | \"gettext\" | \"getattribute\" | \"isvisible\" | \"isenabled\"\n        | \"ischecked\" | \"focus\" | \"clear\" | \"selectall\" | \"scrollintoview\" | \"dispatch\"\n        | \"highlight\" | \"tap\" | \"boundingbox\" | \"innertext\" | \"innerhtml\" | \"inputvalue\"\n        | \"setvalue\" | \"count\" | \"find\" | \"nth\" | \"getbytext\" | \"getbylabel\"\n        | \"getbyplaceholder\" | \"getbyalttext\" | \"getbytitle\" | \"getbytestid\" => {\n            obj.insert(\"selector\".to_string(), json!(\"body\"));\n        }\n        \"getbyrole\" => {\n            obj.insert(\"role\".to_string(), json!(\"button\"));\n            obj.insert(\"selector\".to_string(), json!(\"body\"));\n        }\n        \"setcontent\" => {\n            obj.insert(\"html\".to_string(), json!(\"<html></html>\"));\n        }\n        \"cookies_set\" => {\n            obj.insert(\"name\".to_string(), json!(\"test\"));\n            obj.insert(\"value\".to_string(), json!(\"val\"));\n        }\n        \"storage_get\" | \"storage_set\" | \"storage_clear\" => {\n            obj.insert(\"origin\".to_string(), json!(\"https://example.com\"));\n        }\n        \"state_save\" | \"state_load\" | \"state_show\" | \"state_clear\" => {\n            obj.insert(\"path\".to_string(), json!(\"test-parity-state.json\"));\n        }\n        \"state_rename\" => {\n            obj.insert(\"path\".to_string(), json!(\"test-parity-state.json\"));\n            obj.insert(\"name\".to_string(), json!(\"renamed\"));\n        }\n        \"state_clean\" => {\n            obj.insert(\"days\".to_string(), json!(7));\n        }\n        \"credentials_set\" => {\n            obj.insert(\"name\".to_string(), json!(\"parity-test-cred\"));\n            obj.insert(\"username\".to_string(), json!(\"u\"));\n            obj.insert(\"password\".to_string(), json!(\"p\"));\n        }\n        \"auth_save\" => {\n            obj.insert(\"name\".to_string(), json!(\"parity-test-cred\"));\n            obj.insert(\"url\".to_string(), json!(\"https://example.com\"));\n            obj.insert(\"username\".to_string(), json!(\"u\"));\n            obj.insert(\"password\".to_string(), json!(\"p\"));\n        }\n        \"credentials_get\" | \"credentials_delete\" | \"auth_show\" | \"auth_delete\" => {\n            obj.insert(\"name\".to_string(), json!(\"parity-test-cred\"));\n        }\n        \"tab_switch\" | \"tab_close\" => {\n            obj.insert(\"index\".to_string(), json!(0));\n        }\n        \"viewport\" | \"user_agent\" | \"set_media\" | \"timezone\" | \"locale\" | \"geolocation\"\n        | \"permissions\" | \"device\" => {\n            obj.insert(\"value\".to_string(), json!(null));\n        }\n        \"headers\" => {\n            obj.insert(\"headers\".to_string(), json!({}));\n        }\n        \"offline\" => {\n            obj.insert(\"offline\".to_string(), json!(false));\n        }\n        \"wait\" => {\n            obj.insert(\"timeout\".to_string(), json!(100));\n        }\n        \"waitforloadstate\" => {\n            obj.insert(\"state\".to_string(), json!(\"load\"));\n        }\n        \"waitforfunction\" => {\n            obj.insert(\"script\".to_string(), json!(\"() => true\"));\n        }\n        \"frame\" => {\n            obj.insert(\"selector\".to_string(), json!(\"iframe\"));\n        }\n        \"addscript\" => {\n            obj.insert(\"content\".to_string(), json!(\"console.log('test')\"));\n        }\n        \"addinitscript\" => {\n            obj.insert(\"script\".to_string(), json!(\"console.log('init')\"));\n        }\n        \"addstyle\" => {\n            obj.insert(\"content\".to_string(), json!(\"body { color: red }\"));\n        }\n        \"wheel\" => {\n            obj.insert(\"deltaX\".to_string(), json!(0));\n            obj.insert(\"deltaY\".to_string(), json!(0));\n        }\n        \"upload\" => {\n            obj.insert(\"selector\".to_string(), json!(\"input[type=file]\"));\n            obj.insert(\"files\".to_string(), json!([]));\n        }\n        \"dialog\" => {\n            obj.insert(\"accept\".to_string(), json!(true));\n        }\n        \"credentials\" => {\n            obj.insert(\"username\".to_string(), json!(\"u\"));\n            obj.insert(\"password\".to_string(), json!(\"p\"));\n        }\n        \"auth_login\" => {\n            obj.insert(\"name\".to_string(), json!(\"parity-test-cred\"));\n        }\n        \"route\" => {\n            obj.insert(\"url\".to_string(), json!(\"*\"));\n            obj.insert(\"handler\".to_string(), json!(\"continue\"));\n        }\n        \"diff_snapshot\" | \"diff_screenshot\" => {\n            obj.insert(\"selector\".to_string(), json!(\"body\"));\n        }\n        \"recording_start\" | \"recording_restart\" => {\n            obj.insert(\"path\".to_string(), json!(\"/tmp/parity-recording.webm\"));\n        }\n        \"video_start\" => {\n            obj.insert(\"path\".to_string(), json!(\"/tmp/parity-video.webm\"));\n        }\n        \"profiler_start\" => {\n            obj.insert(\"path\".to_string(), json!(\"/tmp/parity-profile\"));\n        }\n        \"trace_stop\" | \"har_stop\" => {\n            obj.insert(\"path\".to_string(), json!(\"/tmp/parity-trace\"));\n        }\n        \"download\" => {\n            obj.insert(\"path\".to_string(), json!(\"/tmp/parity-download\"));\n        }\n        \"multiselect\" => {\n            obj.insert(\"selector\".to_string(), json!(\"select\"));\n            obj.insert(\"values\".to_string(), json!([]));\n        }\n        \"responsebody\" => {\n            obj.insert(\"url\".to_string(), json!(\"https://example.com\"));\n        }\n        \"waitfordownload\" => {\n            obj.insert(\"path\".to_string(), json!(\"/tmp/parity-download\"));\n        }\n        \"styles\" => {\n            obj.insert(\"selector\".to_string(), json!(\"body\"));\n            obj.insert(\"names\".to_string(), json!([]));\n        }\n        \"evalhandle\" => {\n            obj.insert(\"handle\".to_string(), json!(\"\"));\n            obj.insert(\"script\".to_string(), json!(\"h => h\"));\n        }\n        \"drag\" => {\n            obj.insert(\"selector\".to_string(), json!(\"body\"));\n            obj.insert(\"target\".to_string(), json!(\"body\"));\n        }\n        \"swipe\" => {\n            obj.insert(\"selector\".to_string(), json!(\"body\"));\n            obj.insert(\"direction\".to_string(), json!(\"left\"));\n        }\n        \"input_mouse\" | \"mousemove\" | \"mousedown\" | \"mouseup\" => {\n            obj.insert(\"x\".to_string(), json!(100));\n            obj.insert(\"y\".to_string(), json!(100));\n        }\n        \"input_keyboard\" | \"keydown\" | \"keyup\" => {\n            obj.insert(\"key\".to_string(), json!(\"a\"));\n        }\n        \"input_touch\" => {\n            obj.insert(\"type\".to_string(), json!(\"touchStart\"));\n            obj.insert(\"touchPoints\".to_string(), json!([]));\n        }\n        \"inserttext\" => {\n            obj.insert(\"text\".to_string(), json!(\"test\"));\n        }\n        _ => {}\n    }\n    cmd\n}\n\n// ---------------------------------------------------------------------------\n// 1. Action dispatch coverage\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn test_all_documented_actions_are_handled() {\n    let mut state = DaemonState::new();\n\n    for (i, action) in DOCUMENTED_ACTIONS.iter().enumerate() {\n        let id = format!(\"parity-{}\", i);\n        let cmd = minimal_command(action, &id);\n        let result = execute_command(&cmd, &mut state).await;\n\n        assert!(\n            result.get(\"id\").is_some(),\n            \"Action '{}': response missing 'id'\",\n            action\n        );\n\n        let error = result.get(\"error\").and_then(|v| v.as_str()).unwrap_or(\"\");\n\n        assert!(\n            !error.contains(\"Not yet implemented\"),\n            \"Action '{}' returned 'Not yet implemented')\",\n            action\n        );\n    }\n}\n\n// ---------------------------------------------------------------------------\n// 2. Response format consistency\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn test_success_response_format() {\n    let mut state = DaemonState::new();\n    let cmd = json!({ \"action\": \"state_list\", \"id\": \"fmt-1\" });\n    let result = execute_command(&cmd, &mut state).await;\n\n    assert_eq!(result[\"success\"], true);\n    assert!(result.get(\"id\").is_some());\n    assert!(result.get(\"data\").is_some());\n    assert!(result.get(\"error\").is_none());\n}\n\n#[tokio::test]\nasync fn test_error_response_format() {\n    let mut state = DaemonState::new();\n    let cmd = json!({ \"action\": \"nonexistent_action_xyz\", \"id\": \"fmt-2\" });\n    let result = execute_command(&cmd, &mut state).await;\n\n    assert_eq!(result[\"success\"], false);\n    assert!(result.get(\"id\").is_some());\n    assert!(result.get(\"error\").is_some());\n}\n\n// ---------------------------------------------------------------------------\n// 3. Credential/state actions work without a browser\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn test_state_list_without_browser() {\n    let mut state = DaemonState::new();\n    let cmd = json!({ \"action\": \"state_list\", \"id\": \"nb-1\" });\n    let result = execute_command(&cmd, &mut state).await;\n\n    assert_eq!(result[\"success\"], true);\n    assert!(result[\"data\"][\"files\"].is_array());\n}\n\n#[tokio::test]\nasync fn test_credentials_list_without_browser() {\n    let mut state = DaemonState::new();\n    let cmd = json!({ \"action\": \"credentials_list\", \"id\": \"nb-2\" });\n    let result = execute_command(&cmd, &mut state).await;\n\n    assert_eq!(result[\"success\"], true);\n    assert!(result[\"data\"][\"credentials\"].is_array() || result[\"data\"][\"profiles\"].is_array());\n}\n\n// ---------------------------------------------------------------------------\n// 4. New feature parity tests\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn test_auth_profile_name_validation() {\n    use super::auth;\n    let _key_guard = TestKeyGuard::new();\n    let valid = auth::credentials_set(\"valid-name_123\", \"u\", \"p\", None);\n    assert!(valid.is_ok());\n    let invalid = auth::credentials_set(\"invalid/name\", \"u\", \"p\", None);\n    assert!(invalid.is_err());\n    let invalid2 = auth::credentials_set(\"\", \"u\", \"p\", None);\n    assert!(invalid2.is_err());\n    let invalid3 = auth::credentials_set(\"has space\", \"u\", \"p\", None);\n    assert!(invalid3.is_err());\n    // Cleanup\n    let _ = auth::credentials_delete(\"valid-name_123\");\n}\n\n#[tokio::test]\nasync fn test_auth_save_and_show() {\n    use super::auth;\n    let _key_guard = TestKeyGuard::new();\n    let result = auth::auth_save(\n        \"parity-roundtrip\",\n        \"https://example.com\",\n        \"user\",\n        \"pass\",\n        Some(\"input#user\"),\n        None,\n        None,\n    );\n    assert!(result.is_ok());\n\n    let show = auth::auth_show(\"parity-roundtrip\");\n    assert!(show.is_ok());\n    let data = show.unwrap();\n    assert_eq!(data[\"profile\"][\"username\"], \"user\");\n    assert_eq!(data[\"profile\"][\"usernameSelector\"], \"input#user\");\n\n    let full = auth::credentials_get_full(\"parity-roundtrip\");\n    assert!(full.is_ok());\n    assert_eq!(full.unwrap().password, \"pass\");\n\n    // Cleanup\n    let _ = auth::credentials_delete(\"parity-roundtrip\");\n}\n\n#[tokio::test]\nasync fn test_har_start_stop_without_browser() {\n    let mut state = DaemonState::new();\n    // har_start requires a browser. Because execute_command auto-launches when\n    // no browser is present, the result depends on Chrome availability: success\n    // if Chrome is found (CI), failure if not. Both outcomes are valid.\n    let cmd = json!({ \"action\": \"har_start\", \"id\": \"har-1\" });\n    let result = execute_command(&cmd, &mut state).await;\n    let success = result[\"success\"].as_bool().unwrap_or(false);\n    if success {\n        assert!(state.har_recording);\n    } else {\n        assert!(result[\"error\"].as_str().is_some());\n    }\n}\n\n#[tokio::test]\nasync fn test_state_clean_action() {\n    let mut state = DaemonState::new();\n    let cmd = json!({ \"action\": \"state_clean\", \"id\": \"clean-1\", \"days\": 30 });\n    let result = execute_command(&cmd, &mut state).await;\n    assert_eq!(result[\"success\"], true);\n}\n\n#[tokio::test]\nasync fn test_daemon_state_new_defaults() {\n    let state = DaemonState::new();\n    assert!(state.browser.is_none());\n    assert!(!state.har_recording);\n    assert!(state.har_entries.is_empty());\n    assert!(state.pending_confirmation.is_none());\n    assert!(!state.request_tracking);\n    assert!(state.tracked_requests.is_empty());\n    assert!(state.active_frame_id.is_none());\n    assert!(state.webdriver_backend.is_none());\n    assert!(state.stream_client.is_none());\n}\n\n#[tokio::test]\nasync fn test_tracked_request_struct() {\n    use super::actions::TrackedRequest;\n    let tr = TrackedRequest {\n        url: \"https://example.com/api\".to_string(),\n        method: \"GET\".to_string(),\n        headers: json!({\"Accept\": \"text/html\"}),\n        timestamp: 12345,\n        resource_type: \"Document\".to_string(),\n    };\n    let serialized = serde_json::to_value(&tr).unwrap();\n    assert_eq!(serialized[\"url\"], \"https://example.com/api\");\n    assert_eq!(serialized[\"method\"], \"GET\");\n    assert_eq!(serialized[\"resourceType\"], \"Document\");\n    assert_eq!(serialized[\"timestamp\"], 12345);\n}\n\n#[tokio::test]\nasync fn test_request_tracking_state() {\n    let mut state = DaemonState::new();\n    assert!(!state.request_tracking);\n    assert!(state.tracked_requests.is_empty());\n\n    state.tracked_requests.push(super::actions::TrackedRequest {\n        url: \"https://example.com\".to_string(),\n        method: \"GET\".to_string(),\n        headers: json!({}),\n        timestamp: 1,\n        resource_type: \"Document\".to_string(),\n    });\n    state.tracked_requests.push(super::actions::TrackedRequest {\n        url: \"https://other.com\".to_string(),\n        method: \"POST\".to_string(),\n        headers: json!({}),\n        timestamp: 2,\n        resource_type: \"XHR\".to_string(),\n    });\n    assert_eq!(state.tracked_requests.len(), 2);\n\n    // Filter\n    let filtered: Vec<_> = state\n        .tracked_requests\n        .iter()\n        .filter(|r| r.url.contains(\"example\"))\n        .collect();\n    assert_eq!(filtered.len(), 1);\n    assert_eq!(filtered[0].url, \"https://example.com\");\n\n    // Clear\n    state.tracked_requests.clear();\n    assert!(state.tracked_requests.is_empty());\n}\n\n#[tokio::test]\nasync fn test_addscript_and_addinitscript_separate_dispatch() {\n    let mut state = DaemonState::new();\n\n    // Both should be handled (not \"Not yet implemented\") even without a browser\n    let cmd1 = json!({ \"action\": \"addscript\", \"id\": \"as-1\", \"content\": \"console.log(1)\" });\n    let result1 = execute_command(&cmd1, &mut state).await;\n    let err1 = result1[\"error\"].as_str().unwrap_or(\"\");\n    assert!(\n        !err1.contains(\"Not yet implemented\"),\n        \"addscript should be handled\"\n    );\n\n    let cmd2 = json!({ \"action\": \"addinitscript\", \"id\": \"ais-1\", \"script\": \"console.log(2)\" });\n    let result2 = execute_command(&cmd2, &mut state).await;\n    let err2 = result2[\"error\"].as_str().unwrap_or(\"\");\n    assert!(\n        !err2.contains(\"Not yet implemented\"),\n        \"addinitscript should be handled\"\n    );\n}\n\n#[tokio::test]\nasync fn test_frame_context_management() {\n    let mut state = DaemonState::new();\n    assert!(state.active_frame_id.is_none());\n\n    // Set a frame ID and verify it persists\n    state.active_frame_id = Some(\"child-frame-123\".to_string());\n    assert_eq!(state.active_frame_id.as_deref(), Some(\"child-frame-123\"));\n\n    // Clearing the frame ID (what mainframe does)\n    state.active_frame_id = None;\n    assert!(state.active_frame_id.is_none());\n}\n\n#[tokio::test]\nasync fn test_addstyle_supports_content_and_url() {\n    let mut state = DaemonState::new();\n\n    // Both content-based and url-based addstyle should be recognized\n    let cmd1 = json!({ \"action\": \"addstyle\", \"id\": \"style-1\", \"content\": \"body { color: red }\" });\n    let result1 = execute_command(&cmd1, &mut state).await;\n    let err1 = result1[\"error\"].as_str().unwrap_or(\"\");\n    assert!(!err1.contains(\"Not yet implemented\"));\n\n    let cmd2 =\n        json!({ \"action\": \"addstyle\", \"id\": \"style-2\", \"url\": \"https://example.com/style.css\" });\n    let result2 = execute_command(&cmd2, &mut state).await;\n    let err2 = result2[\"error\"].as_str().unwrap_or(\"\");\n    assert!(!err2.contains(\"Not yet implemented\"));\n}\n\n#[tokio::test]\nasync fn test_domain_filter_sanitize() {\n    use super::network::DomainFilter;\n    let filter = DomainFilter::new(\"example.com\");\n    assert!(filter.is_allowed(\"example.com\"));\n    assert!(!filter.is_allowed(\"evil.com\"));\n    filter.check_url(\"https://example.com/path\").unwrap();\n    assert!(filter.check_url(\"https://evil.com\").is_err());\n}\n\n#[tokio::test]\nasync fn test_state_find_auto_returns_none_for_nonexistent() {\n    use super::state;\n    let result = state::find_auto_state_file(\"nonexistent-session-xyz\");\n    assert!(result.is_none());\n}\n"
  },
  {
    "path": "cli/src/native/policy.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::env;\nuse std::fs;\nuse std::path::PathBuf;\n\n/// Result of a policy check for an action.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum PolicyResult {\n    /// Action is allowed.\n    Allow,\n    /// Action is blocked with the given reason.\n    Deny(String),\n    /// Action requires confirmation before proceeding.\n    RequiresConfirmation,\n}\n\n/// Policy configuration loaded from a JSON file.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ActionPolicy {\n    #[serde(skip)]\n    path: PathBuf,\n    #[serde(default)]\n    default: Option<String>,\n    #[serde(default)]\n    allow: Option<Vec<String>>,\n    #[serde(default)]\n    deny: Option<Vec<String>>,\n    #[serde(default)]\n    confirm: Option<Vec<String>>,\n}\n\n/// Confirmation categories parsed from AGENT_BROWSER_CONFIRM_ACTIONS.\n#[derive(Debug, Clone)]\npub struct ConfirmActions {\n    pub categories: HashSet<String>,\n}\n\nimpl ConfirmActions {\n    pub fn from_env() -> Option<Self> {\n        let val = env::var(\"AGENT_BROWSER_CONFIRM_ACTIONS\").ok()?;\n        if val.is_empty() {\n            return None;\n        }\n        let categories: HashSet<String> = val\n            .split(',')\n            .map(|s| s.trim().to_lowercase())\n            .filter(|s| !s.is_empty())\n            .collect();\n        if categories.is_empty() {\n            None\n        } else {\n            Some(Self { categories })\n        }\n    }\n\n    pub fn requires_confirmation(&self, action: &str) -> bool {\n        self.categories.contains(action)\n    }\n}\n\nimpl ActionPolicy {\n    /// Load policy from a JSON file at the given path.\n    pub fn load(path: &str) -> Result<Self, String> {\n        let path_buf = PathBuf::from(path);\n        let contents = fs::read_to_string(&path_buf)\n            .map_err(|e| format!(\"Failed to read policy file: {}\", e))?;\n        let mut policy: ActionPolicy =\n            serde_json::from_str(&contents).map_err(|e| format!(\"Invalid policy JSON: {}\", e))?;\n        policy.path = path_buf;\n        Ok(policy)\n    }\n\n    /// Load policy if AGENT_BROWSER_ACTION_POLICY env var is set.\n    /// Falls back to AGENT_BROWSER_POLICY for backwards compatibility.\n    pub fn load_if_exists() -> Option<Self> {\n        let path = env::var(\"AGENT_BROWSER_ACTION_POLICY\")\n            .or_else(|_| env::var(\"AGENT_BROWSER_POLICY\"))\n            .ok()?;\n        Self::load(&path).ok()\n    }\n\n    /// Check whether an action is allowed, denied, or requires confirmation.\n    pub fn check(&self, action: &str) -> PolicyResult {\n        if let Some(deny) = &self.deny {\n            if deny.iter().any(|a| a == action) {\n                return PolicyResult::Deny(format!(\"Action '{}' is denied by policy\", action));\n            }\n        }\n\n        if let Some(confirm) = &self.confirm {\n            if confirm.iter().any(|a| a == action) {\n                return PolicyResult::RequiresConfirmation;\n            }\n        }\n\n        if let Some(allow) = &self.allow {\n            if !allow.is_empty() && !allow.iter().any(|a| a == action) {\n                let is_default_deny = self\n                    .default\n                    .as_deref()\n                    .map(|d| d.eq_ignore_ascii_case(\"deny\"))\n                    .unwrap_or(true);\n                if is_default_deny {\n                    return PolicyResult::Deny(format!(\n                        \"Action '{}' is not in the allow list\",\n                        action\n                    ));\n                }\n            }\n        } else if let Some(ref default) = self.default {\n            if default.eq_ignore_ascii_case(\"deny\") {\n                return PolicyResult::Deny(format!(\n                    \"Action '{}' denied: default policy is deny\",\n                    action\n                ));\n            }\n        }\n\n        PolicyResult::Allow\n    }\n\n    /// Reload policy from the file. Re-reads the JSON and updates the policy.\n    pub fn reload(&mut self) -> Result<(), String> {\n        let contents = fs::read_to_string(&self.path)\n            .map_err(|e| format!(\"Failed to read policy file: {}\", e))?;\n        let mut policy: ActionPolicy =\n            serde_json::from_str(&contents).map_err(|e| format!(\"Invalid policy JSON: {}\", e))?;\n        policy.path = self.path.clone();\n        *self = policy;\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_utils::EnvGuard;\n\n    #[test]\n    fn test_policy_allow_whitelist() {\n        let json = r#\"{\"allow\": [\"click\", \"type\"], \"deny\": [], \"confirm\": []}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"click\"), PolicyResult::Allow);\n        assert_eq!(policy.check(\"type\"), PolicyResult::Allow);\n        assert!(matches!(policy.check(\"navigate\"), PolicyResult::Deny(_)));\n    }\n\n    #[test]\n    fn test_policy_deny() {\n        let json = r#\"{\"allow\": [], \"deny\": [\"delete\"], \"confirm\": []}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert!(matches!(policy.check(\"delete\"), PolicyResult::Deny(_)));\n    }\n\n    #[test]\n    fn test_policy_confirm() {\n        let json = r#\"{\"allow\": [], \"deny\": [], \"confirm\": [\"submit\"]}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"submit\"), PolicyResult::RequiresConfirmation);\n    }\n\n    #[test]\n    fn test_policy_deny_takes_precedence() {\n        let json = r#\"{\"allow\": [\"danger\"], \"deny\": [\"danger\"], \"confirm\": []}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert!(matches!(policy.check(\"danger\"), PolicyResult::Deny(_)));\n    }\n\n    #[test]\n    fn test_policy_confirm_takes_precedence_over_allow() {\n        let json = r#\"{\"allow\": [\"submit\"], \"deny\": [], \"confirm\": [\"submit\"]}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"submit\"), PolicyResult::RequiresConfirmation);\n    }\n\n    #[test]\n    fn test_policy_empty_allow_allows_all() {\n        let json = r#\"{\"allow\": [], \"deny\": [], \"confirm\": []}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"anything\"), PolicyResult::Allow);\n    }\n\n    #[test]\n    fn test_policy_missing_allow_allows_all() {\n        let json = r#\"{\"deny\": []}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"anything\"), PolicyResult::Allow);\n    }\n\n    #[test]\n    fn test_policy_default_allow() {\n        let json = r#\"{\"default\": \"allow\", \"deny\": [\"navigate\"]}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"click\"), PolicyResult::Allow);\n        assert!(matches!(policy.check(\"navigate\"), PolicyResult::Deny(_)));\n    }\n\n    #[test]\n    fn test_policy_default_deny() {\n        let json = r#\"{\"default\": \"deny\", \"allow\": [\"click\"]}\"#;\n        let policy: ActionPolicy = serde_json::from_str(json).unwrap();\n        assert_eq!(policy.check(\"click\"), PolicyResult::Allow);\n        assert!(matches!(policy.check(\"navigate\"), PolicyResult::Deny(_)));\n    }\n\n    #[test]\n    fn test_confirm_actions_from_env() {\n        let _guard = EnvGuard::new(&[\"AGENT_BROWSER_CONFIRM_ACTIONS\"]);\n        _guard.set(\"AGENT_BROWSER_CONFIRM_ACTIONS\", \"navigate,click,fill\");\n        let ca = ConfirmActions::from_env().unwrap();\n        assert!(ca.requires_confirmation(\"navigate\"));\n        assert!(ca.requires_confirmation(\"click\"));\n        assert!(ca.requires_confirmation(\"fill\"));\n        assert!(!ca.requires_confirmation(\"screenshot\"));\n    }\n}\n"
  },
  {
    "path": "cli/src/native/providers.rs",
    "content": "//! Browser provider connections for remote CDP sessions.\n//!\n//! Supports Browserbase, Browserless, Browser Use, and Kernel providers.\n//! Each provider returns a CDP WebSocket URL for connecting via BrowserManager.\n\nuse serde_json::{json, Value};\nuse std::env;\n\n/// Provider session info for cleanup on failure.\npub struct ProviderSession {\n    pub provider: String,\n    pub session_id: String,\n}\n\n/// Connects to the specified browser provider and returns a CDP WebSocket URL\n/// along with session info for cleanup on failure.\npub async fn connect_provider(\n    provider_name: &str,\n) -> Result<(String, Option<ProviderSession>), String> {\n    match provider_name.to_lowercase().as_str() {\n        \"browserbase\" => connect_browserbase().await,\n        \"browserless\" => connect_browserless().await,\n        \"browser-use\" | \"browseruse\" => connect_browser_use().await,\n        \"kernel\" => connect_kernel().await,\n        _ => Err(format!(\n            \"Unknown provider '{}'. Supported: browserbase, browserless, browser-use, kernel\",\n            provider_name\n        )),\n    }\n}\n\n/// Close a provider session (call on CDP connect failure).\npub async fn close_provider_session(session: &ProviderSession) {\n    let client = reqwest::Client::new();\n    match session.provider.as_str() {\n        \"browserbase\" => {\n            if let Ok(api_key) = env::var(\"BROWSERBASE_API_KEY\") {\n                let _ = client\n                    .post(format!(\n                        \"https://api.browserbase.com/v1/sessions/{}\",\n                        session.session_id\n                    ))\n                    .header(\"Content-Type\", \"application/json\")\n                    .header(\"X-BB-API-Key\", &api_key)\n                    .json(&serde_json::json!({ \"status\": \"REQUEST_RELEASE\" }))\n                    .send()\n                    .await;\n            }\n        }\n        \"browser-use\" => {\n            if let Ok(api_key) = env::var(\"BROWSER_USE_API_KEY\") {\n                let _ = client\n                    .patch(format!(\n                        \"https://api.browser-use.com/api/v2/browsers/{}\",\n                        session.session_id\n                    ))\n                    .header(\"X-Browser-Use-API-Key\", &api_key)\n                    .header(\"Content-Type\", \"application/json\")\n                    .json(&json!({ \"action\": \"stop\" }))\n                    .send()\n                    .await;\n            }\n        }\n        \"browserless\" => {\n            // session_id holds the stop URL for browserless\n            let _ = client.delete(&session.session_id).send().await;\n        }\n        \"kernel\" => {\n            if let Ok(api_key) = env::var(\"KERNEL_API_KEY\") {\n                let endpoint = env::var(\"KERNEL_ENDPOINT\")\n                    .unwrap_or_else(|_| \"https://api.onkernel.com\".to_string());\n                let _ = client\n                    .delete(format!(\n                        \"{}/browsers/{}\",\n                        endpoint.trim_end_matches('/'),\n                        session.session_id\n                    ))\n                    .header(\"Authorization\", format!(\"Bearer {}\", api_key))\n                    .send()\n                    .await;\n            }\n        }\n        _ => {}\n    }\n}\n\nasync fn connect_browserbase() -> Result<(String, Option<ProviderSession>), String> {\n    let api_key = env::var(\"BROWSERBASE_API_KEY\")\n        .map_err(|_| \"BROWSERBASE_API_KEY environment variable is not set\")?;\n\n    let client = reqwest::Client::new();\n    let response = client\n        .post(\"https://api.browserbase.com/v1/sessions\")\n        .header(\"X-BB-API-Key\", &api_key)\n        .send()\n        .await\n        .map_err(|e| format!(\"Browserbase request failed: {}\", e))?;\n\n    let status = response.status();\n    let body = response\n        .text()\n        .await\n        .map_err(|e| format!(\"Failed to read Browserbase response: {}\", e))?;\n\n    if !status.is_success() {\n        return Err(format!(\n            \"Browserbase API error ({}): {}\",\n            status.as_u16(),\n            body\n        ));\n    }\n\n    let json: Value =\n        serde_json::from_str(&body).map_err(|e| format!(\"Invalid Browserbase response: {}\", e))?;\n\n    let session_id = json\n        .get(\"id\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"\")\n        .to_string();\n\n    let ws_url = json\n        .get(\"connectUrl\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .ok_or_else(|| \"Browserbase response missing connectUrl\".to_string())?;\n\n    Ok((\n        ws_url,\n        Some(ProviderSession {\n            provider: \"browserbase\".to_string(),\n            session_id,\n        }),\n    ))\n}\n\nasync fn connect_browserless() -> Result<(String, Option<ProviderSession>), String> {\n    let api_key = env::var(\"BROWSERLESS_API_KEY\")\n        .map_err(|_| \"BROWSERLESS_API_KEY environment variable is not set\")?;\n\n    let api_url = env::var(\"BROWSERLESS_API_URL\")\n        .unwrap_or_else(|_| \"https://production-sfo.browserless.io\".to_string());\n    let browser_type =\n        env::var(\"BROWSERLESS_BROWSER_TYPE\").unwrap_or_else(|_| \"chromium\".to_string());\n\n    let supported = [\"chromium\", \"chrome\"];\n    if !supported.contains(&browser_type.as_str()) {\n        return Err(format!(\n            \"BROWSERLESS_BROWSER_TYPE \\\"{}\\\" is not supported. Only {} are allowed.\",\n            browser_type,\n            supported.join(\", \")\n        ));\n    }\n\n    let ttl: u64 = env::var(\"BROWSERLESS_TTL\")\n        .ok()\n        .and_then(|v| v.parse().ok())\n        .unwrap_or(300000);\n    let stealth = env::var(\"BROWSERLESS_STEALTH\")\n        .map(|v| v == \"1\" || v.eq_ignore_ascii_case(\"true\"))\n        .unwrap_or(true);\n\n    let url = format!(\"{}/session\", api_url.trim_end_matches('/'));\n\n    let client = reqwest::Client::new();\n    let response = client\n        .post(&url)\n        .query(&[(\"token\", &api_key)])\n        .header(\"Content-Type\", \"application/json\")\n        .json(&json!({\n            \"ttl\": ttl,\n            \"stealth\": stealth,\n            \"browser\": browser_type,\n        }))\n        .send()\n        .await\n        .map_err(|e| format!(\"Browserless request failed: {}\", e))?;\n\n    let status = response.status();\n    let body = response\n        .text()\n        .await\n        .map_err(|e| format!(\"Failed to read Browserless response: {}\", e))?;\n\n    if !status.is_success() {\n        return Err(format!(\n            \"Browserless API error ({}): {}\",\n            status.as_u16(),\n            body\n        ));\n    }\n\n    let json: Value =\n        serde_json::from_str(&body).map_err(|e| format!(\"Invalid Browserless response: {}\", e))?;\n\n    let connect_url = json\n        .get(\"connect\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .ok_or_else(|| \"Browserless response missing 'connect' URL\".to_string())?;\n\n    let stop_url = json\n        .get(\"stop\")\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .ok_or_else(|| \"Browserless response missing 'stop' URL\".to_string())?;\n\n    Ok((\n        connect_url,\n        Some(ProviderSession {\n            provider: \"browserless\".to_string(),\n            // Store the stop URL as the session_id for cleanup\n            session_id: stop_url,\n        }),\n    ))\n}\n\nasync fn connect_browser_use() -> Result<(String, Option<ProviderSession>), String> {\n    let api_key = env::var(\"BROWSER_USE_API_KEY\")\n        .map_err(|_| \"BROWSER_USE_API_KEY environment variable is not set\")?;\n\n    let client = reqwest::Client::new();\n    let response = client\n        .post(\"https://api.browser-use.com/api/v2/browsers\")\n        .header(\"Content-Type\", \"application/json\")\n        .header(\"X-Browser-Use-API-Key\", &api_key)\n        .json(&json!({}))\n        .send()\n        .await\n        .map_err(|e| format!(\"Browser Use request failed: {}\", e))?;\n\n    let status = response.status();\n    let body = response\n        .text()\n        .await\n        .map_err(|e| format!(\"Failed to read Browser Use response: {}\", e))?;\n\n    if !status.is_success() {\n        return Err(format!(\n            \"Browser Use API error ({}): {}\",\n            status.as_u16(),\n            body\n        ));\n    }\n\n    let json: Value =\n        serde_json::from_str(&body).map_err(|e| format!(\"Invalid Browser Use response: {}\", e))?;\n\n    let session_id = json\n        .get(\"id\")\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"\")\n        .to_string();\n\n    let ws_url = json\n        .get(\"cdp_url\")\n        .or_else(|| json.get(\"cdpUrl\"))\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .ok_or_else(|| \"Browser Use response missing cdp_url or cdpUrl\".to_string())?;\n\n    Ok((\n        ws_url,\n        Some(ProviderSession {\n            provider: \"browser-use\".to_string(),\n            session_id,\n        }),\n    ))\n}\n\nasync fn connect_kernel() -> Result<(String, Option<ProviderSession>), String> {\n    let api_key = env::var(\"KERNEL_API_KEY\").ok();\n    let endpoint =\n        env::var(\"KERNEL_ENDPOINT\").unwrap_or_else(|_| \"https://api.onkernel.com\".to_string());\n\n    let url = format!(\"{}/browsers\", endpoint.trim_end_matches('/'));\n\n    let headless = env::var(\"KERNEL_HEADLESS\")\n        .map(|v| v == \"1\" || v.eq_ignore_ascii_case(\"true\"))\n        .unwrap_or(true);\n    let stealth = env::var(\"KERNEL_STEALTH\")\n        .map(|v| v == \"1\" || v.eq_ignore_ascii_case(\"true\"))\n        .unwrap_or(false);\n    let timeout_seconds = env::var(\"KERNEL_TIMEOUT_SECONDS\")\n        .ok()\n        .and_then(|v| v.parse::<u64>().ok())\n        .unwrap_or(300);\n\n    let mut body = json!({\n        \"headless\": headless,\n        \"stealth\": stealth,\n        \"timeout_seconds\": timeout_seconds,\n    });\n\n    if let Ok(profile) = env::var(\"KERNEL_PROFILE_NAME\") {\n        if !profile.is_empty() {\n            body.as_object_mut()\n                .unwrap()\n                .insert(\"profile\".to_string(), json!(profile));\n        }\n    }\n\n    let client = reqwest::Client::new();\n    let mut request = client.post(&url).header(\"Content-Type\", \"application/json\");\n    if let Some(ref key) = api_key {\n        request = request.header(\"Authorization\", format!(\"Bearer {}\", key));\n    }\n    let response = request\n        .json(&body)\n        .send()\n        .await\n        .map_err(|e| format!(\"Kernel request failed: {}\", e))?;\n\n    let status = response.status();\n    let resp_body = response\n        .text()\n        .await\n        .map_err(|e| format!(\"Failed to read Kernel response: {}\", e))?;\n\n    if !status.is_success() {\n        return Err(format!(\n            \"Kernel API error ({}): {}\",\n            status.as_u16(),\n            resp_body\n        ));\n    }\n\n    let json: Value =\n        serde_json::from_str(&resp_body).map_err(|e| format!(\"Invalid Kernel response: {}\", e))?;\n\n    let session_id = json\n        .get(\"session_id\")\n        .or_else(|| json.get(\"id\"))\n        .and_then(|v| v.as_str())\n        .unwrap_or(\"\")\n        .to_string();\n\n    let ws_url = json\n        .get(\"cdp_ws_url\")\n        .or_else(|| json.get(\"connectUrl\"))\n        .or_else(|| json.get(\"connect_url\"))\n        .or_else(|| json.get(\"cdpUrl\"))\n        .or_else(|| json.get(\"cdp_url\"))\n        .and_then(|v| v.as_str())\n        .map(String::from)\n        .ok_or_else(|| {\n            \"Kernel response missing cdp_ws_url, connectUrl, connect_url, cdpUrl, or cdp_url\"\n                .to_string()\n        })?;\n\n    Ok((\n        ws_url,\n        Some(ProviderSession {\n            provider: \"kernel\".to_string(),\n            session_id,\n        }),\n    ))\n}\n"
  },
  {
    "path": "cli/src/native/recording.rs",
    "content": "use serde_json::{json, Value};\nuse std::process::Stdio;\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::Arc;\nuse std::time::Duration;\nuse tokio::io::AsyncWriteExt;\nuse tokio::sync::oneshot;\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::{CaptureScreenshotParams, CaptureScreenshotResult};\n\nconst CAPTURE_INTERVAL_MS: u64 = 100;\nconst CAPTURE_FPS: u32 = 10;\n\npub struct RecordingState {\n    pub active: bool,\n    pub output_path: String,\n    pub frame_count: u64,\n    pub capture_task: Option<tokio::task::JoinHandle<Result<(), String>>>,\n    pub shared_frame_count: Option<Arc<AtomicU64>>,\n    pub cancel_tx: Option<oneshot::Sender<()>>,\n}\n\nimpl RecordingState {\n    pub fn new() -> Self {\n        Self {\n            active: false,\n            output_path: String::new(),\n            frame_count: 0,\n            capture_task: None,\n            shared_frame_count: None,\n            cancel_tx: None,\n        }\n    }\n}\n\npub fn recording_start(state: &mut RecordingState, path: &str) -> Result<Value, String> {\n    if state.active {\n        return Err(\"Recording already active\".to_string());\n    }\n\n    state.active = true;\n    state.output_path = path.to_string();\n    state.frame_count = 0;\n\n    Ok(json!({ \"started\": true, \"path\": path }))\n}\n\npub fn recording_stop(state: &mut RecordingState) -> Result<Value, String> {\n    if !state.active {\n        return Err(\"No recording in progress\".to_string());\n    }\n\n    state.active = false;\n\n    if state.frame_count == 0 {\n        return Err(\"No frames captured\".to_string());\n    }\n\n    Ok(json!({ \"path\": &state.output_path, \"frames\": state.frame_count }))\n}\n\npub fn recording_restart(state: &mut RecordingState, path: &str) -> Result<Value, String> {\n    let previous = if state.active {\n        let stop_result = recording_stop(state);\n        stop_result\n            .ok()\n            .and_then(|v| v.get(\"path\").and_then(|p| p.as_str()).map(String::from))\n    } else {\n        None\n    };\n\n    recording_start(state, path)?;\n\n    Ok(json!({\n        \"restarted\": true,\n        \"previousPath\": previous,\n        \"path\": path,\n    }))\n}\n\nfn build_ffmpeg_command(output_path: &str) -> tokio::process::Command {\n    let mut cmd = tokio::process::Command::new(\"ffmpeg\");\n\n    cmd.args([\"-y\"])\n        .args([\"-avioflags\", \"direct\"])\n        .args([\n            \"-fpsprobesize\",\n            \"0\",\n            \"-probesize\",\n            \"32\",\n            \"-analyzeduration\",\n            \"0\",\n        ])\n        .args([\n            \"-f\",\n            \"image2pipe\",\n            \"-c:v\",\n            \"mjpeg\",\n            \"-framerate\",\n            &CAPTURE_FPS.to_string(),\n            \"-i\",\n            \"pipe:0\",\n        ])\n        .args([\"-vf\", \"pad=ceil(iw/2)*2:ceil(ih/2)*2\"]);\n\n    if output_path.ends_with(\".webm\") {\n        cmd.args([\"-c:v\", \"libvpx\", \"-crf\", \"30\", \"-b:v\", \"1M\"]);\n    } else {\n        cmd.args([\"-c:v\", \"libx264\", \"-preset\", \"ultrafast\"]);\n    }\n\n    cmd.args([\"-pix_fmt\", \"yuv420p\", \"-threads\", \"1\"])\n        .arg(output_path)\n        .stdin(Stdio::piped())\n        .stdout(Stdio::null())\n        .stderr(Stdio::piped())\n        .kill_on_drop(true);\n\n    cmd\n}\n\n/// Spawn a background task that captures screenshots at a fixed interval\n/// and pipes them to ffmpeg in real-time.\npub fn spawn_recording_task(\n    client: Arc<CdpClient>,\n    session_id: String,\n    output_path: String,\n    shared_count: Arc<AtomicU64>,\n    cancel_rx: oneshot::Receiver<()>,\n) -> tokio::task::JoinHandle<Result<(), String>> {\n    tokio::spawn(async move {\n        let mut cancel_rx = std::pin::pin!(cancel_rx);\n\n        let mut ffmpeg = build_ffmpeg_command(&output_path).spawn().map_err(|e| {\n            format!(\n                \"ffmpeg not found or failed to execute: {}. Install ffmpeg to enable recording.\",\n                e\n            )\n        })?;\n\n        let mut stdin = ffmpeg\n            .stdin\n            .take()\n            .ok_or_else(|| \"Failed to open ffmpeg stdin\".to_string())?;\n\n        let mut interval = tokio::time::interval(Duration::from_millis(CAPTURE_INTERVAL_MS));\n        interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);\n\n        let params = CaptureScreenshotParams {\n            format: Some(\"jpeg\".to_string()),\n            quality: Some(80),\n            clip: None,\n            from_surface: Some(true),\n            capture_beyond_viewport: None,\n        };\n\n        loop {\n            tokio::select! {\n                _ = &mut cancel_rx => break,\n                _ = interval.tick() => {}\n            }\n\n            let result: Result<CaptureScreenshotResult, _> = client\n                .send_command_typed(\"Page.captureScreenshot\", &params, Some(&session_id))\n                .await;\n\n            let screenshot = match result {\n                Ok(s) => s,\n                Err(e) => {\n                    if e.contains(\"Target closed\") || e.contains(\"not found\") {\n                        break;\n                    }\n                    continue;\n                }\n            };\n\n            let bytes = match base64::Engine::decode(\n                &base64::engine::general_purpose::STANDARD,\n                &screenshot.data,\n            ) {\n                Ok(b) => b,\n                Err(_) => continue,\n            };\n\n            if stdin.write_all(&bytes).await.is_err() {\n                break;\n            }\n            shared_count.fetch_add(1, Ordering::Relaxed);\n        }\n\n        drop(stdin);\n\n        let output = ffmpeg\n            .wait_with_output()\n            .await\n            .map_err(|e| format!(\"ffmpeg wait failed: {}\", e))?;\n\n        if !output.status.success() {\n            let stderr = String::from_utf8_lossy(&output.stderr);\n            return Err(format!(\n                \"ffmpeg failed: {}\",\n                stderr.chars().take(300).collect::<String>()\n            ));\n        }\n\n        Ok(())\n    })\n}\n\npub async fn stop_recording_task(state: &mut RecordingState) -> Result<(), String> {\n    if let Some(tx) = state.cancel_tx.take() {\n        let _ = tx.send(());\n    }\n\n    let counter = state.shared_frame_count.take();\n    let handle = state.capture_task.take();\n\n    let result = if let Some(h) = handle {\n        match h.await {\n            Ok(Ok(())) => Ok(()),\n            Ok(Err(e)) => Err(e),\n            Err(e) => Err(format!(\"Recording task panicked: {}\", e)),\n        }\n    } else {\n        Ok(())\n    };\n\n    if let Some(c) = counter {\n        state.frame_count = c.load(Ordering::Relaxed);\n    }\n\n    result\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_recording_state_new() {\n        let state = RecordingState::new();\n        assert!(!state.active);\n        assert!(state.output_path.is_empty());\n        assert_eq!(state.frame_count, 0);\n    }\n\n    #[test]\n    fn test_recording_start_sets_active() {\n        let mut state = RecordingState::new();\n        let result = recording_start(&mut state, \"/tmp/test.mp4\");\n        assert!(result.is_ok());\n        assert!(state.active);\n        assert_eq!(state.output_path, \"/tmp/test.mp4\");\n        assert_eq!(state.frame_count, 0);\n    }\n\n    #[test]\n    fn test_recording_start_while_active() {\n        let mut state = RecordingState::new();\n        recording_start(&mut state, \"/tmp/test1.mp4\").unwrap();\n        let result = recording_start(&mut state, \"/tmp/test2.mp4\");\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"already active\"));\n    }\n\n    #[test]\n    fn test_recording_stop_not_active() {\n        let mut state = RecordingState::new();\n        let result = recording_stop(&mut state);\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"No recording\"));\n    }\n\n    #[test]\n    fn test_recording_stop_no_frames() {\n        let mut state = RecordingState::new();\n        recording_start(&mut state, \"/tmp/test.mp4\").unwrap();\n        let result = recording_stop(&mut state);\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"No frames\"));\n        assert!(!state.active);\n    }\n\n    #[test]\n    fn test_recording_restart_while_inactive() {\n        let mut state = RecordingState::new();\n        let result = recording_restart(&mut state, \"/tmp/new.webm\");\n        assert!(result.is_ok());\n        assert!(state.active);\n        assert_eq!(state.output_path, \"/tmp/new.webm\");\n    }\n\n    #[test]\n    fn test_recording_restart_while_active() {\n        let mut state = RecordingState::new();\n        recording_start(&mut state, \"/tmp/old.webm\").unwrap();\n        state.frame_count = 10;\n        let result = recording_restart(&mut state, \"/tmp/new.webm\").unwrap();\n        assert!(state.active);\n        assert_eq!(state.output_path, \"/tmp/new.webm\");\n        assert_eq!(state.frame_count, 0);\n        assert_eq!(result[\"previousPath\"], \"/tmp/old.webm\");\n    }\n\n    #[test]\n    fn test_build_ffmpeg_command_webm() {\n        let cmd = build_ffmpeg_command(\"/tmp/out.webm\");\n        let args: Vec<&std::ffi::OsStr> = cmd.as_std().get_args().collect();\n        let args_str: Vec<&str> = args.iter().filter_map(|a| a.to_str()).collect();\n        assert!(args_str.contains(&\"libvpx\"));\n        assert!(args_str.contains(&\"/tmp/out.webm\"));\n    }\n\n    #[test]\n    fn test_build_ffmpeg_command_mp4() {\n        let cmd = build_ffmpeg_command(\"/tmp/out.mp4\");\n        let args: Vec<&std::ffi::OsStr> = cmd.as_std().get_args().collect();\n        let args_str: Vec<&str> = args.iter().filter_map(|a| a.to_str()).collect();\n        assert!(args_str.contains(&\"libx264\"));\n        assert!(args_str.contains(&\"/tmp/out.mp4\"));\n    }\n}\n"
  },
  {
    "path": "cli/src/native/screenshot.rs",
    "content": "use serde::Serialize;\nuse serde_json::Value;\nuse std::path::PathBuf;\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::*;\nuse super::element::RefMap;\n\nconst ANNOTATION_OVERLAY_ID: &str = \"__agent_browser_annotations__\";\n\n#[derive(Debug, Clone)]\nstruct Rect {\n    x: f64,\n    y: f64,\n    width: f64,\n    height: f64,\n}\n\n#[derive(Debug, Clone)]\nstruct RawAnnotation {\n    ref_id: String,\n    number: u64,\n    role: String,\n    name: Option<String>,\n    rect: Rect,\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct AnnotationBox {\n    pub x: i64,\n    pub y: i64,\n    pub width: i64,\n    pub height: i64,\n}\n\n#[derive(Debug, Clone)]\npub struct ScreenshotAnnotation {\n    pub ref_id: String,\n    pub number: u64,\n    pub role: String,\n    pub name: Option<String>,\n    pub box_: AnnotationBox,\n}\n\n#[derive(Debug, Clone)]\npub struct ScreenshotResult {\n    pub path: String,\n    pub base64: String,\n    pub annotations: Vec<ScreenshotAnnotation>,\n}\n\n#[derive(Debug, Clone)]\npub struct ScreenshotOptions {\n    pub selector: Option<String>,\n    pub path: Option<String>,\n    pub full_page: bool,\n    pub format: String,\n    pub quality: Option<i32>,\n    pub annotate: bool,\n    pub output_dir: Option<String>,\n}\n\nimpl Default for ScreenshotOptions {\n    fn default() -> Self {\n        Self {\n            selector: None,\n            path: None,\n            full_page: false,\n            format: \"png\".to_string(),\n            quality: None,\n            annotate: false,\n            output_dir: None,\n        }\n    }\n}\n\nimpl Serialize for ScreenshotAnnotation {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n        use serde::ser::SerializeStruct;\n\n        let mut state = serializer.serialize_struct(\"ScreenshotAnnotation\", 5)?;\n        state.serialize_field(\"ref\", &self.ref_id)?;\n        state.serialize_field(\"number\", &self.number)?;\n        state.serialize_field(\"role\", &self.role)?;\n        if let Some(name) = &self.name {\n            state.serialize_field(\"name\", name)?;\n        }\n        state.serialize_field(\"box\", &self.box_)?;\n        state.end()\n    }\n}\n\n/// Captures a screenshot via CDP and optionally overlays numbered annotations\n/// that mirror the Node.js screenshot `annotate` mode.\npub async fn take_screenshot(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    options: &ScreenshotOptions,\n) -> Result<ScreenshotResult, String> {\n    let target_rect = if options.annotate {\n        match options.selector.as_deref() {\n            Some(selector) => get_rect_for_selector(client, session_id, ref_map, selector).await?,\n            None => None,\n        }\n    } else {\n        None\n    };\n\n    let raw_annotations = if options.annotate {\n        collect_annotations(client, session_id, ref_map).await?\n    } else {\n        Vec::new()\n    };\n\n    let overlay_items = filter_annotations(raw_annotations, target_rect.as_ref());\n    let overlay_injected = if options.annotate && !overlay_items.is_empty() {\n        inject_annotation_overlay(client, session_id, &overlay_items).await?;\n        true\n    } else {\n        false\n    };\n\n    let base64 = capture_screenshot_base64(client, session_id, ref_map, options).await;\n\n    if overlay_injected {\n        let _ = remove_annotation_overlay(client, session_id).await;\n    }\n\n    let base64 = base64?;\n    let annotations = if options.annotate {\n        let scroll = if options.full_page {\n            Some(get_scroll_offsets(client, session_id).await?)\n        } else {\n            None\n        };\n        project_annotations(&overlay_items, target_rect.as_ref(), scroll)\n    } else {\n        Vec::new()\n    };\n\n    let ext = if options.format == \"jpeg\" {\n        \"jpg\"\n    } else {\n        \"png\"\n    };\n    let path = save_screenshot(\n        &base64,\n        options.path.as_deref(),\n        ext,\n        options.output_dir.as_deref(),\n    )?;\n\n    Ok(ScreenshotResult {\n        path,\n        base64,\n        annotations,\n    })\n}\n\nasync fn capture_screenshot_base64(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    options: &ScreenshotOptions,\n) -> Result<String, String> {\n    let mut params = CaptureScreenshotParams {\n        format: Some(options.format.clone()),\n        quality: if options.format == \"jpeg\" {\n            options.quality.or(Some(80))\n        } else {\n            None\n        },\n        clip: None,\n        from_surface: Some(true),\n        capture_beyond_viewport: if options.full_page { Some(true) } else { None },\n    };\n\n    if options.full_page {\n        let metrics: Value = client\n            .send_command_no_params(\"Page.getLayoutMetrics\", Some(session_id))\n            .await?;\n\n        let content_size = metrics\n            .get(\"contentSize\")\n            .or_else(|| metrics.get(\"cssContentSize\"));\n        if let Some(size) = content_size {\n            let width = size.get(\"width\").and_then(|v| v.as_f64()).unwrap_or(1280.0);\n            let height = size.get(\"height\").and_then(|v| v.as_f64()).unwrap_or(720.0);\n\n            params.clip = Some(Viewport {\n                x: 0.0,\n                y: 0.0,\n                width,\n                height,\n                scale: 1.0,\n            });\n        }\n    } else if let Some(ref selector) = options.selector {\n        if let Some(rect) = get_rect_for_selector(client, session_id, ref_map, selector).await? {\n            params.clip = Some(Viewport {\n                x: rect.x,\n                y: rect.y,\n                width: rect.width,\n                height: rect.height,\n                scale: 1.0,\n            });\n        }\n    }\n\n    let result: CaptureScreenshotResult = client\n        .send_command_typed(\"Page.captureScreenshot\", &params, Some(session_id))\n        .await?;\n\n    Ok(result.data)\n}\n\nasync fn collect_annotations(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n) -> Result<Vec<RawAnnotation>, String> {\n    let entries = ref_map.entries_sorted();\n    if entries.is_empty() {\n        return Ok(Vec::new());\n    }\n\n    // Collect entries that have backend_node_ids for batch resolution.\n    let with_backend_ids: Vec<(String, super::element::RefEntry, i64)> = entries\n        .iter()\n        .filter_map(|(ref_id, entry)| {\n            entry\n                .backend_node_id\n                .map(|bid| (ref_id.clone(), entry.clone(), bid))\n        })\n        .collect();\n\n    if with_backend_ids.is_empty() {\n        return Ok(Vec::new());\n    }\n\n    // Batch-resolve all backend_node_ids to object IDs using concurrent CDP calls.\n    let resolve_futures: Vec<_> = with_backend_ids\n        .iter()\n        .map(|(_, _, backend_node_id)| {\n            client.send_command(\n                \"DOM.resolveNode\",\n                Some(serde_json::json!({\n                    \"backendNodeId\": backend_node_id,\n                    \"objectGroup\": \"agent-browser-annotate\"\n                })),\n                Some(session_id),\n            )\n        })\n        .collect();\n\n    let resolve_results = futures_util::future::join_all(resolve_futures).await;\n\n    // Collect resolved object IDs paired with their ref info.\n    let mut resolved: Vec<(String, super::element::RefEntry, String)> = Vec::new();\n    for (i, result) in resolve_results.into_iter().enumerate() {\n        if let Ok(val) = result {\n            if let Some(oid) = val\n                .get(\"object\")\n                .and_then(|o| o.get(\"objectId\"))\n                .and_then(|v| v.as_str())\n            {\n                let (ref_id, entry, _) = &with_backend_ids[i];\n                resolved.push((ref_id.clone(), entry.clone(), oid.to_string()));\n            }\n        }\n    }\n\n    if resolved.is_empty() {\n        return Ok(Vec::new());\n    }\n\n    // Batch-get bounding rects for all resolved elements using concurrent CDP calls.\n    let rect_futures: Vec<_> = resolved\n        .iter()\n        .map(|(_, _, object_id)| get_rect_for_object(client, session_id, object_id))\n        .collect();\n\n    let rect_results = futures_util::future::join_all(rect_futures).await;\n\n    let mut annotations = Vec::new();\n    for (i, rect_result) in rect_results.into_iter().enumerate() {\n        let rect = match rect_result {\n            Ok(Some(r)) if r.width > 0.0 && r.height > 0.0 => r,\n            _ => continue,\n        };\n\n        let (ref_id, entry, _) = &resolved[i];\n        let number = ref_id\n            .strip_prefix('e')\n            .and_then(|n| n.parse::<u64>().ok())\n            .unwrap_or(0);\n\n        annotations.push(RawAnnotation {\n            ref_id: ref_id.clone(),\n            number,\n            role: entry.role.clone(),\n            name: (!entry.name.is_empty()).then_some(entry.name.clone()),\n            rect,\n        });\n    }\n\n    Ok(annotations)\n}\n\nasync fn get_rect_for_selector(\n    client: &CdpClient,\n    session_id: &str,\n    ref_map: &RefMap,\n    selector: &str,\n) -> Result<Option<Rect>, String> {\n    let object_id =\n        super::element::resolve_element_object_id(client, session_id, ref_map, selector).await?;\n    get_rect_for_object(client, session_id, &object_id).await\n}\n\nasync fn get_rect_for_object(\n    client: &CdpClient,\n    session_id: &str,\n    object_id: &str,\n) -> Result<Option<Rect>, String> {\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.callFunctionOn\",\n            &CallFunctionOnParams {\n                function_declaration: r#\"function() {\n                    const rect = this.getBoundingClientRect();\n                    return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };\n                }\"#\n                .to_string(),\n                object_id: Some(object_id.to_string()),\n                arguments: None,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(result.result.value.as_ref().and_then(parse_rect))\n}\n\nfn parse_rect(value: &Value) -> Option<Rect> {\n    Some(Rect {\n        x: value.get(\"x\")?.as_f64()?,\n        y: value.get(\"y\")?.as_f64()?,\n        width: value.get(\"width\")?.as_f64()?,\n        height: value.get(\"height\")?.as_f64()?,\n    })\n}\n\nfn filter_annotations(\n    annotations: Vec<RawAnnotation>,\n    target_rect: Option<&Rect>,\n) -> Vec<RawAnnotation> {\n    let mut items = annotations\n        .into_iter()\n        .filter(|annotation| match target_rect {\n            Some(target) => overlaps(&annotation.rect, target),\n            None => true,\n        })\n        .collect::<Vec<_>>();\n\n    items.sort_by_key(|annotation| annotation.number);\n    items\n}\n\nfn overlaps(left: &Rect, right: &Rect) -> bool {\n    let left_x2 = left.x + left.width;\n    let left_y2 = left.y + left.height;\n    let right_x2 = right.x + right.width;\n    let right_y2 = right.y + right.height;\n\n    left.x < right_x2 && left_x2 > right.x && left.y < right_y2 && left_y2 > right.y\n}\n\nasync fn inject_annotation_overlay(\n    client: &CdpClient,\n    session_id: &str,\n    annotations: &[RawAnnotation],\n) -> Result<(), String> {\n    let overlay_data = annotations\n        .iter()\n        .map(|annotation| {\n            serde_json::json!({\n                \"number\": annotation.number,\n                \"x\": round(annotation.rect.x),\n                \"y\": round(annotation.rect.y),\n                \"width\": round(annotation.rect.width),\n                \"height\": round(annotation.rect.height),\n            })\n        })\n        .collect::<Vec<_>>();\n\n    let expression = format!(\n        r#\"(() => {{\n            var items = {items};\n            var id = {overlay_id};\n            var existing = document.getElementById(id);\n            if (existing) existing.remove();\n            var sx = window.scrollX || 0;\n            var sy = window.scrollY || 0;\n            var c = document.createElement('div');\n            c.id = id;\n            c.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483647;';\n            for (var i = 0; i < items.length; i++) {{\n                var it = items[i];\n                var dx = it.x + sx;\n                var dy = it.y + sy;\n                var b = document.createElement('div');\n                b.style.cssText = 'position:absolute;left:' + dx + 'px;top:' + dy + 'px;width:' + it.width + 'px;height:' + it.height + 'px;border:2px solid rgba(255,0,0,0.8);box-sizing:border-box;pointer-events:none;';\n                var l = document.createElement('div');\n                l.textContent = String(it.number);\n                var labelTop = dy < 14 ? '2px' : '-14px';\n                l.style.cssText = 'position:absolute;top:' + labelTop + ';left:-2px;background:rgba(255,0,0,0.9);color:#fff;font:bold 11px/14px monospace;padding:0 4px;border-radius:2px;white-space:nowrap;';\n                b.appendChild(l);\n                c.appendChild(b);\n            }}\n            document.documentElement.appendChild(c);\n            return true;\n        }})()\"#,\n        items = serde_json::to_string(&overlay_data).unwrap_or_else(|_| \"[]\".to_string()),\n        overlay_id =\n            serde_json::to_string(ANNOTATION_OVERLAY_ID).unwrap_or_else(|_| \"\\\"\\\"\".to_string()),\n    );\n\n    let _: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\nasync fn remove_annotation_overlay(client: &CdpClient, session_id: &str) -> Result<(), String> {\n    let expression = format!(\n        r#\"(() => {{\n            var el = document.getElementById({overlay_id});\n            if (el) el.remove();\n            return true;\n        }})()\"#,\n        overlay_id =\n            serde_json::to_string(ANNOTATION_OVERLAY_ID).unwrap_or_else(|_| \"\\\"\\\"\".to_string()),\n    );\n\n    let _: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    Ok(())\n}\n\nasync fn get_scroll_offsets(client: &CdpClient, session_id: &str) -> Result<(f64, f64), String> {\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: \"({x: window.scrollX || 0, y: window.scrollY || 0})\".to_string(),\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    let value = result.result.value.unwrap_or(Value::Null);\n    let x = value.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    let y = value.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(0.0);\n    Ok((x, y))\n}\n\nfn project_annotations(\n    annotations: &[RawAnnotation],\n    target_rect: Option<&Rect>,\n    scroll: Option<(f64, f64)>,\n) -> Vec<ScreenshotAnnotation> {\n    annotations\n        .iter()\n        .map(|annotation| {\n            let rect = if let Some(target) = target_rect {\n                Rect {\n                    x: annotation.rect.x - target.x,\n                    y: annotation.rect.y - target.y,\n                    width: annotation.rect.width,\n                    height: annotation.rect.height,\n                }\n            } else if let Some((scroll_x, scroll_y)) = scroll {\n                Rect {\n                    x: annotation.rect.x + scroll_x,\n                    y: annotation.rect.y + scroll_y,\n                    width: annotation.rect.width,\n                    height: annotation.rect.height,\n                }\n            } else {\n                annotation.rect.clone()\n            };\n\n            ScreenshotAnnotation {\n                ref_id: annotation.ref_id.clone(),\n                number: annotation.number,\n                role: annotation.role.clone(),\n                name: annotation.name.clone(),\n                box_: AnnotationBox {\n                    x: round(rect.x),\n                    y: round(rect.y),\n                    width: round(rect.width),\n                    height: round(rect.height),\n                },\n            }\n        })\n        .collect()\n}\n\nfn save_screenshot(\n    base64_data: &str,\n    explicit_path: Option<&str>,\n    ext: &str,\n    output_dir: Option<&str>,\n) -> Result<String, String> {\n    let save_path = match explicit_path {\n        Some(path) => path.to_string(),\n        None => {\n            let dir = match output_dir {\n                Some(d) => PathBuf::from(d),\n                None => get_screenshot_dir(),\n            };\n            let _ = std::fs::create_dir_all(&dir);\n            let timestamp = std::time::SystemTime::now()\n                .duration_since(std::time::UNIX_EPOCH)\n                .unwrap_or_default()\n                .as_millis();\n            let name = format!(\"screenshot-{}.{}\", timestamp, ext);\n            dir.join(name).to_string_lossy().to_string()\n        }\n    };\n\n    let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, base64_data)\n        .map_err(|e| format!(\"Failed to decode screenshot: {}\", e))?;\n\n    std::fs::write(&save_path, &bytes)\n        .map_err(|e| format!(\"Failed to save screenshot to {}: {}\", save_path, e))?;\n\n    Ok(save_path)\n}\n\nfn round(value: f64) -> i64 {\n    value.round() as i64\n}\n\nfn get_screenshot_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\").join(\"tmp\").join(\"screenshots\")\n    } else {\n        std::env::temp_dir()\n            .join(\"agent-browser\")\n            .join(\"screenshots\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn filters_annotations_to_target_overlap() {\n        let annotations = vec![\n            RawAnnotation {\n                ref_id: \"e1\".to_string(),\n                number: 1,\n                role: \"button\".to_string(),\n                name: Some(\"Inside\".to_string()),\n                rect: Rect {\n                    x: 10.0,\n                    y: 10.0,\n                    width: 50.0,\n                    height: 20.0,\n                },\n            },\n            RawAnnotation {\n                ref_id: \"e2\".to_string(),\n                number: 2,\n                role: \"button\".to_string(),\n                name: Some(\"Outside\".to_string()),\n                rect: Rect {\n                    x: 200.0,\n                    y: 200.0,\n                    width: 40.0,\n                    height: 20.0,\n                },\n            },\n        ];\n\n        let target = Rect {\n            x: 0.0,\n            y: 0.0,\n            width: 100.0,\n            height: 100.0,\n        };\n\n        let filtered = filter_annotations(annotations, Some(&target));\n        assert_eq!(filtered.len(), 1);\n        assert_eq!(filtered[0].ref_id, \"e1\");\n    }\n\n    #[test]\n    fn projects_selector_annotations_relative_to_target() {\n        let annotations = vec![RawAnnotation {\n            ref_id: \"e1\".to_string(),\n            number: 1,\n            role: \"button\".to_string(),\n            name: Some(\"Inside\".to_string()),\n            rect: Rect {\n                x: 25.0,\n                y: 35.0,\n                width: 40.0,\n                height: 20.0,\n            },\n        }];\n\n        let target = Rect {\n            x: 10.0,\n            y: 15.0,\n            width: 100.0,\n            height: 100.0,\n        };\n\n        let projected = project_annotations(&annotations, Some(&target), None);\n        assert_eq!(projected[0].box_.x, 15);\n        assert_eq!(projected[0].box_.y, 20);\n    }\n\n    #[test]\n    fn projects_full_page_annotations_to_document_space() {\n        let annotations = vec![RawAnnotation {\n            ref_id: \"e1\".to_string(),\n            number: 1,\n            role: \"button\".to_string(),\n            name: Some(\"Bottom\".to_string()),\n            rect: Rect {\n                x: 5.0,\n                y: 12.0,\n                width: 40.0,\n                height: 20.0,\n            },\n        }];\n\n        let projected = project_annotations(&annotations, None, Some((10.0, 1000.0)));\n        assert_eq!(projected[0].box_.x, 15);\n        assert_eq!(projected[0].box_.y, 1012);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/snapshot.rs",
    "content": "use std::collections::HashMap;\n\nuse serde_json::Value;\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::{\n    AXNode, AXProperty, AXValue, EvaluateParams, EvaluateResult, GetFullAXTreeResult,\n};\nuse super::element::RefMap;\n\nconst INTERACTIVE_ROLES: &[&str] = &[\n    \"button\",\n    \"link\",\n    \"textbox\",\n    \"checkbox\",\n    \"radio\",\n    \"combobox\",\n    \"listbox\",\n    \"menuitem\",\n    \"menuitemcheckbox\",\n    \"menuitemradio\",\n    \"option\",\n    \"searchbox\",\n    \"slider\",\n    \"spinbutton\",\n    \"switch\",\n    \"tab\",\n    \"treeitem\",\n    \"Iframe\",\n];\n\nconst CONTENT_ROLES: &[&str] = &[\n    \"heading\",\n    \"cell\",\n    \"gridcell\",\n    \"columnheader\",\n    \"rowheader\",\n    \"listitem\",\n    \"article\",\n    \"region\",\n    \"main\",\n    \"navigation\",\n];\n\nconst STRUCTURAL_ROLES: &[&str] = &[\n    \"generic\",\n    \"group\",\n    \"list\",\n    \"table\",\n    \"row\",\n    \"rowgroup\",\n    \"grid\",\n    \"treegrid\",\n    \"menu\",\n    \"menubar\",\n    \"toolbar\",\n    \"tablist\",\n    \"tree\",\n    \"directory\",\n    \"document\",\n    \"application\",\n    \"presentation\",\n    \"none\",\n    \"WebArea\",\n    \"RootWebArea\",\n];\n\n#[derive(Default)]\npub struct SnapshotOptions {\n    pub selector: Option<String>,\n    pub interactive: bool,\n    pub compact: bool,\n    pub depth: Option<usize>,\n    pub cursor: bool,\n}\n\nstruct TreeNode {\n    role: String,\n    name: String,\n    level: Option<i64>,\n    checked: Option<String>,\n    expanded: Option<bool>,\n    selected: Option<bool>,\n    disabled: Option<bool>,\n    required: Option<bool>,\n    value_text: Option<String>,\n    backend_node_id: Option<i64>,\n    children: Vec<usize>,\n    parent_idx: Option<usize>,\n    has_ref: bool,\n    ref_id: Option<String>,\n    depth: usize,\n    /// Cursor-interactive information (only set when options.cursor is true)\n    cursor_info: Option<CursorElementInfo>,\n}\n\n/// Information about a cursor-interactive element (elements with cursor:pointer, onclick, tabindex, etc.)\n#[derive(Clone)]\nstruct CursorElementInfo {\n    kind: String, // \"clickable\", \"focusable\", \"editable\"\n    hints: Vec<String>,\n    text: String, // textContent from the DOM element (fallback when ARIA name is empty)\n}\n\nstruct RoleNameTracker {\n    counts: HashMap<String, usize>,\n    entries: Vec<(usize, String)>,\n}\n\nimpl RoleNameTracker {\n    fn new() -> Self {\n        Self {\n            counts: HashMap::new(),\n            entries: Vec::new(),\n        }\n    }\n\n    fn track(&mut self, role: &str, name: &str, node_idx: usize) -> usize {\n        let key = format!(\"{}:{}\", role, name);\n        let count = self.counts.entry(key.clone()).or_insert(0);\n        let nth = *count;\n        *count += 1;\n        self.entries.push((node_idx, key));\n        nth\n    }\n\n    fn get_duplicates(&self) -> HashMap<String, usize> {\n        self.counts\n            .iter()\n            .filter(|(_, &count)| count > 1)\n            .map(|(key, &count)| (key.clone(), count))\n            .collect()\n    }\n}\n\npub async fn take_snapshot(\n    client: &CdpClient,\n    session_id: &str,\n    options: &SnapshotOptions,\n    ref_map: &mut RefMap,\n    frame_id: Option<&str>,\n) -> Result<String, String> {\n    client\n        .send_command_no_params(\"DOM.enable\", Some(session_id))\n        .await?;\n    client\n        .send_command_no_params(\"Accessibility.enable\", Some(session_id))\n        .await?;\n\n    // If a CSS selector is provided, resolve the set of backendNodeIds that\n    // belong to the DOM subtree rooted at the matched element.  We use this\n    // set to pick the right AX subtree root(s) later.\n    let selector_backend_ids: Option<std::collections::HashSet<i64>> =\n        if let Some(ref selector) = options.selector {\n            let js = format!(\n                \"document.querySelector({})\",\n                serde_json::to_string(selector).unwrap_or_default()\n            );\n            let result: EvaluateResult = client\n                .send_command_typed(\n                    \"Runtime.evaluate\",\n                    &EvaluateParams {\n                        expression: js,\n                        return_by_value: Some(false),\n                        await_promise: Some(false),\n                    },\n                    Some(session_id),\n                )\n                .await?;\n\n            let object_id = result\n                .result\n                .object_id\n                .ok_or_else(|| format!(\"Selector '{}' did not match any element\", selector))?;\n\n            // Request the full DOM subtree (depth: -1) so we can collect all\n            // backendNodeIds that live under the matched element.\n            let describe: Value = client\n                .send_command(\n                    \"DOM.describeNode\",\n                    Some(serde_json::json!({ \"objectId\": object_id, \"depth\": -1 })),\n                    Some(session_id),\n                )\n                .await?;\n\n            let root_node = describe\n                .get(\"node\")\n                .ok_or_else(|| format!(\"Could not resolve DOM node for selector '{}'\", selector))?;\n\n            let mut ids = std::collections::HashSet::new();\n            collect_backend_node_ids(root_node, &mut ids);\n\n            if ids.is_empty() {\n                return Err(format!(\n                    \"Could not resolve backendNodeId for selector '{}'\",\n                    selector\n                ));\n            }\n\n            Some(ids)\n        } else {\n            None\n        };\n\n    let ax_params = if let Some(fid) = frame_id {\n        serde_json::json!({ \"frameId\": fid })\n    } else {\n        serde_json::json!({})\n    };\n    let ax_tree: GetFullAXTreeResult = client\n        .send_command_typed(\"Accessibility.getFullAXTree\", &ax_params, Some(session_id))\n        .await?;\n\n    let (tree_nodes, root_indices) = build_tree(&ax_tree.nodes);\n\n    // When a selector is given, find AX nodes whose backendDOMNodeId falls\n    // within the target DOM subtree and pick the top-level ones as roots.\n    let effective_roots = if let Some(ref id_set) = selector_backend_ids {\n        // Mark which tree_nodes belong to the target DOM subtree.\n        let in_subtree: Vec<bool> = tree_nodes\n            .iter()\n            .map(|n| n.backend_node_id.is_some_and(|bid| id_set.contains(&bid)))\n            .collect();\n\n        // An AX node is a \"top-level\" match if it is in the subtree but its\n        // parent (in the AX tree) is not.\n        let mut roots = Vec::new();\n        for (idx, node) in tree_nodes.iter().enumerate() {\n            if !in_subtree[idx] {\n                continue;\n            }\n            let parent_in_subtree = node.parent_idx.is_some_and(|pidx| in_subtree[pidx]);\n            if !parent_in_subtree {\n                roots.push(idx);\n            }\n        }\n\n        if roots.is_empty() {\n            return Err(format!(\n                \"No accessibility node found for selector '{}'\",\n                options.selector.as_deref().unwrap_or(\"\")\n            ));\n        }\n        roots\n    } else {\n        root_indices\n    };\n\n    let mut tracker = RoleNameTracker::new();\n    let mut next_ref: usize = ref_map.next_ref_num();\n\n    let mut nodes_with_refs: Vec<(usize, usize)> = Vec::new();\n\n    // When cursor mode is enabled, pre-collect cursor-interactive elements\n    // so we can mark them with refs during tree building\n    let cursor_elements: HashMap<i64, CursorElementInfo> = if options.cursor {\n        find_cursor_interactive_elements(client, session_id)\n            .await\n            .unwrap_or_default()\n    } else {\n        HashMap::new()\n    };\n\n    for (idx, node) in tree_nodes.iter().enumerate() {\n        let role = node.role.as_str();\n        let should_ref = if INTERACTIVE_ROLES.contains(&role) {\n            true\n        } else if CONTENT_ROLES.contains(&role) {\n            !node.name.is_empty()\n        } else if options.cursor {\n            // In cursor mode, also ref elements that are cursor-interactive\n            node.backend_node_id\n                .is_some_and(|bid| cursor_elements.contains_key(&bid))\n        } else {\n            false\n        };\n\n        if should_ref {\n            let nth = tracker.track(role, &node.name, idx);\n            nodes_with_refs.push((idx, nth));\n        }\n    }\n\n    let duplicates = tracker.get_duplicates();\n\n    let mut tree_nodes = tree_nodes;\n    for (idx, nth) in &nodes_with_refs {\n        let node = &tree_nodes[*idx];\n        let key = format!(\"{}:{}\", node.role, node.name);\n        let actual_nth = if duplicates.contains_key(&key) {\n            Some(*nth)\n        } else {\n            None\n        };\n\n        let ref_id = format!(\"e{}\", next_ref);\n        next_ref += 1;\n\n        ref_map.add_with_frame(\n            ref_id.clone(),\n            tree_nodes[*idx].backend_node_id,\n            &tree_nodes[*idx].role,\n            &tree_nodes[*idx].name,\n            actual_nth,\n            frame_id,\n        );\n\n        tree_nodes[*idx].has_ref = true;\n        tree_nodes[*idx].ref_id = Some(ref_id);\n    }\n\n    // Populate cursor_info for ref-bearing nodes when cursor mode is enabled\n    if options.cursor {\n        for (idx, _) in &nodes_with_refs {\n            if let Some(bid) = tree_nodes[*idx].backend_node_id {\n                if let Some(cursor_info) = cursor_elements.get(&bid) {\n                    tree_nodes[*idx].cursor_info = Some((*cursor_info).clone());\n                }\n            }\n        }\n    }\n\n    ref_map.set_next_ref_num(next_ref);\n\n    let mut output = String::new();\n    for &root_idx in &effective_roots {\n        render_tree(&tree_nodes, root_idx, 0, &mut output, options);\n    }\n\n    // Recurse into child iframes: for each Iframe node with a backend_node_id,\n    // resolve the child frame ID and take a snapshot of its content.\n    // We only recurse from the main frame (frame_id == None) to avoid\n    // unbounded depth; nested iframes within iframes are not expanded.\n    if frame_id.is_none() {\n        let mut iframe_snapshots: Vec<(String, String)> = Vec::new(); // (ref_id, child_snapshot)\n        for node in tree_nodes.iter() {\n            if node.role != \"Iframe\" || !node.has_ref {\n                continue;\n            }\n            let Some(bid) = node.backend_node_id else {\n                continue;\n            };\n            let ref_id = node.ref_id.as_deref().unwrap_or(\"\");\n            if let Ok(child_fid) = resolve_iframe_frame_id(client, session_id, bid).await {\n                // Snapshot the child frame; errors are silently ignored\n                // (e.g. cross-origin iframes)\n                if let Ok(child_text) = Box::pin(take_snapshot(\n                    client,\n                    session_id,\n                    options,\n                    ref_map,\n                    Some(&child_fid),\n                ))\n                .await\n                {\n                    if !child_text.is_empty()\n                        && child_text != \"(empty page)\"\n                        && child_text != \"(no interactive elements)\"\n                    {\n                        iframe_snapshots.push((ref_id.to_string(), child_text));\n                    }\n                }\n            }\n        }\n\n        // Insert each child snapshot after its Iframe line in the output\n        for (ref_id, child_text) in iframe_snapshots {\n            let marker = format!(\"[ref={}]\", ref_id);\n            if let Some(pos) = output.find(&marker) {\n                // Find the end of the Iframe line\n                let line_end = output[pos..]\n                    .find('\\n')\n                    .map(|i| pos + i)\n                    .unwrap_or(output.len());\n                // Determine the indent of the Iframe line\n                let line_start = output[..pos].rfind('\\n').map(|i| i + 1).unwrap_or(0);\n                let iframe_line = &output[line_start..line_end];\n                let iframe_indent = iframe_line.len() - iframe_line.trim_start().len();\n                let child_indent = iframe_indent + 2; // one level deeper\n                let prefix = \" \".repeat(child_indent);\n\n                let indented_child: String = child_text\n                    .lines()\n                    .map(|line| format!(\"{}{}\\n\", prefix, line))\n                    .collect();\n\n                // Ensure there's a newline to insert after\n                if line_end == output.len() {\n                    output.push('\\n');\n                    output.push_str(&indented_child);\n                } else {\n                    output.insert_str(line_end + 1, &indented_child);\n                }\n            }\n        }\n    }\n\n    if options.compact {\n        output = compact_tree(&output, options.interactive);\n    }\n\n    let trimmed = output.trim().to_string();\n\n    if trimmed.is_empty() {\n        if options.interactive {\n            return Ok(\"(no interactive elements)\".to_string());\n        }\n        return Ok(\"(empty page)\".to_string());\n    }\n\n    Ok(trimmed)\n}\n\n/// Resolve the child frame ID for an iframe element given its backendNodeId.\nasync fn resolve_iframe_frame_id(\n    client: &CdpClient,\n    session_id: &str,\n    backend_node_id: i64,\n) -> Result<String, String> {\n    // depth: 1 ensures contentDocument is included in the response\n    let describe: Value = client\n        .send_command(\n            \"DOM.describeNode\",\n            Some(serde_json::json!({ \"backendNodeId\": backend_node_id, \"depth\": 1 })),\n            Some(session_id),\n        )\n        .await?;\n\n    // Try contentDocument.frameId first (standard for iframes)\n    if let Some(frame_id) = describe\n        .get(\"node\")\n        .and_then(|n| n.get(\"contentDocument\"))\n        .and_then(|cd| cd.get(\"frameId\"))\n        .and_then(|v| v.as_str())\n    {\n        return Ok(frame_id.to_string());\n    }\n\n    // Fallback: the node itself may have a frameId\n    describe\n        .get(\"node\")\n        .and_then(|n| n.get(\"frameId\"))\n        .and_then(|v| v.as_str())\n        .map(|s| s.to_string())\n        .ok_or_else(|| \"Could not resolve iframe frame ID\".to_string())\n}\n\nasync fn find_cursor_interactive_elements(\n    client: &CdpClient,\n    session_id: &str,\n) -> Result<HashMap<i64, CursorElementInfo>, String> {\n    // Single JS evaluation that matches the v0.19.0 Node.js findCursorInteractiveElements():\n    // - Uses querySelectorAll('*') to walk all elements\n    // - Checks getComputedStyle(el).cursor === 'pointer'\n    // - Checks onclick attribute/handler and tabindex\n    // - Skips interactiveTags (a, button, input, select, textarea, details, summary)\n    // - Skips elements with interactive ARIA roles\n    // - Deduplicates inherited cursor:pointer from parent\n    // - Skips empty text and zero-size elements\n    // - Tags each matched element with data-__ab-ci for batch backendNodeId resolution\n    let js = r#\"\n(function() {\n    var results = [];\n    if (!document.body) return results;\n\n    var interactiveRoles = {\n        'button':1, 'link':1, 'textbox':1, 'checkbox':1, 'radio':1, 'combobox':1, 'listbox':1,\n        'menuitem':1, 'menuitemcheckbox':1, 'menuitemradio':1, 'option':1, 'searchbox':1,\n        'slider':1, 'spinbutton':1, 'switch':1, 'tab':1, 'treeitem':1\n    };\n    var interactiveTags = {\n        'a':1, 'button':1, 'input':1, 'select':1, 'textarea':1, 'details':1, 'summary':1\n    };\n\n    var allElements = document.body.querySelectorAll('*');\n    for (var i = 0; i < allElements.length; i++) {\n        var el = allElements[i];\n\n        if (el.closest && el.closest('[hidden], [aria-hidden=\"true\"]')) continue;\n\n        var tagName = el.tagName.toLowerCase();\n        if (interactiveTags[tagName]) continue;\n\n        var role = el.getAttribute('role');\n        if (role && interactiveRoles[role.toLowerCase()]) continue;\n\n        var computedStyle = getComputedStyle(el);\n        var hasCursorPointer = computedStyle.cursor === 'pointer';\n        var hasOnClick = el.hasAttribute('onclick') || el.onclick !== null;\n        var tabIndex = el.getAttribute('tabindex');\n        var hasTabIndex = tabIndex !== null && tabIndex !== '-1';\n        var ce = el.getAttribute('contenteditable');\n        var isEditable = ce === '' || ce === 'true';\n\n        if (!hasCursorPointer && !hasOnClick && !hasTabIndex && !isEditable) continue;\n\n        // Skip elements that only inherit cursor:pointer from an ancestor\n        if (hasCursorPointer && !hasOnClick && !hasTabIndex && !isEditable) {\n            var parent = el.parentElement;\n            if (parent && getComputedStyle(parent).cursor === 'pointer') continue;\n        }\n\n        var text = (el.textContent || '').trim().slice(0, 100);\n\n        var rect = el.getBoundingClientRect();\n        if (rect.width === 0 || rect.height === 0) continue;\n\n        el.setAttribute('data-__ab-ci', String(results.length));\n        results.push({\n            text: text,\n            tagName: tagName,\n            hasOnClick: hasOnClick,\n            hasCursorPointer: hasCursorPointer,\n            hasTabIndex: hasTabIndex,\n            isEditable: isEditable\n        });\n    }\n    return results;\n})()\n\"#;\n\n    let result: EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: js.to_string(),\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    let elements: Vec<Value> = result\n        .result\n        .value\n        .and_then(|v| serde_json::from_value::<Vec<Value>>(v).ok())\n        .unwrap_or_default();\n\n    if elements.is_empty() {\n        return Ok(HashMap::new());\n    }\n\n    // Batch-resolve backendNodeIds: use DOM.getDocument to get the root nodeId,\n    // then DOM.querySelectorAll to get all tagged elements in a single call.\n    let doc: Value = client\n        .send_command(\n            \"DOM.getDocument\",\n            Some(serde_json::json!({ \"depth\": 0 })),\n            Some(session_id),\n        )\n        .await?;\n\n    let root_node_id = doc\n        .get(\"root\")\n        .and_then(|r| r.get(\"nodeId\"))\n        .and_then(|v| v.as_i64())\n        .ok_or(\"DOM.getDocument did not return root nodeId\")?;\n\n    let query_result: Value = client\n        .send_command(\n            \"DOM.querySelectorAll\",\n            Some(serde_json::json!({\n                \"nodeId\": root_node_id,\n                \"selector\": \"[data-__ab-ci]\"\n            })),\n            Some(session_id),\n        )\n        .await?;\n\n    let node_ids: Vec<i64> = query_result\n        .get(\"nodeIds\")\n        .and_then(|v| v.as_array())\n        .map(|arr| arr.iter().filter_map(|v| v.as_i64()).collect())\n        .unwrap_or_default();\n\n    // Resolve backendNodeIds for each DOM node using concurrent CDP calls.\n    let describe_futures: Vec<_> = node_ids\n        .iter()\n        .map(|&node_id| {\n            client.send_command(\n                \"DOM.describeNode\",\n                Some(serde_json::json!({ \"nodeId\": node_id })),\n                Some(session_id),\n            )\n        })\n        .collect();\n\n    let describe_results = futures_util::future::join_all(describe_futures).await;\n\n    // Build a map from data-__ab-ci index to backendNodeId.\n    let mut idx_to_backend: HashMap<usize, i64> = HashMap::new();\n    for desc in describe_results.into_iter().flatten() {\n        let backend_id = desc\n            .get(\"node\")\n            .and_then(|n| n.get(\"backendNodeId\"))\n            .and_then(|v| v.as_i64());\n        let ci_attr = desc\n            .get(\"node\")\n            .and_then(|n| n.get(\"attributes\"))\n            .and_then(|a| a.as_array())\n            .and_then(|attrs| {\n                // attributes is a flat array: [name, value, name, value, ...]\n                attrs\n                    .iter()\n                    .enumerate()\n                    .find(|(_, v)| v.as_str() == Some(\"data-__ab-ci\"))\n                    .and_then(|(i, _)| attrs.get(i + 1))\n                    .and_then(|v| v.as_str())\n                    .and_then(|s| s.parse::<usize>().ok())\n            });\n        if let (Some(bid), Some(idx)) = (backend_id, ci_attr) {\n            idx_to_backend.insert(idx, bid);\n        }\n    }\n\n    // Clean up the data attributes we injected for backendNodeId resolution.\n    let cleanup_js =\n        r#\"(function(){ var els = document.querySelectorAll('[data-__ab-ci]'); for (var i = 0; i < els.length; i++) els[i].removeAttribute('data-__ab-ci'); return els.length; })()\"#.to_string();\n    if let Err(e) = client\n        .send_command_typed::<EvaluateParams, EvaluateResult>(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: cleanup_js,\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await\n    {\n        eprintln!(\"[agent-browser] Warning: failed to clean up data-__ab-ci attributes: {e}\");\n    }\n\n    // Build the map\n    let mut map: HashMap<i64, CursorElementInfo> = HashMap::new();\n    for (i, elem) in elements.iter().enumerate() {\n        let backend_node_id = idx_to_backend.get(&i).copied();\n\n        // Role differentiation: v0.19.0 uses 'clickable' for cursor:pointer or onclick,\n        // 'focusable' for tabindex-only elements.\n        let has_cursor_pointer = elem\n            .get(\"hasCursorPointer\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false);\n        let has_on_click = elem\n            .get(\"hasOnClick\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false);\n        let has_tab_index = elem\n            .get(\"hasTabIndex\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false);\n        let is_editable = elem\n            .get(\"isEditable\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false);\n\n        let kind = if has_cursor_pointer || has_on_click {\n            \"clickable\"\n        } else if is_editable {\n            \"editable\"\n        } else {\n            \"focusable\"\n        };\n\n        let mut hints: Vec<String> = Vec::new();\n        if has_cursor_pointer {\n            hints.push(\"cursor:pointer\".to_string());\n        }\n        if has_on_click {\n            hints.push(\"onclick\".to_string());\n        }\n        if has_tab_index {\n            hints.push(\"tabindex\".to_string());\n        }\n        if is_editable {\n            hints.push(\"contenteditable\".to_string());\n        }\n\n        let text = elem\n            .get(\"text\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .trim()\n            .to_string();\n\n        if let Some(bid) = backend_node_id {\n            map.insert(\n                bid,\n                CursorElementInfo {\n                    kind: kind.to_string(),\n                    hints,\n                    text,\n                },\n            );\n        }\n    }\n\n    Ok(map)\n}\n\nfn build_tree(nodes: &[AXNode]) -> (Vec<TreeNode>, Vec<usize>) {\n    let mut tree_nodes: Vec<TreeNode> = Vec::with_capacity(nodes.len());\n    let mut id_to_idx: HashMap<String, usize> = HashMap::new();\n\n    for (i, node) in nodes.iter().enumerate() {\n        let role = extract_ax_string(&node.role);\n        let name = extract_ax_string(&node.name);\n        let value_text = extract_ax_string_opt(&node.value);\n\n        let (level, checked, expanded, selected, disabled, required) =\n            extract_properties(&node.properties);\n\n        if (node.ignored.unwrap_or(false) && role != \"RootWebArea\") || role == \"InlineTextBox\" {\n            tree_nodes.push(TreeNode {\n                role: String::new(),\n                name: String::new(),\n                level: None,\n                checked: None,\n                expanded: None,\n                selected: None,\n                disabled: None,\n                required: None,\n                value_text: None,\n                backend_node_id: None,\n                children: Vec::new(),\n                parent_idx: None,\n                has_ref: false,\n                ref_id: None,\n                depth: 0,\n                cursor_info: None,\n            });\n            id_to_idx.insert(node.node_id.clone(), i);\n            continue;\n        }\n\n        tree_nodes.push(TreeNode {\n            role,\n            name,\n            level,\n            checked,\n            expanded,\n            selected,\n            disabled,\n            required,\n            value_text,\n            backend_node_id: node.backend_d_o_m_node_id,\n            children: Vec::new(),\n            parent_idx: None,\n            has_ref: false,\n            ref_id: None,\n            depth: 0,\n            cursor_info: None,\n        });\n        id_to_idx.insert(node.node_id.clone(), i);\n    }\n\n    // Build parent-child relationships\n    for (i, node) in nodes.iter().enumerate() {\n        if let Some(ref child_ids) = node.child_ids {\n            for cid in child_ids {\n                if let Some(&child_idx) = id_to_idx.get(cid) {\n                    tree_nodes[i].children.push(child_idx);\n                    tree_nodes[child_idx].parent_idx = Some(i);\n                }\n            }\n        }\n    }\n\n    // Set depths\n    let mut root_indices = Vec::new();\n    let children_exist: Vec<bool> = nodes.iter().map(|_| false).collect();\n    let mut is_child = children_exist;\n    for node in &tree_nodes {\n        for &child in &node.children {\n            is_child[child] = true;\n        }\n    }\n    for (i, &is_c) in is_child.iter().enumerate() {\n        if !is_c {\n            root_indices.push(i);\n        }\n    }\n\n    fn set_depth(nodes: &mut [TreeNode], idx: usize, depth: usize) {\n        nodes[idx].depth = depth;\n        let children: Vec<usize> = nodes[idx].children.clone();\n        for child_idx in children {\n            set_depth(nodes, child_idx, depth + 1);\n        }\n    }\n\n    for &root in &root_indices {\n        set_depth(&mut tree_nodes, root, 0);\n    }\n\n    (tree_nodes, root_indices)\n}\n\nfn render_tree(\n    nodes: &[TreeNode],\n    idx: usize,\n    indent: usize,\n    output: &mut String,\n    options: &SnapshotOptions,\n) {\n    let node = &nodes[idx];\n\n    if node.role.is_empty() {\n        // Ignored node -- still render children\n        for &child in &node.children {\n            render_tree(nodes, child, indent, output, options);\n        }\n        return;\n    }\n\n    if let Some(max_depth) = options.depth {\n        if indent > max_depth {\n            return;\n        }\n    }\n\n    let role = &node.role;\n\n    // Skip root WebArea wrapper\n    if role == \"RootWebArea\" || role == \"WebArea\" {\n        for &child in &node.children {\n            render_tree(nodes, child, indent, output, options);\n        }\n        return;\n    }\n\n    if options.interactive && !node.has_ref {\n        // In interactive mode, skip non-interactive but render children\n        for &child in &node.children {\n            render_tree(nodes, child, indent, output, options);\n        }\n        return;\n    }\n\n    let prefix = \"  \".repeat(indent);\n    let mut line = format!(\"{}- {}\", prefix, role);\n\n    // Use ARIA name if available, otherwise fall back to cursor-interactive textContent\n    let display_name = if !node.name.is_empty() {\n        &node.name\n    } else if let Some(ref ci) = node.cursor_info {\n        &ci.text\n    } else {\n        &node.name\n    };\n    if !display_name.is_empty() {\n        line.push_str(&format!(\" \\\"{}\\\"\", display_name));\n    }\n\n    // Properties\n    let mut attrs = Vec::new();\n\n    if let Some(level) = node.level {\n        attrs.push(format!(\"level={}\", level));\n    }\n    if let Some(ref checked) = node.checked {\n        attrs.push(format!(\"checked={}\", checked));\n    }\n    if let Some(expanded) = node.expanded {\n        attrs.push(format!(\"expanded={}\", expanded));\n    }\n    if let Some(selected) = node.selected {\n        if selected {\n            attrs.push(\"selected\".to_string());\n        }\n    }\n    if let Some(disabled) = node.disabled {\n        if disabled {\n            attrs.push(\"disabled\".to_string());\n        }\n    }\n    if let Some(required) = node.required {\n        if required {\n            attrs.push(\"required\".to_string());\n        }\n    }\n\n    if let Some(ref ref_id) = node.ref_id {\n        attrs.push(format!(\"ref={}\", ref_id));\n    }\n\n    if !attrs.is_empty() {\n        line.push_str(&format!(\" [{}]\", attrs.join(\", \")));\n    }\n\n    // Add cursor-interactive kind & hints\n    if let Some(ref cursor_info) = node.cursor_info {\n        line.push_str(&format!(\n            \" {} [{}]\",\n            &cursor_info.kind,\n            &cursor_info.hints.join(\", \")\n        ));\n    }\n\n    // Value\n    if let Some(ref val) = node.value_text {\n        if !val.is_empty() && val != &node.name {\n            line.push_str(&format!(\": {}\", val));\n        }\n    }\n\n    output.push_str(&line);\n    output.push('\\n');\n\n    for &child in &node.children {\n        render_tree(nodes, child, indent + 1, output, options);\n    }\n}\n\nfn compact_tree(tree: &str, interactive: bool) -> String {\n    let lines: Vec<&str> = tree.lines().collect();\n    if lines.is_empty() {\n        return String::new();\n    }\n\n    let mut keep = vec![false; lines.len()];\n\n    for (i, line) in lines.iter().enumerate() {\n        if line.contains(\"[ref=\") || line.contains(\": \") {\n            keep[i] = true;\n            // Mark ancestors\n            let my_indent = count_indent(line);\n            for j in (0..i).rev() {\n                let ancestor_indent = count_indent(lines[j]);\n                if ancestor_indent < my_indent {\n                    keep[j] = true;\n                    if ancestor_indent == 0 {\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    let result: Vec<&str> = lines\n        .iter()\n        .enumerate()\n        .filter(|(i, _)| keep[*i])\n        .map(|(_, line)| *line)\n        .collect();\n\n    let output = result.join(\"\\n\");\n    if output.trim().is_empty() && interactive {\n        return \"(no interactive elements)\".to_string();\n    }\n    output\n}\n\nfn count_indent(line: &str) -> usize {\n    let trimmed = line.trim_start();\n    (line.len() - trimmed.len()) / 2\n}\n\nfn extract_ax_string(value: &Option<AXValue>) -> String {\n    match value {\n        Some(v) => match &v.value {\n            Some(Value::String(s)) => s.clone(),\n            Some(Value::Number(n)) => n.to_string(),\n            Some(Value::Bool(b)) => b.to_string(),\n            _ => String::new(),\n        },\n        None => String::new(),\n    }\n}\n\nfn extract_ax_string_opt(value: &Option<AXValue>) -> Option<String> {\n    match value {\n        Some(v) => match &v.value {\n            Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),\n            Some(Value::Number(n)) => Some(n.to_string()),\n            _ => None,\n        },\n        None => None,\n    }\n}\n\ntype NodeProperties = (\n    Option<i64>,    // level\n    Option<String>, // checked\n    Option<bool>,   // expanded\n    Option<bool>,   // selected\n    Option<bool>,   // disabled\n    Option<bool>,   // required\n);\n\nfn extract_properties(props: &Option<Vec<AXProperty>>) -> NodeProperties {\n    let mut level = None;\n    let mut checked = None;\n    let mut expanded = None;\n    let mut selected = None;\n    let mut disabled = None;\n    let mut required = None;\n\n    if let Some(properties) = props {\n        for prop in properties {\n            match prop.name.as_str() {\n                \"level\" => {\n                    level = prop.value.value.as_ref().and_then(|v| v.as_i64());\n                }\n                \"checked\" => {\n                    checked = prop.value.value.as_ref().map(|v| match v {\n                        Value::String(s) => s.clone(),\n                        Value::Bool(b) => b.to_string(),\n                        _ => \"false\".to_string(),\n                    });\n                }\n                \"expanded\" => {\n                    expanded = prop.value.value.as_ref().and_then(|v| v.as_bool());\n                }\n                \"selected\" => {\n                    selected = prop.value.value.as_ref().and_then(|v| v.as_bool());\n                }\n                \"disabled\" => {\n                    disabled = prop.value.value.as_ref().and_then(|v| v.as_bool());\n                }\n                \"required\" => {\n                    required = prop.value.value.as_ref().and_then(|v| v.as_bool());\n                }\n                _ => {}\n            }\n        }\n    }\n\n    (level, checked, expanded, selected, disabled, required)\n}\n\n/// Build the set of texts to de-duplicate cursor-interactive elements against.\n///\n/// All ref-bearing ARIA tree nodes have their names stored in `ref_map` during\n/// tree construction, so the ref-map entries are the single source of truth.\n/// This avoids fragile parsing of the rendered tree text.\nfn build_dedup_set(ref_map: &RefMap) -> std::collections::HashSet<String> {\n    ref_map\n        .entries_sorted()\n        .into_iter()\n        .filter(|(_, entry)| !entry.name.is_empty())\n        .map(|(_, entry)| entry.name.to_lowercase())\n        .collect()\n}\n\n/// Recursively collect all `backendNodeId` values from a CDP DOM node tree\n/// (as returned by `DOM.describeNode` with `depth: -1`).\nfn collect_backend_node_ids(node: &Value, ids: &mut std::collections::HashSet<i64>) {\n    if let Some(id) = node.get(\"backendNodeId\").and_then(|v| v.as_i64()) {\n        ids.insert(id);\n    }\n    if let Some(children) = node.get(\"children\").and_then(|v| v.as_array()) {\n        for child in children {\n            collect_backend_node_ids(child, ids);\n        }\n    }\n    // Shadow DOM and content documents\n    if let Some(shadow) = node.get(\"shadowRoots\").and_then(|v| v.as_array()) {\n        for child in shadow {\n            collect_backend_node_ids(child, ids);\n        }\n    }\n    if let Some(doc) = node.get(\"contentDocument\") {\n        collect_backend_node_ids(doc, ids);\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_interactive_roles() {\n        assert!(INTERACTIVE_ROLES.contains(&\"button\"));\n        assert!(INTERACTIVE_ROLES.contains(&\"textbox\"));\n        assert!(!INTERACTIVE_ROLES.contains(&\"heading\"));\n    }\n\n    #[test]\n    fn test_content_roles() {\n        assert!(CONTENT_ROLES.contains(&\"heading\"));\n        assert!(!CONTENT_ROLES.contains(&\"button\"));\n    }\n\n    #[test]\n    fn test_compact_tree_basic() {\n        let tree = \"- navigation\\n  - link \\\"Home\\\" [ref=e1]\\n  - link \\\"About\\\" [ref=e2]\\n- main\\n  - heading \\\"Title\\\"\\n  - paragraph\\n    - text: Hello\\n\";\n        let result = compact_tree(tree, false);\n        assert!(result.contains(\"[ref=e1]\"));\n        assert!(result.contains(\"[ref=e2]\"));\n        assert!(result.contains(\"Hello\"));\n    }\n\n    #[test]\n    fn test_compact_tree_empty_interactive() {\n        let result = compact_tree(\"- generic\\n\", true);\n        assert_eq!(result, \"(no interactive elements)\");\n    }\n\n    #[test]\n    fn test_count_indent() {\n        assert_eq!(count_indent(\"- heading\"), 0);\n        assert_eq!(count_indent(\"  - link\"), 1);\n        assert_eq!(count_indent(\"    - text\"), 2);\n    }\n\n    #[test]\n    fn test_role_name_tracker() {\n        let mut tracker = RoleNameTracker::new();\n        assert_eq!(tracker.track(\"button\", \"Submit\", 0), 0);\n        assert_eq!(tracker.track(\"button\", \"Submit\", 1), 1);\n        assert_eq!(tracker.track(\"button\", \"Cancel\", 2), 0);\n\n        let dups = tracker.get_duplicates();\n        assert!(dups.contains_key(\"button:Submit\"));\n        assert!(!dups.contains_key(\"button:Cancel\"));\n    }\n\n    // -----------------------------------------------------------------------\n    // Cursor-interactive text dedup (Issue #841 regression guard)\n    // -----------------------------------------------------------------------\n\n    #[test]\n    fn test_dedup_set_from_ref_map_names() {\n        let mut ref_map = RefMap::new();\n        ref_map.add(\"e1\".to_string(), Some(1), \"link\", \"Example Link\", None);\n        ref_map.add(\"e2\".to_string(), Some(2), \"button\", \"Submit\", None);\n\n        let set = build_dedup_set(&ref_map);\n        assert!(set.contains(\"example link\"));\n        assert!(set.contains(\"submit\"));\n        assert!(!set.contains(\"other text\"));\n    }\n\n    #[test]\n    fn test_dedup_set_case_insensitive() {\n        let mut ref_map = RefMap::new();\n        ref_map.add(\"e1\".to_string(), Some(1), \"button\", \"Submit Form\", None);\n\n        let set = build_dedup_set(&ref_map);\n        assert!(set.contains(\"submit form\"));\n        assert!(!set.contains(\"Submit Form\"));\n    }\n\n    #[test]\n    fn test_dedup_set_empty_inputs() {\n        let ref_map = RefMap::new();\n        let set = build_dedup_set(&ref_map);\n        assert!(set.is_empty());\n    }\n\n    #[test]\n    fn test_dedup_set_skips_empty_names() {\n        let mut ref_map = RefMap::new();\n        ref_map.add(\"e1\".to_string(), Some(1), \"generic\", \"\", None);\n        ref_map.add(\"e2\".to_string(), Some(2), \"button\", \"OK\", None);\n\n        let set = build_dedup_set(&ref_map);\n        assert_eq!(set.len(), 1);\n        assert!(set.contains(\"ok\"));\n    }\n}\n"
  },
  {
    "path": "cli/src/native/state.rs",
    "content": "use aes_gcm::{aead::Aead, aead::KeyInit, Aes256Gcm};\nuse serde::{Deserialize, Serialize};\nuse serde_json::{json, Value};\nuse sha2::{Digest, Sha256};\nuse std::fs;\nuse std::path::PathBuf;\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::EvaluateParams;\nuse super::cookies::{self, Cookie};\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct StorageState {\n    pub cookies: Vec<Cookie>,\n    pub origins: Vec<OriginStorage>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct OriginStorage {\n    pub origin: String,\n    pub local_storage: Vec<StorageEntry>,\n    #[serde(default)]\n    pub session_storage: Vec<StorageEntry>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct StorageEntry {\n    pub name: String,\n    pub value: String,\n}\n\npub async fn save_state(\n    client: &CdpClient,\n    session_id: &str,\n    path: Option<&str>,\n    session_name: Option<&str>,\n    session_id_str: &str,\n) -> Result<String, String> {\n    let cookies = cookies::get_cookies(client, session_id, None).await?;\n\n    // Get current origin's storage\n    let origin_js = r#\"(() => {\n        const result = { origin: location.origin, localStorage: [], sessionStorage: [] };\n        try {\n            for (let i = 0; i < localStorage.length; i++) {\n                const key = localStorage.key(i);\n                result.localStorage.push({ name: key, value: localStorage.getItem(key) });\n            }\n        } catch(e) {}\n        try {\n            for (let i = 0; i < sessionStorage.length; i++) {\n                const key = sessionStorage.key(i);\n                result.sessionStorage.push({ name: key, value: sessionStorage.getItem(key) });\n            }\n        } catch(e) {}\n        return result;\n    })()\"#;\n\n    let origin_result: super::cdp::types::EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: origin_js.to_string(),\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    let origin_data = origin_result.result.value.unwrap_or(Value::Null);\n    let origins = if origin_data.is_object() {\n        let origin = origin_data\n            .get(\"origin\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .to_string();\n        let local_storage: Vec<StorageEntry> = origin_data\n            .get(\"localStorage\")\n            .and_then(|v| serde_json::from_value(v.clone()).ok())\n            .unwrap_or_default();\n        let session_storage: Vec<StorageEntry> = origin_data\n            .get(\"sessionStorage\")\n            .and_then(|v| serde_json::from_value(v.clone()).ok())\n            .unwrap_or_default();\n\n        if !origin.is_empty() && origin != \"null\" {\n            vec![OriginStorage {\n                origin,\n                local_storage,\n                session_storage,\n            }]\n        } else {\n            vec![]\n        }\n    } else {\n        vec![]\n    };\n\n    let state = StorageState { cookies, origins };\n    let json_str = serde_json::to_string_pretty(&state)\n        .map_err(|e| format!(\"Failed to serialize state: {}\", e))?;\n\n    let mut save_path = match path {\n        Some(p) => p.to_string(),\n        None => {\n            let dir = get_sessions_dir();\n            let _ = fs::create_dir_all(&dir);\n            let name = session_name.unwrap_or(\"default\");\n            dir.join(format!(\"{}-{}.json\", name, session_id_str))\n                .to_string_lossy()\n                .to_string()\n        }\n    };\n\n    if let Ok(key) = std::env::var(\"AGENT_BROWSER_ENCRYPTION_KEY\") {\n        let encrypted = encrypt_data(json_str.as_bytes(), &key)?;\n        save_path.push_str(\".enc\");\n        fs::write(&save_path, &encrypted)\n            .map_err(|e| format!(\"Failed to write state to {}: {}\", save_path, e))?;\n    } else {\n        fs::write(&save_path, &json_str)\n            .map_err(|e| format!(\"Failed to write state to {}: {}\", save_path, e))?;\n    }\n\n    Ok(save_path)\n}\n\npub async fn load_state(client: &CdpClient, session_id: &str, path: &str) -> Result<(), String> {\n    let json_str = if path.ends_with(\".enc\") {\n        let key = std::env::var(\"AGENT_BROWSER_ENCRYPTION_KEY\").map_err(|_| {\n            \"Encrypted state file requires AGENT_BROWSER_ENCRYPTION_KEY\".to_string()\n        })?;\n        let data =\n            fs::read(path).map_err(|e| format!(\"Failed to read state from {}: {}\", path, e))?;\n        let decrypted = decrypt_data(&data, &key)?;\n        String::from_utf8(decrypted)\n            .map_err(|e| format!(\"Decrypted state is not valid UTF-8: {}\", e))?\n    } else {\n        match fs::read_to_string(path) {\n            Ok(s) => s,\n            Err(e) => {\n                if let Ok(key) = std::env::var(\"AGENT_BROWSER_ENCRYPTION_KEY\") {\n                    let enc_path = format!(\"{}.enc\", path);\n                    if let Ok(data) = fs::read(&enc_path) {\n                        let decrypted = decrypt_data(&data, &key)?;\n                        String::from_utf8(decrypted)\n                            .map_err(|de| format!(\"Decrypted state is not valid UTF-8: {}\", de))?\n                    } else {\n                        return Err(format!(\"Failed to read state from {}: {}\", path, e));\n                    }\n                } else {\n                    return Err(format!(\"Failed to read state from {}: {}\", path, e));\n                }\n            }\n        }\n    };\n\n    let state: StorageState =\n        serde_json::from_str(&json_str).map_err(|e| format!(\"Invalid state file: {}\", e))?;\n\n    // Load cookies\n    if !state.cookies.is_empty() {\n        let cookie_values: Vec<Value> = state\n            .cookies\n            .iter()\n            .map(|c| serde_json::to_value(c).unwrap_or(Value::Null))\n            .collect();\n        cookies::set_cookies(client, session_id, cookie_values, None).await?;\n    }\n\n    // Load storage per origin\n    for origin in &state.origins {\n        if origin.local_storage.is_empty() && origin.session_storage.is_empty() {\n            continue;\n        }\n\n        // Navigate to origin to set storage\n        let navigate_url = format!(\"{}/\", origin.origin.trim_end_matches('/'));\n        client\n            .send_command(\n                \"Page.navigate\",\n                Some(json!({ \"url\": navigate_url })),\n                Some(session_id),\n            )\n            .await?;\n\n        // Brief wait for navigation\n        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n\n        for entry in &origin.local_storage {\n            let js = format!(\n                \"localStorage.setItem({}, {})\",\n                serde_json::to_string(&entry.name).unwrap_or_default(),\n                serde_json::to_string(&entry.value).unwrap_or_default(),\n            );\n            let _ = client\n                .send_command_typed::<_, super::cdp::types::EvaluateResult>(\n                    \"Runtime.evaluate\",\n                    &EvaluateParams {\n                        expression: js,\n                        return_by_value: Some(true),\n                        await_promise: Some(false),\n                    },\n                    Some(session_id),\n                )\n                .await;\n        }\n\n        for entry in &origin.session_storage {\n            let js = format!(\n                \"sessionStorage.setItem({}, {})\",\n                serde_json::to_string(&entry.name).unwrap_or_default(),\n                serde_json::to_string(&entry.value).unwrap_or_default(),\n            );\n            let _ = client\n                .send_command_typed::<_, super::cdp::types::EvaluateResult>(\n                    \"Runtime.evaluate\",\n                    &EvaluateParams {\n                        expression: js,\n                        return_by_value: Some(true),\n                        await_promise: Some(false),\n                    },\n                    Some(session_id),\n                )\n                .await;\n        }\n    }\n\n    Ok(())\n}\n\nfn is_state_file(path: &std::path::Path) -> bool {\n    let fname = path\n        .file_name()\n        .unwrap_or_default()\n        .to_string_lossy()\n        .to_string();\n    fname.ends_with(\".json\") || fname.ends_with(\".json.enc\")\n}\n\nfn is_encrypted_state(path: &std::path::Path) -> bool {\n    path.to_string_lossy().ends_with(\".json.enc\")\n}\n\npub fn state_list() -> Result<Value, String> {\n    let dir = get_sessions_dir();\n    if !dir.exists() {\n        return Ok(json!({ \"files\": [], \"directory\": dir.to_string_lossy() }));\n    }\n\n    let mut files = Vec::new();\n\n    let entries = fs::read_dir(&dir).map_err(|e| format!(\"Failed to read sessions dir: {}\", e))?;\n\n    for entry in entries.flatten() {\n        let path = entry.path();\n        if is_state_file(&path) {\n            let metadata = fs::metadata(&path).ok();\n            let filename = path\n                .file_name()\n                .unwrap_or_default()\n                .to_string_lossy()\n                .to_string();\n            let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0);\n            let modified = metadata\n                .as_ref()\n                .and_then(|m| m.modified().ok())\n                .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())\n                .map(|d| d.as_secs())\n                .unwrap_or(0);\n            let encrypted = is_encrypted_state(&path);\n\n            files.push(json!({\n                \"filename\": filename,\n                \"path\": path.to_string_lossy(),\n                \"size\": size,\n                \"modified\": modified,\n                \"encrypted\": encrypted,\n            }));\n        }\n    }\n\n    Ok(json!({ \"files\": files, \"directory\": dir.to_string_lossy() }))\n}\n\npub fn state_show(path: &str) -> Result<Value, String> {\n    let encrypted = path.ends_with(\".enc\");\n    let json_str = if encrypted {\n        let key = std::env::var(\"AGENT_BROWSER_ENCRYPTION_KEY\").map_err(|_| {\n            \"Encrypted state file requires AGENT_BROWSER_ENCRYPTION_KEY\".to_string()\n        })?;\n        let data = fs::read(path).map_err(|e| format!(\"Failed to read state file: {}\", e))?;\n        let decrypted = decrypt_data(&data, &key)?;\n        String::from_utf8(decrypted)\n            .map_err(|e| format!(\"Decrypted state is not valid UTF-8: {}\", e))?\n    } else {\n        fs::read_to_string(path).map_err(|e| format!(\"Failed to read state file: {}\", e))?\n    };\n\n    let state: StorageState =\n        serde_json::from_str(&json_str).map_err(|e| format!(\"Invalid state file: {}\", e))?;\n\n    let metadata = fs::metadata(path).ok();\n    let filename = std::path::Path::new(path)\n        .file_name()\n        .unwrap_or_default()\n        .to_string_lossy()\n        .to_string();\n\n    Ok(json!({\n        \"filename\": filename,\n        \"path\": path,\n        \"size\": metadata.as_ref().map(|m| m.len()).unwrap_or(0),\n        \"modified\": metadata.as_ref()\n            .and_then(|m| m.modified().ok())\n            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())\n            .map(|d| d.as_secs())\n            .unwrap_or(0),\n        \"encrypted\": encrypted,\n        \"summary\": format!(\"{} cookies, {} origins\", state.cookies.len(), state.origins.len()),\n        \"state\": state,\n    }))\n}\n\npub fn state_clear(path: Option<&str>) -> Result<Value, String> {\n    if let Some(p) = path {\n        fs::remove_file(p).map_err(|e| format!(\"Failed to delete state: {}\", e))?;\n        return Ok(json!({ \"deleted\": p }));\n    }\n\n    let dir = get_sessions_dir();\n    if !dir.exists() {\n        return Ok(json!({ \"deleted\": 0 }));\n    }\n\n    let mut count = 0;\n    if let Ok(entries) = fs::read_dir(&dir) {\n        for entry in entries.flatten() {\n            let path = entry.path();\n            if is_state_file(&path) {\n                let _ = fs::remove_file(&path);\n                count += 1;\n            }\n        }\n    }\n\n    Ok(json!({ \"deleted\": count }))\n}\n\npub fn state_clean(max_age_days: u64) -> Result<Value, String> {\n    let dir = get_sessions_dir();\n    if !dir.exists() {\n        return Ok(json!({ \"cleaned\": 0, \"keptCount\": 0, \"days\": max_age_days }));\n    }\n\n    let now = std::time::SystemTime::now();\n    let max_age = std::time::Duration::from_secs(max_age_days * 86400);\n    let mut deleted = 0;\n    let mut kept = 0;\n\n    if let Ok(entries) = fs::read_dir(&dir) {\n        for entry in entries.flatten() {\n            let path = entry.path();\n            if !is_state_file(&path) {\n                continue;\n            }\n\n            if let Ok(metadata) = fs::metadata(&path) {\n                if let Ok(modified) = metadata.modified() {\n                    if let Ok(age) = now.duration_since(modified) {\n                        if age > max_age {\n                            let _ = fs::remove_file(&path);\n                            deleted += 1;\n                            continue;\n                        }\n                    }\n                }\n            }\n            kept += 1;\n        }\n    }\n\n    Ok(json!({ \"cleaned\": deleted, \"keptCount\": kept, \"days\": max_age_days }))\n}\n\npub fn state_rename(old_path: &str, new_name: &str) -> Result<Value, String> {\n    let old = PathBuf::from(old_path);\n    if !old.exists() {\n        return Err(format!(\"State file not found: {}\", old_path));\n    }\n\n    let fallback = PathBuf::from(\".\");\n    let dir = old.parent().unwrap_or(&fallback);\n    let new_path = dir.join(format!(\"{}.json\", new_name));\n\n    fs::rename(&old, &new_path).map_err(|e| format!(\"Failed to rename state: {}\", e))?;\n\n    Ok(json!({\n        \"renamed\": true,\n        \"from\": old_path,\n        \"to\": new_path.to_string_lossy(),\n    }))\n}\n\nfn encrypt_data(data: &[u8], key_str: &str) -> Result<Vec<u8>, String> {\n    let mut hasher = Sha256::new();\n    hasher.update(key_str.as_bytes());\n    let key_bytes = hasher.finalize();\n    let cipher =\n        Aes256Gcm::new_from_slice(&key_bytes).map_err(|e| format!(\"Invalid key: {}\", e))?;\n\n    let mut nonce = [0u8; 12];\n    getrandom::getrandom(&mut nonce).map_err(|e| format!(\"Failed to generate nonce: {}\", e))?;\n    let ciphertext = cipher\n        .encrypt(aes_gcm::Nonce::from_slice(&nonce), data)\n        .map_err(|e| format!(\"Encryption failed: {}\", e))?;\n\n    let mut result = Vec::with_capacity(12 + ciphertext.len());\n    result.extend_from_slice(&nonce);\n    result.extend_from_slice(&ciphertext);\n    Ok(result)\n}\n\nfn decrypt_data(data: &[u8], key_str: &str) -> Result<Vec<u8>, String> {\n    if data.len() < 13 {\n        return Err(\"Ciphertext too short\".to_string());\n    }\n    let (nonce_bytes, ciphertext) = data.split_at(12);\n\n    let mut hasher = Sha256::new();\n    hasher.update(key_str.as_bytes());\n    let key_bytes = hasher.finalize();\n    let cipher =\n        Aes256Gcm::new_from_slice(&key_bytes).map_err(|e| format!(\"Invalid key: {}\", e))?;\n    let plaintext = cipher\n        .decrypt(aes_gcm::Nonce::from_slice(nonce_bytes), ciphertext)\n        .map_err(|e| format!(\"Decryption failed: {}\", e))?;\n    Ok(plaintext)\n}\n\npub fn find_auto_state_file(session_name: &str) -> Option<String> {\n    let dir = get_sessions_dir();\n    if !dir.exists() {\n        return None;\n    }\n    let prefix = format!(\"{}-\", session_name);\n    let mut best_path: Option<(String, std::time::SystemTime)> = None;\n\n    if let Ok(entries) = fs::read_dir(&dir) {\n        for entry in entries.flatten() {\n            let path = entry.path();\n            let fname = path\n                .file_name()\n                .unwrap_or_default()\n                .to_string_lossy()\n                .to_string();\n            let is_match = fname.starts_with(&prefix)\n                && (fname.ends_with(\".json\") || fname.ends_with(\".json.enc\"));\n            if !is_match {\n                continue;\n            }\n            let modified = fs::metadata(&path)\n                .ok()\n                .and_then(|m| m.modified().ok())\n                .unwrap_or(std::time::UNIX_EPOCH);\n            if best_path.as_ref().is_none_or(|(_, t)| modified > *t) {\n                best_path = Some((path.to_string_lossy().to_string(), modified));\n            }\n        }\n    }\n    best_path.map(|(p, _)| p)\n}\n\npub fn get_sessions_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\").join(\"sessions\")\n    } else {\n        std::env::temp_dir().join(\"agent-browser\").join(\"sessions\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_storage_state_serialization() {\n        let state = StorageState {\n            cookies: vec![Cookie {\n                name: \"session\".to_string(),\n                value: \"abc123\".to_string(),\n                domain: \".example.com\".to_string(),\n                path: \"/\".to_string(),\n                expires: 0.0,\n                size: 0,\n                http_only: true,\n                secure: false,\n                session: true,\n                same_site: Some(\"Lax\".to_string()),\n            }],\n            origins: vec![OriginStorage {\n                origin: \"https://example.com\".to_string(),\n                local_storage: vec![StorageEntry {\n                    name: \"key\".to_string(),\n                    value: \"val\".to_string(),\n                }],\n                session_storage: vec![],\n            }],\n        };\n\n        let json = serde_json::to_string_pretty(&state).unwrap();\n        let parsed: StorageState = serde_json::from_str(&json).unwrap();\n        assert_eq!(parsed.cookies.len(), 1);\n        assert_eq!(parsed.cookies[0].name, \"session\");\n        assert_eq!(parsed.origins.len(), 1);\n        assert_eq!(parsed.origins[0].local_storage.len(), 1);\n    }\n\n    #[test]\n    fn test_storage_state_empty() {\n        let state = StorageState {\n            cookies: vec![],\n            origins: vec![],\n        };\n        let json = serde_json::to_string(&state).unwrap();\n        let parsed: StorageState = serde_json::from_str(&json).unwrap();\n        assert!(parsed.cookies.is_empty());\n        assert!(parsed.origins.is_empty());\n    }\n\n    #[test]\n    fn test_state_show_nonexistent_file() {\n        let result = state_show(\"/tmp/nonexistent-agent-browser-state-file.json\");\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_state_clear_nonexistent_file() {\n        let result = state_clear(Some(\"/tmp/nonexistent-agent-browser-state-file.json\"));\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_state_rename_nonexistent() {\n        let result = state_rename(\"/tmp/nonexistent-agent-browser-state-file.json\", \"new-name\");\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"not found\"));\n    }\n\n    #[test]\n    fn test_state_list_returns_json() {\n        let result = state_list().unwrap();\n        assert!(result.get(\"files\").is_some());\n        assert!(result.get(\"directory\").is_some());\n    }\n\n    #[test]\n    fn test_sessions_dir_path() {\n        let dir = get_sessions_dir();\n        assert!(dir.to_string_lossy().contains(\"sessions\"));\n    }\n\n    #[test]\n    fn test_encrypt_decrypt_roundtrip() {\n        let plain = b\"hello world\";\n        let key = \"test-secret-key\";\n        let encrypted = encrypt_data(plain, key).unwrap();\n        assert!(encrypted.len() > 12);\n        assert_ne!(&encrypted[12..], plain);\n        let decrypted = decrypt_data(&encrypted, key).unwrap();\n        assert_eq!(decrypted, plain);\n    }\n\n    #[test]\n    fn test_decrypt_wrong_key_fails() {\n        let plain = b\"secret data\";\n        let encrypted = encrypt_data(plain, \"key1\").unwrap();\n        let result = decrypt_data(&encrypted, \"key2\");\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_cookie_serde_roundtrip() {\n        let cookie = Cookie {\n            name: \"test\".to_string(),\n            value: \"123\".to_string(),\n            domain: \".test.com\".to_string(),\n            path: \"/api\".to_string(),\n            expires: 1700000000.0,\n            size: 7,\n            http_only: false,\n            secure: true,\n            session: false,\n            same_site: Some(\"Strict\".to_string()),\n        };\n\n        let json = serde_json::to_value(&cookie).unwrap();\n        assert_eq!(json[\"name\"], \"test\");\n        assert_eq!(json[\"httpOnly\"], false);\n        assert_eq!(json[\"secure\"], true);\n        assert_eq!(json[\"sameSite\"], \"Strict\");\n    }\n}\n"
  },
  {
    "path": "cli/src/native/storage.rs",
    "content": "use serde_json::{json, Value};\n\nuse super::cdp::client::CdpClient;\nuse super::cdp::types::EvaluateParams;\n\npub async fn storage_get(\n    client: &CdpClient,\n    session_id: &str,\n    storage_type: &str,\n    key: Option<&str>,\n) -> Result<Value, String> {\n    let st = storage_js_name(storage_type);\n\n    if let Some(k) = key {\n        let js = format!(\n            \"{}.getItem({})\",\n            st,\n            serde_json::to_string(k).unwrap_or_default()\n        );\n        let result = eval_simple(client, session_id, &js).await?;\n        Ok(json!({ \"key\": k, \"value\": result }))\n    } else {\n        let js = format!(\n            r#\"(() => {{\n                const s = {};\n                const data = {{}};\n                for (let i = 0; i < s.length; i++) {{\n                    const key = s.key(i);\n                    data[key] = s.getItem(key);\n                }}\n                return data;\n            }})()\"#,\n            st\n        );\n        let result = eval_simple(client, session_id, &js).await?;\n        Ok(json!({ \"data\": result }))\n    }\n}\n\npub async fn storage_set(\n    client: &CdpClient,\n    session_id: &str,\n    storage_type: &str,\n    key: &str,\n    value: &str,\n) -> Result<(), String> {\n    let st = storage_js_name(storage_type);\n    let js = format!(\n        \"{}.setItem({}, {})\",\n        st,\n        serde_json::to_string(key).unwrap_or_default(),\n        serde_json::to_string(value).unwrap_or_default(),\n    );\n    eval_simple(client, session_id, &js).await?;\n    Ok(())\n}\n\npub async fn storage_clear(\n    client: &CdpClient,\n    session_id: &str,\n    storage_type: &str,\n) -> Result<(), String> {\n    let st = storage_js_name(storage_type);\n    let js = format!(\"{}.clear()\", st);\n    eval_simple(client, session_id, &js).await?;\n    Ok(())\n}\n\nfn storage_js_name(storage_type: &str) -> &str {\n    match storage_type {\n        \"session\" => \"sessionStorage\",\n        _ => \"localStorage\",\n    }\n}\n\nasync fn eval_simple(client: &CdpClient, session_id: &str, js: &str) -> Result<Value, String> {\n    let result: super::cdp::types::EvaluateResult = client\n        .send_command_typed(\n            \"Runtime.evaluate\",\n            &EvaluateParams {\n                expression: js.to_string(),\n                return_by_value: Some(true),\n                await_promise: Some(false),\n            },\n            Some(session_id),\n        )\n        .await?;\n\n    if let Some(ref details) = result.exception_details {\n        return Err(format!(\"Storage error: {}\", details.text));\n    }\n\n    Ok(result.result.value.unwrap_or(Value::Null))\n}\n"
  },
  {
    "path": "cli/src/native/stream.rs",
    "content": "use serde_json::{json, Value};\nuse std::net::SocketAddr;\nuse std::sync::Arc;\n\nuse futures_util::{SinkExt, StreamExt};\nuse tokio::net::TcpListener;\nuse tokio::sync::{broadcast, Mutex, Notify, RwLock};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::cdp::client::CdpClient;\n\n/// Frame metadata from CDP Page.screencastFrame events.\n#[derive(Debug, Clone)]\npub struct FrameMetadata {\n    pub offset_top: f64,\n    pub page_scale_factor: f64,\n    pub device_width: u32,\n    pub device_height: u32,\n    pub scroll_offset_x: f64,\n    pub scroll_offset_y: f64,\n    pub timestamp: u64,\n}\n\nimpl Default for FrameMetadata {\n    fn default() -> Self {\n        Self {\n            offset_top: 0.0,\n            page_scale_factor: 1.0,\n            device_width: 1280,\n            device_height: 720,\n            scroll_offset_x: 0.0,\n            scroll_offset_y: 0.0,\n            timestamp: 0,\n        }\n    }\n}\n\npub struct StreamServer {\n    port: u16,\n    frame_tx: broadcast::Sender<String>,\n    client_count: Arc<Mutex<usize>>,\n    client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,\n    /// The active CDP page session ID (from Target.attachToTarget).\n    cdp_session_id: Arc<RwLock<Option<String>>>,\n    client_notify: Arc<Notify>,\n    screencasting: Arc<Mutex<bool>>,\n}\n\nimpl StreamServer {\n    pub async fn start(\n        preferred_port: u16,\n        client: Arc<CdpClient>,\n        session_id: String,\n    ) -> Result<Self, String> {\n        let client_slot = Arc::new(RwLock::new(Some(client)));\n        let (server, _) = Self::start_inner(preferred_port, client_slot, session_id).await?;\n        Ok(server)\n    }\n\n    /// Start the stream server without a CDP client (e.g. at daemon startup before browser launch).\n    /// Returns the server and a shared slot to set the client when the browser launches.\n    /// Input messages are ignored until the client is set.\n    pub async fn start_without_client(\n        preferred_port: u16,\n        session_id: String,\n    ) -> Result<(Self, Arc<RwLock<Option<Arc<CdpClient>>>>), String> {\n        let client_slot = Arc::new(RwLock::new(None::<Arc<CdpClient>>));\n        Self::start_inner(preferred_port, client_slot, session_id).await\n    }\n\n    /// Notify the background CDP listener that the client has changed (browser launched/closed).\n    pub fn notify_client_changed(&self) {\n        self.client_notify.notify_one();\n    }\n\n    /// Update the active CDP page session ID used for screencast commands.\n    pub async fn set_cdp_session_id(&self, session_id: Option<String>) {\n        let mut guard = self.cdp_session_id.write().await;\n        *guard = session_id;\n    }\n\n    /// Check whether the server currently has active screencast running.\n    pub async fn is_screencasting(&self) -> bool {\n        *self.screencasting.lock().await\n    }\n\n    async fn start_inner(\n        preferred_port: u16,\n        client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,\n        _session_id: String,\n    ) -> Result<(Self, Arc<RwLock<Option<Arc<CdpClient>>>>), String> {\n        let addr = format!(\"127.0.0.1:{}\", preferred_port);\n        let listener = TcpListener::bind(&addr)\n            .await\n            .map_err(|e| format!(\"Failed to bind stream server: {}\", e))?;\n\n        let actual_addr = listener\n            .local_addr()\n            .map_err(|e| format!(\"Failed to get stream address: {}\", e))?;\n        let port = actual_addr.port();\n\n        let (frame_tx, _) = broadcast::channel::<String>(64);\n        let client_count = Arc::new(Mutex::new(0usize));\n        let client_notify = Arc::new(Notify::new());\n        let screencasting = Arc::new(Mutex::new(false));\n        let cdp_session_id = Arc::new(RwLock::new(None::<String>));\n\n        let frame_tx_clone = frame_tx.clone();\n        let client_count_clone = client_count.clone();\n        let client_slot_clone = client_slot.clone();\n        let notify_clone = client_notify.clone();\n        let screencasting_clone = screencasting.clone();\n        let cdp_session_clone = cdp_session_id.clone();\n\n        // WebSocket accept loop\n        tokio::spawn(async move {\n            accept_loop(\n                listener,\n                frame_tx_clone,\n                client_count_clone,\n                client_slot_clone,\n                notify_clone,\n                screencasting_clone,\n                cdp_session_clone,\n            )\n            .await;\n        });\n\n        // Background CDP event listener for real-time frame broadcasting\n        let frame_tx_bg = frame_tx.clone();\n        let client_slot_bg = client_slot.clone();\n        let client_notify_bg = client_notify.clone();\n        let screencasting_bg = screencasting.clone();\n        let client_count_bg = client_count.clone();\n        let cdp_session_bg = cdp_session_id.clone();\n        tokio::spawn(async move {\n            cdp_event_loop(\n                frame_tx_bg,\n                client_slot_bg,\n                client_notify_bg,\n                screencasting_bg,\n                client_count_bg,\n                cdp_session_bg,\n            )\n            .await;\n        });\n\n        Ok((\n            Self {\n                port,\n                frame_tx,\n                client_count,\n                client_slot: client_slot.clone(),\n                cdp_session_id,\n                client_notify,\n                screencasting,\n            },\n            client_slot,\n        ))\n    }\n\n    pub fn port(&self) -> u16 {\n        self.port\n    }\n\n    /// Broadcast a raw frame string (legacy).\n    pub fn broadcast_frame(&self, frame_json: &str) {\n        let _ = self.frame_tx.send(frame_json.to_string());\n    }\n\n    /// Broadcast a screencast frame with structured metadata.\n    pub fn broadcast_screencast_frame(&self, base64_data: &str, metadata: &FrameMetadata) {\n        let msg = json!({\n            \"type\": \"frame\",\n            \"data\": base64_data,\n            \"metadata\": {\n                \"offsetTop\": metadata.offset_top,\n                \"pageScaleFactor\": metadata.page_scale_factor,\n                \"deviceWidth\": metadata.device_width,\n                \"deviceHeight\": metadata.device_height,\n                \"scrollOffsetX\": metadata.scroll_offset_x,\n                \"scrollOffsetY\": metadata.scroll_offset_y,\n                \"timestamp\": metadata.timestamp,\n            }\n        });\n        let _ = self.frame_tx.send(msg.to_string());\n    }\n\n    /// Broadcast a status message to all connected clients.\n    pub fn broadcast_status(\n        &self,\n        connected: bool,\n        screencasting: bool,\n        viewport_width: u32,\n        viewport_height: u32,\n    ) {\n        let msg = json!({\n            \"type\": \"status\",\n            \"connected\": connected,\n            \"screencasting\": screencasting,\n            \"viewportWidth\": viewport_width,\n            \"viewportHeight\": viewport_height,\n        });\n        let _ = self.frame_tx.send(msg.to_string());\n    }\n\n    /// Broadcast an error message to all connected clients.\n    pub fn broadcast_error(&self, message: &str) {\n        let msg = json!({\n            \"type\": \"error\",\n            \"message\": message,\n        });\n        let _ = self.frame_tx.send(msg.to_string());\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nasync fn accept_loop(\n    listener: TcpListener,\n    frame_tx: broadcast::Sender<String>,\n    client_count: Arc<Mutex<usize>>,\n    client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,\n    client_notify: Arc<Notify>,\n    screencasting: Arc<Mutex<bool>>,\n    cdp_session_id: Arc<RwLock<Option<String>>>,\n) {\n    while let Ok((stream, addr)) = listener.accept().await {\n        let frame_rx = frame_tx.subscribe();\n        let client_count = client_count.clone();\n        let client_slot = client_slot.clone();\n        let client_notify = client_notify.clone();\n        let screencasting = screencasting.clone();\n        let cdp_session_id = cdp_session_id.clone();\n\n        tokio::spawn(async move {\n            handle_ws_client(\n                stream,\n                addr,\n                frame_rx,\n                client_count,\n                client_slot,\n                client_notify,\n                screencasting,\n                cdp_session_id,\n            )\n            .await;\n        });\n    }\n}\n\n#[allow(clippy::result_large_err, clippy::too_many_arguments)]\nasync fn handle_ws_client(\n    stream: tokio::net::TcpStream,\n    _addr: SocketAddr,\n    mut frame_rx: broadcast::Receiver<String>,\n    client_count: Arc<Mutex<usize>>,\n    client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,\n    client_notify: Arc<Notify>,\n    screencasting: Arc<Mutex<bool>>,\n    cdp_session_id: Arc<RwLock<Option<String>>>,\n) {\n    let callback =\n        |req: &tokio_tungstenite::tungstenite::handshake::server::Request,\n         resp: tokio_tungstenite::tungstenite::handshake::server::Response| {\n            let origin = req\n                .headers()\n                .get(\"origin\")\n                .and_then(|v| v.to_str().ok())\n                .map(|s| s.to_string());\n            if !is_allowed_origin(origin.as_deref()) {\n                let mut reject =\n                    tokio_tungstenite::tungstenite::handshake::server::ErrorResponse::new(Some(\n                        \"Origin not allowed\".to_string(),\n                    ));\n                *reject.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::FORBIDDEN;\n                return Err(reject);\n            }\n            Ok(resp)\n        };\n\n    let ws_stream = match tokio_tungstenite::accept_hdr_async(stream, callback).await {\n        Ok(ws) => ws,\n        Err(_) => return,\n    };\n\n    {\n        let mut count = client_count.lock().await;\n        *count += 1;\n    }\n\n    let (mut ws_tx, mut ws_rx) = ws_stream.split();\n\n    // Send initial status (screencasting:false initially, matching 0.19.0)\n    {\n        let guard = client_slot.read().await;\n        let connected = guard.is_some();\n        let sc = *screencasting.lock().await;\n        let status = json!({\n            \"type\": \"status\",\n            \"connected\": connected,\n            \"screencasting\": sc,\n            \"viewportWidth\": 1280,\n            \"viewportHeight\": 720,\n        });\n        let _ = ws_tx.send(Message::Text(status.to_string())).await;\n    }\n\n    // Notify the CDP event loop that a client connected (may trigger auto-start screencast)\n    client_notify.notify_one();\n\n    loop {\n        tokio::select! {\n            frame = frame_rx.recv() => {\n                match frame {\n                    Ok(data) => {\n                        if ws_tx.send(Message::Text(data)).await.is_err() {\n                            break;\n                        }\n                    }\n                    Err(broadcast::error::RecvError::Lagged(_)) => {\n                        // Slow consumer; skip missed frames and continue\n                        continue;\n                    }\n                    Err(broadcast::error::RecvError::Closed) => break,\n                }\n            }\n            msg = ws_rx.next() => {\n                match msg {\n                    Some(Ok(Message::Text(text))) => {\n                        let guard = client_slot.read().await;\n                        if let Some(ref client) = *guard {\n                            let sid = cdp_session_id.read().await;\n                            handle_client_message(&text, client.as_ref(), sid.as_deref()).await;\n                        }\n                    }\n                    Some(Ok(Message::Close(_))) | None => break,\n                    _ => {}\n                }\n            }\n        }\n    }\n\n    {\n        let mut count = client_count.lock().await;\n        *count = count.saturating_sub(1);\n    }\n\n    // Notify the CDP event loop that a client disconnected (may trigger auto-stop screencast)\n    client_notify.notify_one();\n}\n\n/// Background task that subscribes to CDP events and broadcasts screencast frames in real-time.\n/// Also handles auto-start/stop of screencast based on WebSocket client count.\nasync fn cdp_event_loop(\n    frame_tx: broadcast::Sender<String>,\n    client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,\n    client_notify: Arc<Notify>,\n    screencasting: Arc<Mutex<bool>>,\n    client_count: Arc<Mutex<usize>>,\n    cdp_session_id: Arc<RwLock<Option<String>>>,\n) {\n    loop {\n        // Wait until we're notified of a client/connection change\n        client_notify.notified().await;\n\n        // Check if we have WS clients and a CDP client\n        let count = *client_count.lock().await;\n        let guard = client_slot.read().await;\n\n        if count > 0 {\n            if let Some(ref client) = *guard {\n                // We have WS clients and a CDP client — start screencast and listen for frames\n                let mut event_rx = client.subscribe();\n                let client_arc = Arc::clone(client);\n                drop(guard);\n\n                // Get the CDP page session ID for targeted commands\n                let session_id = cdp_session_id.read().await.clone();\n\n                let _ = client_arc\n                    .send_command(\n                        \"Page.startScreencast\",\n                        Some(json!({\n                            \"format\": \"jpeg\",\n                            \"quality\": 80,\n                            \"maxWidth\": 1280,\n                            \"maxHeight\": 720,\n                            \"everyNthFrame\": 1,\n                        })),\n                        session_id.as_deref(),\n                    )\n                    .await;\n\n                {\n                    let mut sc = screencasting.lock().await;\n                    *sc = true;\n                }\n\n                // Broadcast screencasting:true status (matching 0.19.0 two-status sequence)\n                let status = json!({\n                    \"type\": \"status\",\n                    \"connected\": true,\n                    \"screencasting\": true,\n                    \"viewportWidth\": 1280,\n                    \"viewportHeight\": 720,\n                });\n                let _ = frame_tx.send(status.to_string());\n\n                // Process CDP events in real-time until client disconnects or CDP closes\n                loop {\n                    tokio::select! {\n                        event = event_rx.recv() => {\n                            match event {\n                                Ok(evt) => {\n                                    if evt.method == \"Page.screencastFrame\" {\n                                        // Ack immediately (like 0.19.0)\n                                        if let Some(sid) = evt.params.get(\"sessionId\").and_then(|v| v.as_i64()) {\n                                            let _ = client_arc.send_command(\n                                                \"Page.screencastFrameAck\",\n                                                Some(json!({ \"sessionId\": sid })),\n                                                evt.session_id.as_deref(),\n                                            ).await;\n                                        }\n\n                                        // Broadcast frame to WS clients\n                                        if let Some(data) = evt.params.get(\"data\").and_then(|v| v.as_str()) {\n                                            let meta = evt.params.get(\"metadata\");\n                                            let msg = json!({\n                                                \"type\": \"frame\",\n                                                \"data\": data,\n                                                \"metadata\": {\n                                                    \"offsetTop\": meta.and_then(|m| m.get(\"offsetTop\")).and_then(|v| v.as_f64()).unwrap_or(0.0),\n                                                    \"pageScaleFactor\": meta.and_then(|m| m.get(\"pageScaleFactor\")).and_then(|v| v.as_f64()).unwrap_or(1.0),\n                                                    \"deviceWidth\": meta.and_then(|m| m.get(\"deviceWidth\")).and_then(|v| v.as_u64()).unwrap_or(1280),\n                                                    \"deviceHeight\": meta.and_then(|m| m.get(\"deviceHeight\")).and_then(|v| v.as_u64()).unwrap_or(720),\n                                                    \"scrollOffsetX\": meta.and_then(|m| m.get(\"scrollOffsetX\")).and_then(|v| v.as_f64()).unwrap_or(0.0),\n                                                    \"scrollOffsetY\": meta.and_then(|m| m.get(\"scrollOffsetY\")).and_then(|v| v.as_f64()).unwrap_or(0.0),\n                                                    \"timestamp\": meta.and_then(|m| m.get(\"timestamp\")).and_then(|v| v.as_u64()).unwrap_or(0),\n                                                }\n                                            });\n                                            let _ = frame_tx.send(msg.to_string());\n                                        }\n                                    }\n                                }\n                                Err(broadcast::error::RecvError::Lagged(_)) => continue,\n                                Err(broadcast::error::RecvError::Closed) => break,\n                            }\n                        }\n                        // Also check for notify (client count change or CDP client change)\n                        _ = client_notify.notified() => {\n                            let count = *client_count.lock().await;\n                            let session_id = cdp_session_id.read().await.clone();\n                            if count == 0 {\n                                // All WS clients gone — stop screencast\n                                let _ = client_arc\n                                    .send_command_no_params(\"Page.stopScreencast\", session_id.as_deref())\n                                    .await;\n                                let mut sc = screencasting.lock().await;\n                                *sc = false;\n                                break;\n                            }\n                            // Check if CDP client changed (browser closed/relaunched)\n                            let client_changed = {\n                                let guard = client_slot.read().await;\n                                let same = guard\n                                    .as_ref()\n                                    .is_some_and(|c| Arc::ptr_eq(c, &client_arc));\n                                !same\n                            };\n                            if client_changed {\n                                // CDP client changed — stop our screencast and restart loop\n                                let _ = client_arc\n                                    .send_command_no_params(\"Page.stopScreencast\", session_id.as_deref())\n                                    .await;\n                                let mut sc = screencasting.lock().await;\n                                *sc = false;\n                                // Re-notify so we pick up the new client in the outer loop\n                                client_notify.notify_one();\n                                break;\n                            }\n                        }\n                    }\n                }\n            } else {\n                drop(guard);\n                // No CDP client yet — wait for next notification\n            }\n        } else {\n            // No WS clients — if screencasting, stop it\n            let was_screencasting = *screencasting.lock().await;\n            if was_screencasting {\n                if let Some(ref client) = *guard {\n                    let session_id = cdp_session_id.read().await.clone();\n                    let _ = client\n                        .send_command_no_params(\"Page.stopScreencast\", session_id.as_deref())\n                        .await;\n                }\n                let mut sc = screencasting.lock().await;\n                *sc = false;\n            }\n            drop(guard);\n        }\n    }\n}\n\nasync fn handle_client_message(msg: &str, client: &CdpClient, session_id: Option<&str>) {\n    let parsed: Value = match serde_json::from_str(msg) {\n        Ok(v) => v,\n        Err(_) => return,\n    };\n\n    let msg_type = parsed.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"\");\n\n    match msg_type {\n        \"input_mouse\" => {\n            let _ = client\n                .send_command(\n                    \"Input.dispatchMouseEvent\",\n                    Some(json!({\n                        \"type\": parsed.get(\"eventType\").and_then(|v| v.as_str()).unwrap_or(\"mouseMoved\"),\n                        \"x\": parsed.get(\"x\").and_then(|v| v.as_f64()).unwrap_or(0.0),\n                        \"y\": parsed.get(\"y\").and_then(|v| v.as_f64()).unwrap_or(0.0),\n                        \"button\": parsed.get(\"button\").and_then(|v| v.as_str()).unwrap_or(\"none\"),\n                        \"clickCount\": parsed.get(\"clickCount\").and_then(|v| v.as_i64()).unwrap_or(0),\n                        \"deltaX\": parsed.get(\"deltaX\").and_then(|v| v.as_f64()).unwrap_or(0.0),\n                        \"deltaY\": parsed.get(\"deltaY\").and_then(|v| v.as_f64()).unwrap_or(0.0),\n                        \"modifiers\": parsed.get(\"modifiers\").and_then(|v| v.as_i64()).unwrap_or(0),\n                    })),\n                    session_id,\n                )\n                .await;\n        }\n        \"input_keyboard\" => {\n            let _ = client\n                .send_command(\n                    \"Input.dispatchKeyEvent\",\n                    Some(json!({\n                        \"type\": parsed.get(\"eventType\").and_then(|v| v.as_str()).unwrap_or(\"keyDown\"),\n                        \"key\": parsed.get(\"key\"),\n                        \"code\": parsed.get(\"code\"),\n                        \"text\": parsed.get(\"text\"),\n                        \"modifiers\": parsed.get(\"modifiers\").and_then(|v| v.as_i64()).unwrap_or(0),\n                    })),\n                    session_id,\n                )\n                .await;\n        }\n        \"input_touch\" => {\n            let _ = client\n                .send_command(\n                    \"Input.dispatchTouchEvent\",\n                    Some(json!({\n                        \"type\": parsed.get(\"eventType\").and_then(|v| v.as_str()).unwrap_or(\"touchStart\"),\n                        \"touchPoints\": parsed.get(\"touchPoints\").unwrap_or(&json!([])),\n                        \"modifiers\": parsed.get(\"modifiers\").and_then(|v| v.as_i64()).unwrap_or(0),\n                    })),\n                    session_id,\n                )\n                .await;\n        }\n        \"status\" => {\n            // Client requesting status -- handled via broadcast_status from the caller\n        }\n        _ => {}\n    }\n}\n\npub fn is_allowed_origin(origin: Option<&str>) -> bool {\n    match origin {\n        None => true,\n        Some(o) => {\n            if o.starts_with(\"file://\") {\n                return true;\n            }\n            if let Ok(url) = url::Url::parse(o) {\n                let host = url.host_str().unwrap_or(\"\");\n                host == \"localhost\" || host == \"127.0.0.1\" || host == \"::1\" || host == \"[::1]\"\n            } else {\n                false\n            }\n        }\n    }\n}\n\npub async fn start_screencast(\n    client: &CdpClient,\n    session_id: &str,\n    format: &str,\n    quality: i32,\n    max_width: i32,\n    max_height: i32,\n) -> Result<(), String> {\n    client\n        .send_command(\n            \"Page.startScreencast\",\n            Some(json!({\n                \"format\": format,\n                \"quality\": quality,\n                \"maxWidth\": max_width,\n                \"maxHeight\": max_height,\n                \"everyNthFrame\": 1,\n            })),\n            Some(session_id),\n        )\n        .await?;\n    Ok(())\n}\n\npub async fn stop_screencast(client: &CdpClient, session_id: &str) -> Result<(), String> {\n    client\n        .send_command_no_params(\"Page.stopScreencast\", Some(session_id))\n        .await?;\n    Ok(())\n}\n\npub async fn ack_screencast_frame(\n    client: &CdpClient,\n    session_id: &str,\n    screencast_session_id: i64,\n) -> Result<(), String> {\n    client\n        .send_command(\n            \"Page.screencastFrameAck\",\n            Some(json!({ \"sessionId\": screencast_session_id })),\n            Some(session_id),\n        )\n        .await?;\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_allowed_origin_none() {\n        assert!(is_allowed_origin(None));\n    }\n\n    #[test]\n    fn test_allowed_origin_file() {\n        assert!(is_allowed_origin(Some(\"file:///path/to/file\")));\n    }\n\n    #[test]\n    fn test_allowed_origin_localhost() {\n        assert!(is_allowed_origin(Some(\"http://localhost:3000\")));\n        assert!(is_allowed_origin(Some(\"http://127.0.0.1:8080\")));\n    }\n\n    #[test]\n    fn test_disallowed_origin() {\n        assert!(!is_allowed_origin(Some(\"http://evil.com\")));\n    }\n\n    #[test]\n    fn test_frame_metadata_default() {\n        let meta = FrameMetadata::default();\n        assert_eq!(meta.device_width, 1280);\n        assert_eq!(meta.device_height, 720);\n        assert_eq!(meta.page_scale_factor, 1.0);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/test_fixtures/drag_probe.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Drag Probe</title>\n  <style>\n    body {\n      margin: 0;\n      font: 14px/1.4 sans-serif;\n      background: #f4f4f4;\n    }\n\n    #pad {\n      position: relative;\n      width: 800px;\n      height: 500px;\n      margin: 24px;\n      border: 1px solid #999;\n      background: white;\n      overflow: hidden;\n    }\n\n    #target {\n      position: absolute;\n      left: 320px;\n      top: 40px;\n      width: 100px;\n      height: 40px;\n      background: #e34c26;\n      color: white;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      user-select: none;\n      cursor: grab;\n    }\n\n    #target.dragging {\n      cursor: grabbing;\n      background: #0d9488;\n    }\n\n    #log {\n      margin: 24px;\n      white-space: pre-wrap;\n      font-family: ui-monospace, monospace;\n    }\n  </style>\n</head>\n<body>\n  <div id=\"pad\">\n    <div id=\"target\">drag me</div>\n  </div>\n  <pre id=\"log\"></pre>\n  <script>\n    const target = document.getElementById(\"target\");\n    const logEl = document.getElementById(\"log\");\n\n    window.__dragProbe = {\n      dragging: false,\n      events: [],\n      finalLeft: 320,\n      finalTop: 40,\n    };\n\n    let offsetX = 0;\n    let offsetY = 0;\n\n    function pushEvent(event, extra = {}) {\n      window.__dragProbe.events.push({\n        type: event.type,\n        button: event.button,\n        buttons: event.buttons,\n        x: event.clientX,\n        y: event.clientY,\n        target: event.target.id || event.target.tagName,\n        ...extra,\n      });\n      logEl.textContent = JSON.stringify(window.__dragProbe, null, 2);\n    }\n\n    function onPointerLikeStart(event) {\n      if (event.type === \"mousedown\") {\n        const rect = target.getBoundingClientRect();\n        offsetX = event.clientX - rect.left;\n        offsetY = event.clientY - rect.top;\n        window.__dragProbe.dragging = true;\n        target.classList.add(\"dragging\");\n        event.preventDefault();\n      }\n      pushEvent(event, { phase: \"start\" });\n    }\n\n    target.addEventListener(\"mousedown\", (event) => {\n      const rect = target.getBoundingClientRect();\n      offsetX = event.clientX - rect.left;\n      offsetY = event.clientY - rect.top;\n      window.__dragProbe.dragging = true;\n      target.classList.add(\"dragging\");\n      event.preventDefault();\n      pushEvent(event, { phase: \"start\" });\n    });\n    target.addEventListener(\"pointerdown\", onPointerLikeStart);\n\n    document.addEventListener(\"mousemove\", (event) => {\n      if (window.__dragProbe.dragging) {\n        const left = event.clientX - offsetX;\n        const top = event.clientY - offsetY;\n        target.style.left = `${left}px`;\n        target.style.top = `${top}px`;\n        window.__dragProbe.finalLeft = left;\n        window.__dragProbe.finalTop = top;\n      }\n      pushEvent(event);\n    });\n    document.addEventListener(\"pointermove\", (event) => {\n      pushEvent(event);\n    });\n\n    document.addEventListener(\"mouseup\", (event) => {\n      if (window.__dragProbe.dragging) {\n        window.__dragProbe.dragging = false;\n        target.classList.remove(\"dragging\");\n      }\n      pushEvent(event, { phase: \"end\" });\n    });\n    document.addEventListener(\"pointerup\", (event) => {\n      pushEvent(event, { phase: \"end\" });\n    });\n    target.addEventListener(\"dragstart\", (event) => {\n      pushEvent(event, { phase: \"dragstart\" });\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "cli/src/native/test_fixtures/html5_drag_probe.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>HTML5 Drag Probe</title>\n  <style>\n    body {\n      margin: 24px;\n      font: 14px/1.4 sans-serif;\n    }\n\n    #source, #dest {\n      width: 120px;\n      height: 80px;\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      border: 1px solid #666;\n      user-select: none;\n      margin-right: 40px;\n    }\n\n    #source {\n      background: #f97316;\n      color: white;\n    }\n\n    #dest {\n      background: #e5e7eb;\n    }\n\n    pre {\n      margin-top: 24px;\n      white-space: pre-wrap;\n      font-family: ui-monospace, monospace;\n    }\n  </style>\n</head>\n<body>\n  <div id=\"source\" draggable=\"true\">drag source</div>\n  <div id=\"dest\">drop zone</div>\n  <pre id=\"log\"></pre>\n  <script>\n    const source = document.getElementById(\"source\");\n    const dest = document.getElementById(\"dest\");\n    const logEl = document.getElementById(\"log\");\n\n    window.__html5DragProbe = { events: [] };\n\n    function pushEvent(event, extra = {}) {\n      window.__html5DragProbe.events.push({\n        type: event.type,\n        target: event.target.id || event.target.tagName,\n        x: event.clientX,\n        y: event.clientY,\n        button: event.button,\n        buttons: event.buttons,\n        ...extra,\n      });\n      logEl.textContent = JSON.stringify(window.__html5DragProbe, null, 2);\n    }\n\n    for (const type of [\"pointerdown\", \"mousedown\", \"dragstart\", \"drag\", \"dragend\"]) {\n      source.addEventListener(type, (event) => {\n        if (type === \"dragstart\") {\n          event.dataTransfer.setData(\"text/plain\", \"probe\");\n        }\n        pushEvent(event);\n      });\n    }\n\n    for (const type of [\"pointermove\", \"mousemove\", \"dragenter\", \"dragover\", \"drop\", \"pointerup\", \"mouseup\"]) {\n      document.addEventListener(type, (event) => {\n        if (type === \"dragover\") {\n          event.preventDefault();\n        }\n        if (type === \"drop\") {\n          pushEvent(event, { dropped: event.dataTransfer.getData(\"text/plain\") });\n          return;\n        }\n        pushEvent(event);\n      });\n    }\n\n    dest.addEventListener(\"dragover\", (event) => event.preventDefault());\n    dest.addEventListener(\"drop\", (event) => {\n      pushEvent(event, { dropped: event.dataTransfer.getData(\"text/plain\") });\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "cli/src/native/test_fixtures/pointer_capture_probe.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Pointer Capture Probe</title>\n  <style>\n    body {\n      margin: 24px;\n      font: 14px/1.4 sans-serif;\n    }\n    #crop {\n      position: relative;\n      width: 240px;\n      height: 180px;\n      border: 2px solid #fff;\n      outline: 1px solid #555;\n      background: rgba(0, 0, 0, 0.2);\n    }\n    #handle {\n      position: absolute;\n      width: 20px;\n      height: 20px;\n      top: -16px;\n      left: -16px;\n      padding-top: 13px;\n      padding-left: 13px;\n      box-sizing: content-box;\n      background: rgba(255, 0, 0, 0.25);\n    }\n    #handle::after {\n      content: \"\";\n      display: block;\n      width: 20px;\n      height: 20px;\n      border-top: 2px solid white;\n      border-left: 2px solid white;\n    }\n    pre {\n      margin-top: 24px;\n      white-space: pre-wrap;\n      font-family: ui-monospace, monospace;\n    }\n  </style>\n</head>\n<body>\n  <div id=\"crop\" aria-label=\"crop area\">\n    <div id=\"handle\" aria-label=\"crop handle topLeft\" data-anchor=\"topLeft\"></div>\n  </div>\n  <pre id=\"log\"></pre>\n  <script>\n    const crop = document.getElementById(\"crop\");\n    const handle = document.getElementById(\"handle\");\n    const logEl = document.getElementById(\"log\");\n\n    const state = {\n      targetAnchor: null,\n      dragging: false,\n      moved: false,\n      events: [],\n    };\n    window.__pointerCaptureProbe = state;\n\n    function sync() {\n      logEl.textContent = JSON.stringify(state, null, 2);\n    }\n\n    function push(event, extra = {}) {\n      state.events.push({\n        type: event.type,\n        target: event.target.id || event.target.tagName,\n        currentTarget: event.currentTarget.id || event.currentTarget.tagName,\n        pointerId: event.pointerId,\n        button: event.button,\n        buttons: event.buttons,\n        hasCapture: event.currentTarget.hasPointerCapture?.(event.pointerId) ?? false,\n        x: event.clientX,\n        y: event.clientY,\n        ...extra,\n      });\n      sync();\n    }\n\n    crop.addEventListener(\"pointerdown\", (event) => {\n      state.targetAnchor = event.target.getAttribute(\"data-anchor\");\n      crop.setPointerCapture(event.pointerId);\n      event.preventDefault();\n      push(event, { phase: \"down\", targetAnchor: state.targetAnchor });\n    });\n\n    crop.addEventListener(\"pointermove\", (event) => {\n      const hasCapture = crop.hasPointerCapture(event.pointerId);\n      if (hasCapture && state.targetAnchor) {\n        state.dragging = true;\n        state.moved = true;\n      }\n      push(event, { phase: hasCapture ? \"drag\" : \"hover\", targetAnchor: state.targetAnchor });\n    });\n\n    crop.addEventListener(\"pointerup\", (event) => {\n      const hadCapture = crop.hasPointerCapture(event.pointerId);\n      state.dragging = false;\n      push(event, { phase: \"up\", targetAnchor: state.targetAnchor, hadCapture });\n      state.targetAnchor = null;\n    });\n\n    handle.addEventListener(\"pointerdown\", (event) => push(event, { listener: \"handle\" }));\n    handle.addEventListener(\"pointermove\", (event) => push(event, { listener: \"handle\" }));\n    handle.addEventListener(\"pointerup\", (event) => push(event, { listener: \"handle\" }));\n\n    sync();\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "cli/src/native/tracing.rs",
    "content": "use serde_json::{json, Value};\nuse std::path::PathBuf;\n\nuse super::cdp::client::CdpClient;\n\nconst MAX_PROFILE_EVENTS: usize = 5_000_000;\n\nconst DEFAULT_PROFILER_CATEGORIES: &[&str] = &[\n    \"devtools.timeline\",\n    \"disabled-by-default-devtools.timeline\",\n    \"disabled-by-default-devtools.timeline.frame\",\n    \"disabled-by-default-devtools.timeline.stack\",\n    \"v8.execute\",\n    \"disabled-by-default-v8.cpu_profiler\",\n    \"disabled-by-default-v8.cpu_profiler.hires\",\n    \"v8\",\n    \"disabled-by-default-v8.runtime_stats\",\n    \"blink\",\n    \"blink.user_timing\",\n    \"latencyInfo\",\n    \"renderer.scheduler\",\n    \"sequence_manager\",\n    \"toplevel\",\n];\n\npub struct TracingState {\n    pub active: bool,\n    pub events: Vec<Value>,\n    pub events_dropped: bool,\n}\n\nimpl TracingState {\n    pub fn new() -> Self {\n        Self {\n            active: false,\n            events: Vec::new(),\n            events_dropped: false,\n        }\n    }\n}\n\npub async fn trace_start(\n    client: &CdpClient,\n    session_id: &str,\n    tracing_state: &mut TracingState,\n) -> Result<Value, String> {\n    if tracing_state.active {\n        return Err(\"Tracing already active\".to_string());\n    }\n\n    client\n        .send_command(\n            \"Tracing.start\",\n            Some(json!({\n                \"traceConfig\": {\n                    \"recordMode\": \"recordContinuously\",\n                },\n                \"transferMode\": \"ReturnAsStream\",\n            })),\n            Some(session_id),\n        )\n        .await?;\n\n    tracing_state.active = true;\n    tracing_state.events.clear();\n    tracing_state.events_dropped = false;\n\n    Ok(json!({ \"started\": true }))\n}\n\npub async fn trace_stop(\n    client: &CdpClient,\n    session_id: &str,\n    tracing_state: &mut TracingState,\n    path: Option<&str>,\n) -> Result<Value, String> {\n    if !tracing_state.active {\n        return Err(\"No tracing in progress\".to_string());\n    }\n\n    // Subscribe to events before stopping\n    let mut rx = client.subscribe();\n\n    client\n        .send_command_no_params(\"Tracing.end\", Some(session_id))\n        .await?;\n\n    // Collect trace data with timeout\n    let mut trace_events: Vec<Value> = Vec::new();\n    let mut stream_handle: Option<String> = None;\n\n    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(30);\n\n    loop {\n        let result = tokio::time::timeout_at(deadline, rx.recv()).await;\n\n        match result {\n            Ok(Ok(event)) => {\n                if event.session_id.as_deref() != Some(session_id) {\n                    continue;\n                }\n                match event.method.as_str() {\n                    \"Tracing.dataCollected\" => {\n                        if let Some(arr) = event.params.get(\"value\").and_then(|v| v.as_array()) {\n                            trace_events.extend(arr.iter().cloned());\n                        }\n                    }\n                    \"Tracing.tracingComplete\" => {\n                        stream_handle = event\n                            .params\n                            .get(\"stream\")\n                            .and_then(|v| v.as_str())\n                            .map(String::from);\n                        break;\n                    }\n                    _ => {}\n                }\n            }\n            Ok(Err(_)) => break,\n            Err(_) => {\n                return Err(\"Tracing stop timed out after 30s\".to_string());\n            }\n        }\n    }\n\n    // If ReturnAsStream mode was used, read trace data from the IO stream\n    if let Some(handle) = stream_handle {\n        if trace_events.is_empty() {\n            let stream_data = read_io_stream(client, session_id, &handle).await?;\n            if let Ok(parsed) = serde_json::from_str::<Value>(&stream_data) {\n                if let Some(events) = parsed.get(\"traceEvents\").and_then(|v| v.as_array()) {\n                    trace_events.extend(events.iter().cloned());\n                }\n            } else {\n                // Try parsing as newline-delimited JSON\n                for line in stream_data.lines() {\n                    if let Ok(val) = serde_json::from_str::<Value>(line) {\n                        if let Some(events) = val.get(\"traceEvents\").and_then(|v| v.as_array()) {\n                            trace_events.extend(events.iter().cloned());\n                        } else {\n                            trace_events.push(val);\n                        }\n                    }\n                }\n            }\n        }\n        // Close the IO stream\n        let _ = client\n            .send_command(\n                \"IO.close\",\n                Some(json!({ \"handle\": handle })),\n                Some(session_id),\n            )\n            .await;\n    }\n\n    tracing_state.active = false;\n\n    let save_path = match path {\n        Some(p) => p.to_string(),\n        None => {\n            let dir = get_traces_dir();\n            let _ = std::fs::create_dir_all(&dir);\n            let timestamp = std::time::SystemTime::now()\n                .duration_since(std::time::UNIX_EPOCH)\n                .unwrap_or_default()\n                .as_millis();\n            dir.join(format!(\"trace-{}.json\", timestamp))\n                .to_string_lossy()\n                .to_string()\n        }\n    };\n\n    let trace_json = json!({ \"traceEvents\": trace_events });\n    let json_str = serde_json::to_string(&trace_json)\n        .map_err(|e| format!(\"Failed to serialize trace: {}\", e))?;\n    std::fs::write(&save_path, json_str)\n        .map_err(|e| format!(\"Failed to write trace to {}: {}\", save_path, e))?;\n\n    Ok(json!({ \"path\": save_path, \"eventCount\": trace_events.len() }))\n}\n\npub async fn profiler_start(\n    client: &CdpClient,\n    session_id: &str,\n    tracing_state: &mut TracingState,\n    categories: Option<Vec<String>>,\n) -> Result<Value, String> {\n    if tracing_state.active {\n        return Err(\"Profiling/tracing already active\".to_string());\n    }\n\n    let cats: Vec<String> = categories.unwrap_or_else(|| {\n        DEFAULT_PROFILER_CATEGORIES\n            .iter()\n            .map(|s| s.to_string())\n            .collect()\n    });\n\n    client\n        .send_command(\n            \"Tracing.start\",\n            Some(json!({\n                \"traceConfig\": {\n                    \"includedCategories\": cats,\n                    \"enableSampling\": true,\n                },\n                \"transferMode\": \"ReportEvents\",\n            })),\n            Some(session_id),\n        )\n        .await?;\n\n    tracing_state.active = true;\n    tracing_state.events.clear();\n    tracing_state.events_dropped = false;\n\n    Ok(json!({ \"started\": true }))\n}\n\npub async fn profiler_stop(\n    client: &CdpClient,\n    session_id: &str,\n    tracing_state: &mut TracingState,\n    path: Option<&str>,\n) -> Result<Value, String> {\n    if !tracing_state.active {\n        return Err(\"No profiling in progress\".to_string());\n    }\n\n    let mut rx = client.subscribe();\n\n    client\n        .send_command_no_params(\"Tracing.end\", Some(session_id))\n        .await?;\n\n    let mut events: Vec<Value> = Vec::new();\n    let mut dropped = false;\n    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(30);\n\n    loop {\n        let result = tokio::time::timeout_at(deadline, rx.recv()).await;\n\n        match result {\n            Ok(Ok(event)) => {\n                if event.session_id.as_deref() != Some(session_id) {\n                    continue;\n                }\n                match event.method.as_str() {\n                    \"Tracing.dataCollected\" => {\n                        if let Some(arr) = event.params.get(\"value\").and_then(|v| v.as_array()) {\n                            if events.len() + arr.len() > MAX_PROFILE_EVENTS {\n                                dropped = true;\n                            } else {\n                                events.extend(arr.iter().cloned());\n                            }\n                        }\n                    }\n                    \"Tracing.tracingComplete\" => {\n                        break;\n                    }\n                    _ => {}\n                }\n            }\n            Ok(Err(_)) => break,\n            Err(_) => {\n                return Err(\"Profiler stop timed out after 30s\".to_string());\n            }\n        }\n    }\n\n    tracing_state.active = false;\n\n    let save_path = match path {\n        Some(p) => p.to_string(),\n        None => {\n            let dir = get_profiles_dir();\n            let _ = std::fs::create_dir_all(&dir);\n            let timestamp = std::time::SystemTime::now()\n                .duration_since(std::time::UNIX_EPOCH)\n                .unwrap_or_default()\n                .as_millis();\n            dir.join(format!(\"profile-{}.json\", timestamp))\n                .to_string_lossy()\n                .to_string()\n        }\n    };\n\n    let clock_domain = get_clock_domain();\n    let mut profile = json!({ \"traceEvents\": events });\n    if let Some(cd) = clock_domain {\n        profile\n            .as_object_mut()\n            .unwrap()\n            .insert(\"metadata\".to_string(), json!({ \"clock-domain\": cd }));\n    }\n\n    let json_str = serde_json::to_string(&profile)\n        .map_err(|e| format!(\"Failed to serialize profile: {}\", e))?;\n    std::fs::write(&save_path, json_str)\n        .map_err(|e| format!(\"Failed to write profile to {}: {}\", save_path, e))?;\n\n    let event_count = events.len();\n    let mut result = json!({ \"path\": save_path, \"eventCount\": event_count });\n    if dropped {\n        result.as_object_mut().unwrap().insert(\n            \"warning\".to_string(),\n            Value::String(format!(\n                \"Events exceeded {} limit; some dropped\",\n                MAX_PROFILE_EVENTS\n            )),\n        );\n    }\n\n    Ok(result)\n}\n\n/// Read all data from a CDP IO stream handle.\nasync fn read_io_stream(\n    client: &CdpClient,\n    session_id: &str,\n    handle: &str,\n) -> Result<String, String> {\n    let mut data = String::new();\n    loop {\n        let result = client\n            .send_command(\n                \"IO.read\",\n                Some(json!({\n                    \"handle\": handle,\n                    \"size\": 1024 * 1024,\n                })),\n                Some(session_id),\n            )\n            .await?;\n\n        if let Some(chunk) = result.get(\"data\").and_then(|v| v.as_str()) {\n            data.push_str(chunk);\n        }\n\n        let eof = result.get(\"eof\").and_then(|v| v.as_bool()).unwrap_or(true);\n        if eof {\n            break;\n        }\n    }\n    Ok(data)\n}\n\nfn get_clock_domain() -> Option<&'static str> {\n    if cfg!(target_os = \"linux\") {\n        Some(\"LINUX_CLOCK_MONOTONIC\")\n    } else if cfg!(target_os = \"macos\") {\n        Some(\"MAC_MACH_ABSOLUTE_TIME\")\n    } else {\n        None\n    }\n}\n\nfn get_traces_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\").join(\"tmp\").join(\"traces\")\n    } else {\n        std::env::temp_dir().join(\"agent-browser\").join(\"traces\")\n    }\n}\n\nfn get_profiles_dir() -> PathBuf {\n    if let Some(home) = dirs::home_dir() {\n        home.join(\".agent-browser\").join(\"tmp\").join(\"profiles\")\n    } else {\n        std::env::temp_dir().join(\"agent-browser\").join(\"profiles\")\n    }\n}\n"
  },
  {
    "path": "cli/src/native/webdriver/appium.rs",
    "content": "use serde_json::{json, Value};\nuse std::process::{Child, Command, Stdio};\nuse std::time::Duration;\n\nuse super::client::WebDriverClient;\n\nconst APPIUM_DEFAULT_PORT: u16 = 4723;\nconst APPIUM_STARTUP_TIMEOUT_SECS: u64 = 30;\n\npub struct AppiumManager {\n    pub client: WebDriverClient,\n    appium_process: Option<Child>,\n    pub device_udid: Option<String>,\n}\n\nimpl AppiumManager {\n    pub async fn connect_or_launch(device_udid: Option<&str>) -> Result<Self, String> {\n        let port = APPIUM_DEFAULT_PORT;\n        let client = WebDriverClient::new(port);\n\n        // Check if Appium is already running\n        if is_appium_running(port).await {\n            return Ok(Self {\n                client,\n                appium_process: None,\n                device_udid: device_udid.map(String::from),\n            });\n        }\n\n        // Try to launch Appium\n        let appium_process = launch_appium(port)?;\n\n        // Wait for Appium to be ready\n        wait_for_appium(port, APPIUM_STARTUP_TIMEOUT_SECS).await?;\n\n        Ok(Self {\n            client,\n            appium_process: Some(appium_process),\n            device_udid: device_udid.map(String::from),\n        })\n    }\n\n    pub fn build_ios_capabilities(\n        device_udid: Option<&str>,\n        device_name: Option<&str>,\n        platform_version: Option<&str>,\n    ) -> Value {\n        let mut caps = json!({\n            \"platformName\": \"iOS\",\n            \"appium:automationName\": \"XCUITest\",\n            \"browserName\": \"Safari\",\n            \"appium:noReset\": true,\n        });\n\n        if let Some(name) = device_name {\n            caps[\"appium:deviceName\"] = json!(name);\n        } else {\n            caps[\"appium:deviceName\"] = json!(\"iPhone\");\n        }\n\n        if let Some(ver) = platform_version {\n            caps[\"appium:platformVersion\"] = json!(ver);\n        }\n\n        if let Some(udid) = device_udid {\n            caps[\"appium:udid\"] = json!(udid);\n        }\n\n        caps\n    }\n\n    pub async fn create_ios_session(\n        &mut self,\n        device_name: Option<&str>,\n        platform_version: Option<&str>,\n    ) -> Result<Value, String> {\n        let caps = Self::build_ios_capabilities(\n            self.device_udid.as_deref(),\n            device_name,\n            platform_version,\n        );\n        self.client.create_session(caps).await\n    }\n\n    pub async fn tap(&self, x: f64, y: f64) -> Result<(), String> {\n        let sid = self\n            .client\n            .session_id_pub()\n            .ok_or(\"No active session\")?\n            .to_string();\n        let actions = json!({\n            \"actions\": [{\n                \"type\": \"pointer\",\n                \"id\": \"finger1\",\n                \"parameters\": { \"pointerType\": \"touch\" },\n                \"actions\": [\n                    { \"type\": \"pointerMove\", \"duration\": 0, \"x\": x as i64, \"y\": y as i64 },\n                    { \"type\": \"pointerDown\", \"button\": 0 },\n                    { \"type\": \"pause\", \"duration\": 100 },\n                    { \"type\": \"pointerUp\", \"button\": 0 },\n                ]\n            }]\n        });\n        self.client.execute_actions(&sid, &actions).await\n    }\n\n    pub async fn swipe(\n        &self,\n        start_x: f64,\n        start_y: f64,\n        end_x: f64,\n        end_y: f64,\n        duration_ms: u64,\n    ) -> Result<(), String> {\n        let sid = self\n            .client\n            .session_id_pub()\n            .ok_or(\"No active session\")?\n            .to_string();\n        let actions = json!({\n            \"actions\": [{\n                \"type\": \"pointer\",\n                \"id\": \"finger1\",\n                \"parameters\": { \"pointerType\": \"touch\" },\n                \"actions\": [\n                    { \"type\": \"pointerMove\", \"duration\": 0, \"x\": start_x as i64, \"y\": start_y as i64 },\n                    { \"type\": \"pointerDown\", \"button\": 0 },\n                    { \"type\": \"pointerMove\", \"duration\": duration_ms, \"x\": end_x as i64, \"y\": end_y as i64 },\n                    { \"type\": \"pointerUp\", \"button\": 0 },\n                ]\n            }]\n        });\n        self.client.execute_actions(&sid, &actions).await\n    }\n\n    pub async fn close(&mut self) -> Result<(), String> {\n        let _ = self.client.delete_session().await;\n        if let Some(ref mut child) = self.appium_process {\n            let _ = child.kill();\n            let _ = child.wait();\n        }\n        Ok(())\n    }\n}\n\nimpl Drop for AppiumManager {\n    fn drop(&mut self) {\n        if let Some(ref mut child) = self.appium_process {\n            let _ = child.kill();\n            let _ = child.wait();\n        }\n    }\n}\n\nasync fn is_appium_running(port: u16) -> bool {\n    let addr = format!(\"127.0.0.1:{}\", port);\n    tokio::time::timeout(\n        Duration::from_secs(2),\n        tokio::net::TcpStream::connect(&addr),\n    )\n    .await\n    .map(|r| r.is_ok())\n    .unwrap_or(false)\n}\n\nfn launch_appium(port: u16) -> Result<Child, String> {\n    // Try npx appium first, then direct appium\n    let result = Command::new(\"npx\")\n        .args([\"appium\", \"--relaxed-security\", \"--port\", &port.to_string()])\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .spawn();\n\n    match result {\n        Ok(child) => Ok(child),\n        Err(_) => Command::new(\"appium\")\n            .args([\"--relaxed-security\", \"--port\", &port.to_string()])\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .spawn()\n            .map_err(|e| {\n                format!(\n                    \"Failed to launch Appium. Install it with: npm install -g appium. Error: {}\",\n                    e\n                )\n            }),\n    }\n}\n\nasync fn wait_for_appium(port: u16, timeout_secs: u64) -> Result<(), String> {\n    let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);\n    loop {\n        if tokio::time::Instant::now() > deadline {\n            return Err(\"Timeout waiting for Appium to start\".to_string());\n        }\n        if is_appium_running(port).await {\n            return Ok(());\n        }\n        tokio::time::sleep(Duration::from_millis(500)).await;\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_appium_constants() {\n        assert_eq!(APPIUM_DEFAULT_PORT, 4723);\n        assert_eq!(APPIUM_STARTUP_TIMEOUT_SECS, 30);\n    }\n\n    #[test]\n    fn test_ios_capabilities_use_vendor_prefix() {\n        let caps = AppiumManager::build_ios_capabilities(\n            Some(\"TEST-UDID-123\"),\n            Some(\"iPhone 16 Pro\"),\n            Some(\"18.5\"),\n        );\n\n        // W3C standard capabilities must NOT have vendor prefix\n        assert!(caps.get(\"platformName\").is_some());\n        assert!(caps.get(\"browserName\").is_some());\n\n        // Non-standard capabilities MUST have appium: vendor prefix\n        assert!(caps.get(\"appium:automationName\").is_some());\n        assert!(caps.get(\"appium:noReset\").is_some());\n        assert!(caps.get(\"appium:deviceName\").is_some());\n        assert!(caps.get(\"appium:platformVersion\").is_some());\n        assert!(caps.get(\"appium:udid\").is_some());\n\n        // Must NOT have unprefixed non-standard capabilities\n        assert!(caps.get(\"automationName\").is_none());\n        assert!(caps.get(\"noReset\").is_none());\n        assert!(caps.get(\"deviceName\").is_none());\n        assert!(caps.get(\"udid\").is_none());\n    }\n}\n"
  },
  {
    "path": "cli/src/native/webdriver/backend.rs",
    "content": "use async_trait::async_trait;\nuse serde_json::Value;\n\n/// Abstract backend for browser automation. CDP (Chromium) and WebDriver\n/// (Safari/iOS) share this interface so actions.rs can remain backend-agnostic\n/// in the future.\n#[async_trait]\npub trait BrowserBackend: Send + Sync {\n    async fn navigate(&self, url: &str) -> Result<(), String>;\n    async fn get_url(&self) -> Result<String, String>;\n    async fn get_title(&self) -> Result<String, String>;\n    async fn get_content(&self) -> Result<String, String>;\n    async fn evaluate(&self, script: &str) -> Result<Value, String>;\n    async fn screenshot(&self) -> Result<String, String>;\n    async fn click(&self, selector: &str) -> Result<(), String>;\n    async fn fill(&self, selector: &str, value: &str) -> Result<(), String>;\n    async fn close(&mut self) -> Result<(), String>;\n    async fn back(&self) -> Result<(), String>;\n    async fn forward(&self) -> Result<(), String>;\n    async fn reload(&self) -> Result<(), String>;\n    async fn get_cookies(&self) -> Result<Value, String>;\n    fn backend_type(&self) -> &str;\n\n    fn supports(&self, feature: &str) -> bool {\n        match feature {\n            \"navigate\" | \"evaluate\" | \"screenshot\" | \"click\" | \"fill\" => true,\n            \"screencast\" | \"tracing\" | \"network_intercept\" | \"cdp\" => self.backend_type() == \"cdp\",\n            _ => false,\n        }\n    }\n\n    fn unsupported_error(&self, action: &str) -> String {\n        format!(\n            \"Action '{}' is not supported on the {} backend\",\n            action,\n            self.backend_type()\n        )\n    }\n}\n\n/// WebDriver implementation of BrowserBackend\npub struct WebDriverBackend {\n    client: super::client::WebDriverClient,\n}\n\nimpl WebDriverBackend {\n    pub fn new(client: super::client::WebDriverClient) -> Self {\n        Self { client }\n    }\n}\n\n#[async_trait]\nimpl BrowserBackend for WebDriverBackend {\n    async fn navigate(&self, url: &str) -> Result<(), String> {\n        self.client.navigate(url).await\n    }\n\n    async fn get_url(&self) -> Result<String, String> {\n        self.client.get_url().await\n    }\n\n    async fn get_title(&self) -> Result<String, String> {\n        self.client.get_title().await\n    }\n\n    async fn get_content(&self) -> Result<String, String> {\n        self.client.get_page_source().await\n    }\n\n    async fn evaluate(&self, script: &str) -> Result<Value, String> {\n        self.client.execute_script(script, vec![]).await\n    }\n\n    async fn screenshot(&self) -> Result<String, String> {\n        self.client.screenshot().await\n    }\n\n    async fn click(&self, selector: &str) -> Result<(), String> {\n        let element_id = self.client.find_element(\"css selector\", selector).await?;\n        self.client.click_element(&element_id).await\n    }\n\n    async fn fill(&self, selector: &str, value: &str) -> Result<(), String> {\n        let element_id = self.client.find_element(\"css selector\", selector).await?;\n        self.client.clear_element(&element_id).await?;\n        self.client.send_keys(&element_id, value).await\n    }\n\n    async fn close(&mut self) -> Result<(), String> {\n        self.client.delete_session().await\n    }\n\n    async fn back(&self) -> Result<(), String> {\n        self.client.back().await\n    }\n\n    async fn forward(&self) -> Result<(), String> {\n        self.client.forward().await\n    }\n\n    async fn reload(&self) -> Result<(), String> {\n        self.client.refresh().await\n    }\n\n    async fn get_cookies(&self) -> Result<Value, String> {\n        self.client.get_cookies().await\n    }\n\n    fn backend_type(&self) -> &str {\n        \"webdriver\"\n    }\n}\n\n/// CDP-backed backend constants for unsupported actions on WebDriver\npub const WEBDRIVER_UNSUPPORTED_ACTIONS: &[&str] = &[\n    \"screencast_start\",\n    \"screencast_stop\",\n    \"trace_start\",\n    \"trace_stop\",\n    \"profiler_start\",\n    \"profiler_stop\",\n    \"route\",\n    \"unroute\",\n    \"expose\",\n    \"addscript\",\n    \"addinitscript\",\n    \"network\",\n    \"har_start\",\n    \"har_stop\",\n];\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_unsupported_actions() {\n        assert!(WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&\"screencast_start\"));\n        assert!(WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&\"trace_start\"));\n        assert!(!WEBDRIVER_UNSUPPORTED_ACTIONS.contains(&\"navigate\"));\n    }\n}\n"
  },
  {
    "path": "cli/src/native/webdriver/client.rs",
    "content": "use serde_json::{json, Value};\nuse std::time::Duration;\n\npub struct WebDriverClient {\n    base_url: String,\n    session_id: Option<String>,\n}\n\nimpl WebDriverClient {\n    pub fn new(port: u16) -> Self {\n        Self {\n            base_url: format!(\"http://127.0.0.1:{}\", port),\n            session_id: None,\n        }\n    }\n\n    pub async fn create_session(&mut self, capabilities: Value) -> Result<Value, String> {\n        let body = json!({\n            \"capabilities\": {\n                \"alwaysMatch\": capabilities,\n            }\n        });\n\n        let response = self.post(\"/session\", &body).await?;\n\n        let session_id = response\n            .get(\"value\")\n            .and_then(|v| v.get(\"sessionId\"))\n            .and_then(|v| v.as_str())\n            .ok_or(\"No sessionId in response\")?\n            .to_string();\n\n        self.session_id = Some(session_id);\n        Ok(response)\n    }\n\n    pub async fn delete_session(&mut self) -> Result<(), String> {\n        if let Some(ref sid) = self.session_id.clone() {\n            let _ = self.delete(&format!(\"/session/{}\", sid)).await;\n            self.session_id = None;\n        }\n        Ok(())\n    }\n\n    pub async fn navigate(&self, url: &str) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(&format!(\"/session/{}/url\", sid), &json!({ \"url\": url }))\n            .await?;\n        Ok(())\n    }\n\n    pub async fn get_url(&self) -> Result<String, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self.get(&format!(\"/session/{}/url\", sid)).await?;\n        Ok(response\n            .get(\"value\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .to_string())\n    }\n\n    pub async fn get_title(&self) -> Result<String, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self.get(&format!(\"/session/{}/title\", sid)).await?;\n        Ok(response\n            .get(\"value\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .to_string())\n    }\n\n    pub async fn find_element(&self, using: &str, value: &str) -> Result<String, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self\n            .post(\n                &format!(\"/session/{}/element\", sid),\n                &json!({ \"using\": using, \"value\": value }),\n            )\n            .await?;\n\n        let element_value = response.get(\"value\").ok_or(\"No element in response\")?;\n\n        element_value\n            .get(\"element-6066-11e4-a52e-4f735466cecf\")\n            .or_else(|| element_value.get(\"ELEMENT\"))\n            .and_then(|v| v.as_str())\n            .map(String::from)\n            .ok_or(\"No element ID in response\".to_string())\n    }\n\n    pub async fn click_element(&self, element_id: &str) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(\n            &format!(\"/session/{}/element/{}/click\", sid, element_id),\n            &json!({}),\n        )\n        .await?;\n        Ok(())\n    }\n\n    pub async fn send_keys(&self, element_id: &str, text: &str) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(\n            &format!(\"/session/{}/element/{}/value\", sid, element_id),\n            &json!({ \"text\": text }),\n        )\n        .await?;\n        Ok(())\n    }\n\n    pub async fn clear_element(&self, element_id: &str) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(\n            &format!(\"/session/{}/element/{}/clear\", sid, element_id),\n            &json!({}),\n        )\n        .await?;\n        Ok(())\n    }\n\n    pub async fn execute_script(&self, script: &str, args: Vec<Value>) -> Result<Value, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self\n            .post(\n                &format!(\"/session/{}/execute/sync\", sid),\n                &json!({ \"script\": script, \"args\": args }),\n            )\n            .await?;\n        Ok(response.get(\"value\").cloned().unwrap_or(Value::Null))\n    }\n\n    pub async fn screenshot(&self) -> Result<String, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self.get(&format!(\"/session/{}/screenshot\", sid)).await?;\n        Ok(response\n            .get(\"value\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .to_string())\n    }\n\n    pub async fn get_cookies(&self) -> Result<Value, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self.get(&format!(\"/session/{}/cookie\", sid)).await?;\n        Ok(response.get(\"value\").cloned().unwrap_or(Value::Null))\n    }\n\n    pub async fn get_page_source(&self) -> Result<String, String> {\n        let sid = self.session_id()?.to_string();\n        let response = self.get(&format!(\"/session/{}/source\", sid)).await?;\n        Ok(response\n            .get(\"value\")\n            .and_then(|v| v.as_str())\n            .unwrap_or(\"\")\n            .to_string())\n    }\n\n    pub async fn back(&self) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(&format!(\"/session/{}/back\", sid), &json!({}))\n            .await?;\n        Ok(())\n    }\n\n    pub async fn forward(&self) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(&format!(\"/session/{}/forward\", sid), &json!({}))\n            .await?;\n        Ok(())\n    }\n\n    pub async fn refresh(&self) -> Result<(), String> {\n        let sid = self.session_id()?.to_string();\n        self.post(&format!(\"/session/{}/refresh\", sid), &json!({}))\n            .await?;\n        Ok(())\n    }\n\n    pub fn session_id_pub(&self) -> Option<&str> {\n        self.session_id.as_deref()\n    }\n\n    pub fn new_with_session(port: u16, session_id: String) -> Self {\n        Self {\n            base_url: format!(\"http://127.0.0.1:{}\", port),\n            session_id: Some(session_id),\n        }\n    }\n\n    pub async fn execute_actions(&self, session_id: &str, actions: &Value) -> Result<(), String> {\n        self.post(&format!(\"/session/{}/actions\", session_id), actions)\n            .await?;\n        Ok(())\n    }\n\n    fn session_id(&self) -> Result<&str, String> {\n        self.session_id\n            .as_deref()\n            .ok_or(\"No active WebDriver session\".to_string())\n    }\n\n    async fn get(&self, path: &str) -> Result<Value, String> {\n        http_request(\"GET\", &format!(\"{}{}\", self.base_url, path), None).await\n    }\n\n    async fn post(&self, path: &str, body: &Value) -> Result<Value, String> {\n        http_request(\"POST\", &format!(\"{}{}\", self.base_url, path), Some(body)).await\n    }\n\n    async fn delete(&self, path: &str) -> Result<Value, String> {\n        http_request(\"DELETE\", &format!(\"{}{}\", self.base_url, path), None).await\n    }\n}\n\nasync fn http_request(method: &str, url: &str, body: Option<&Value>) -> Result<Value, String> {\n    let parsed = url::Url::parse(url).map_err(|e| format!(\"Invalid URL: {}\", e))?;\n    let host = parsed.host_str().unwrap_or(\"127.0.0.1\");\n    let port = parsed.port().unwrap_or(80);\n    let path = parsed.path();\n\n    let addr = format!(\"{}:{}\", host, port);\n    let stream = tokio::time::timeout(\n        Duration::from_secs(10),\n        tokio::net::TcpStream::connect(&addr),\n    )\n    .await\n    .map_err(|_| format!(\"Connection timeout: {}\", addr))?\n    .map_err(|e| format!(\"Connection failed: {}\", e))?;\n\n    use tokio::io::{AsyncReadExt, AsyncWriteExt};\n\n    let body_str = body\n        .map(|b| serde_json::to_string(b).unwrap_or_default())\n        .unwrap_or_default();\n\n    let request = if body.is_some() {\n        format!(\n            \"{} {} HTTP/1.1\\r\\nHost: {}\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n            method, path, addr, body_str.len(), body_str\n        )\n    } else {\n        format!(\n            \"{} {} HTTP/1.1\\r\\nHost: {}\\r\\nConnection: close\\r\\n\\r\\n\",\n            method, path, addr\n        )\n    };\n\n    let mut stream = stream;\n    stream\n        .write_all(request.as_bytes())\n        .await\n        .map_err(|e| format!(\"Write failed: {}\", e))?;\n\n    let mut response = Vec::new();\n    stream\n        .read_to_end(&mut response)\n        .await\n        .map_err(|e| format!(\"Read failed: {}\", e))?;\n\n    let response_str = String::from_utf8_lossy(&response);\n    let body_part = response_str.split(\"\\r\\n\\r\\n\").nth(1).unwrap_or(\"\").trim();\n\n    // Handle chunked encoding\n    let json_body = if body_part.contains('\\n')\n        && body_part\n            .chars()\n            .next()\n            .map(|c| c.is_ascii_hexdigit())\n            .unwrap_or(false)\n    {\n        // Chunked: skip chunk size lines\n        body_part\n            .lines()\n            .filter(|l| !l.chars().all(|c| c.is_ascii_hexdigit() || c == '\\r'))\n            .collect::<Vec<&str>>()\n            .join(\"\")\n    } else {\n        body_part.to_string()\n    };\n\n    if json_body.is_empty() {\n        return Ok(json!({}));\n    }\n\n    serde_json::from_str(&json_body).map_err(|e| {\n        format!(\n            \"Invalid JSON response: {} (body: {})\",\n            e,\n            json_body.chars().take(100).collect::<String>()\n        )\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_client_new() {\n        let client = WebDriverClient::new(4444);\n        assert_eq!(client.base_url, \"http://127.0.0.1:4444\");\n        assert!(client.session_id.is_none());\n    }\n\n    #[test]\n    fn test_session_id_none() {\n        let client = WebDriverClient::new(4444);\n        let result = client.session_id();\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"No active WebDriver session\"));\n    }\n\n    #[test]\n    fn test_client_custom_port() {\n        let client = WebDriverClient::new(9515);\n        assert_eq!(client.base_url, \"http://127.0.0.1:9515\");\n    }\n}\n"
  },
  {
    "path": "cli/src/native/webdriver/ios.rs",
    "content": "use serde_json::{json, Value};\nuse std::process::Command;\n\n#[derive(Debug, Clone)]\npub struct IosDevice {\n    pub name: String,\n    pub udid: String,\n    pub state: String,\n    pub runtime: String,\n    pub is_real: bool,\n}\n\npub fn list_simulators() -> Result<Vec<IosDevice>, String> {\n    let output = Command::new(\"xcrun\")\n        .args([\"simctl\", \"list\", \"devices\", \"--json\"])\n        .output()\n        .map_err(|e| format!(\"Failed to run xcrun simctl: {}\", e))?;\n\n    if !output.status.success() {\n        return Err(\"xcrun simctl failed. Xcode may not be installed.\".to_string());\n    }\n\n    let json_str = String::from_utf8_lossy(&output.stdout);\n    let parsed: Value =\n        serde_json::from_str(&json_str).map_err(|e| format!(\"Failed to parse simctl: {}\", e))?;\n\n    let mut devices = Vec::new();\n    if let Some(device_map) = parsed.get(\"devices\").and_then(|v| v.as_object()) {\n        for (runtime, device_list) in device_map {\n            if let Some(arr) = device_list.as_array() {\n                for device in arr {\n                    let name = device\n                        .get(\"name\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\")\n                        .to_string();\n                    let udid = device\n                        .get(\"udid\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\")\n                        .to_string();\n                    let state = device\n                        .get(\"state\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\")\n                        .to_string();\n                    devices.push(IosDevice {\n                        name,\n                        udid,\n                        state,\n                        runtime: runtime.clone(),\n                        is_real: false,\n                    });\n                }\n            }\n        }\n    }\n    Ok(devices)\n}\n\npub fn list_real_devices() -> Result<Vec<IosDevice>, String> {\n    let output = Command::new(\"xcrun\")\n        .args([\"xctrace\", \"list\", \"devices\"])\n        .output()\n        .map_err(|e| format!(\"Failed to run xcrun xctrace: {}\", e))?;\n\n    if !output.status.success() {\n        return Ok(Vec::new());\n    }\n\n    let stdout = String::from_utf8_lossy(&output.stdout);\n    let mut devices = Vec::new();\n    let mut in_devices = false;\n\n    for line in stdout.lines() {\n        let trimmed = line.trim();\n        if trimmed.starts_with(\"== Devices ==\") {\n            in_devices = true;\n            continue;\n        }\n        if trimmed.starts_with(\"== Simulators ==\") {\n            break;\n        }\n        if !in_devices || trimmed.is_empty() {\n            continue;\n        }\n        // Format: \"Device Name (OS Version) (UDID)\"\n        if let Some(udid_start) = trimmed.rfind('(') {\n            let udid_end = trimmed.len() - 1;\n            let udid = &trimmed[udid_start + 1..udid_end];\n            // Validate it looks like a UDID (contains hyphens)\n            if udid.contains('-') && udid.len() > 20 {\n                let name_part = trimmed[..udid_start].trim();\n                let name = if let Some(paren_pos) = name_part.rfind('(') {\n                    name_part[..paren_pos].trim().to_string()\n                } else {\n                    name_part.to_string()\n                };\n                devices.push(IosDevice {\n                    name,\n                    udid: udid.to_string(),\n                    state: \"Connected\".to_string(),\n                    runtime: String::new(),\n                    is_real: true,\n                });\n            }\n        }\n    }\n\n    Ok(devices)\n}\n\npub fn list_all_devices() -> Result<Vec<IosDevice>, String> {\n    let mut all = list_simulators().unwrap_or_default();\n    all.extend(list_real_devices().unwrap_or_default());\n    Ok(all)\n}\n\npub fn boot_simulator(udid: &str) -> Result<(), String> {\n    let output = Command::new(\"xcrun\")\n        .args([\"simctl\", \"boot\", udid])\n        .output()\n        .map_err(|e| format!(\"Failed to boot simulator: {}\", e))?;\n\n    if !output.status.success() {\n        let stderr = String::from_utf8_lossy(&output.stderr);\n        if stderr.contains(\"current state: Booted\") {\n            return Ok(());\n        }\n        return Err(format!(\"Failed to boot simulator {}: {}\", udid, stderr));\n    }\n    Ok(())\n}\n\npub fn shutdown_simulator(udid: &str) -> Result<(), String> {\n    let output = Command::new(\"xcrun\")\n        .args([\"simctl\", \"shutdown\", udid])\n        .output()\n        .map_err(|e| format!(\"Failed to shutdown simulator: {}\", e))?;\n\n    if !output.status.success() {\n        let stderr = String::from_utf8_lossy(&output.stderr);\n        if stderr.contains(\"current state: Shutdown\") {\n            return Ok(());\n        }\n        return Err(format!(\"Failed to shutdown simulator {}: {}\", udid, stderr));\n    }\n    Ok(())\n}\n\npub fn select_device(device_name: Option<&str>, udid: Option<&str>) -> Result<IosDevice, String> {\n    if let Some(u) = udid {\n        let devices = list_all_devices()?;\n        return devices\n            .into_iter()\n            .find(|d| d.udid == u)\n            .ok_or_else(|| format!(\"Device with UDID '{}' not found\", u));\n    }\n\n    if let Some(name) = device_name {\n        let devices = list_all_devices()?;\n        return devices\n            .into_iter()\n            .find(|d| d.name.to_lowercase().contains(&name.to_lowercase()))\n            .ok_or_else(|| format!(\"Device '{}' not found\", name));\n    }\n\n    // Default: prefer most recent iPhone, prefer Pro\n    let devices = list_simulators()?;\n    let iphone_devices: Vec<&IosDevice> = devices\n        .iter()\n        .filter(|d| d.name.starts_with(\"iPhone\"))\n        .collect();\n\n    if iphone_devices.is_empty() {\n        return devices\n            .into_iter()\n            .next()\n            .ok_or(\"No iOS simulators found\".to_string());\n    }\n\n    // Prefer Pro models\n    if let Some(pro) = iphone_devices.iter().find(|d| d.name.contains(\"Pro\")) {\n        return Ok((*pro).clone());\n    }\n\n    Ok((*iphone_devices.last().unwrap()).clone())\n}\n\npub fn to_device_json(devices: &[IosDevice]) -> Value {\n    let list: Vec<Value> = devices\n        .iter()\n        .map(|d| {\n            json!({\n                \"name\": d.name,\n                \"udid\": d.udid,\n                \"state\": d.state,\n                \"runtime\": d.runtime,\n                \"isReal\": d.is_real,\n            })\n        })\n        .collect();\n    json!({ \"devices\": list })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_ios_device_struct() {\n        let device = IosDevice {\n            name: \"iPhone 15 Pro\".to_string(),\n            udid: \"ABC-123\".to_string(),\n            state: \"Booted\".to_string(),\n            runtime: \"iOS-17-0\".to_string(),\n            is_real: false,\n        };\n        assert_eq!(device.name, \"iPhone 15 Pro\");\n        assert!(!device.is_real);\n    }\n\n    #[test]\n    fn test_to_device_json() {\n        let devices = vec![IosDevice {\n            name: \"Test\".to_string(),\n            udid: \"123\".to_string(),\n            state: \"Shutdown\".to_string(),\n            runtime: \"iOS-17\".to_string(),\n            is_real: false,\n        }];\n        let json = to_device_json(&devices);\n        assert!(json.get(\"devices\").unwrap().as_array().unwrap().len() == 1);\n    }\n}\n"
  },
  {
    "path": "cli/src/native/webdriver/mod.rs",
    "content": "pub mod appium;\npub mod backend;\npub mod client;\npub mod ios;\npub mod safari;\npub mod types;\n"
  },
  {
    "path": "cli/src/native/webdriver/safari.rs",
    "content": "use std::path::PathBuf;\nuse std::process::{Child, Command, Stdio};\nuse std::time::Duration;\n\npub struct SafariDriverProcess {\n    child: Child,\n    pub port: u16,\n}\n\nimpl SafariDriverProcess {\n    pub fn kill(&mut self) {\n        let _ = self.child.kill();\n        let _ = self.child.wait();\n    }\n}\n\nimpl Drop for SafariDriverProcess {\n    fn drop(&mut self) {\n        self.kill();\n    }\n}\n\npub fn find_safaridriver() -> Option<PathBuf> {\n    let candidates = [\"/usr/bin/safaridriver\"];\n\n    for c in &candidates {\n        let p = PathBuf::from(c);\n        if p.exists() {\n            return Some(p);\n        }\n    }\n\n    // Try PATH\n    if let Ok(output) = Command::new(\"which\").arg(\"safaridriver\").output() {\n        if output.status.success() {\n            let path = String::from_utf8_lossy(&output.stdout).trim().to_string();\n            if !path.is_empty() {\n                return Some(PathBuf::from(path));\n            }\n        }\n    }\n\n    None\n}\n\npub fn launch_safaridriver(port: u16) -> Result<SafariDriverProcess, String> {\n    let driver_path = find_safaridriver()\n        .ok_or(\"safaridriver not found. Safari WebDriver requires macOS with Safari.\")?;\n\n    let child = Command::new(&driver_path)\n        .arg(\"--port\")\n        .arg(port.to_string())\n        .stdin(Stdio::null())\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .spawn()\n        .map_err(|e| format!(\"Failed to launch safaridriver: {}\", e))?;\n\n    // Wait for driver to be ready\n    std::thread::sleep(Duration::from_millis(500));\n\n    Ok(SafariDriverProcess { child, port })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_find_safaridriver() {\n        // Only check on macOS\n        if cfg!(target_os = \"macos\") {\n            let result = find_safaridriver();\n            // Don't assert Some since it may not be enabled\n            if let Some(path) = result {\n                assert!(path.exists());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/native/webdriver/types.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewSessionRequest {\n    pub capabilities: Capabilities,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Capabilities {\n    pub always_match: Value,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SessionResponse {\n    pub value: SessionValue,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SessionValue {\n    pub session_id: String,\n    pub capabilities: Value,\n}\n\n#[derive(Debug, Deserialize)]\npub struct WebDriverResponse {\n    pub value: Value,\n}\n\n#[derive(Debug, Deserialize)]\npub struct WebDriverError {\n    pub error: String,\n    pub message: String,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ElementResponse {\n    pub value: ElementValue,\n}\n\n#[derive(Debug, Deserialize)]\npub struct ElementValue {\n    #[serde(rename = \"element-6066-11e4-a52e-4f735466cecf\")]\n    pub element_id: Option<String>,\n    #[serde(rename = \"ELEMENT\")]\n    pub element_legacy: Option<String>,\n}\n\nimpl ElementValue {\n    pub fn id(&self) -> Option<&str> {\n        self.element_id\n            .as_deref()\n            .or(self.element_legacy.as_deref())\n    }\n}\n\n#[derive(Debug, Serialize)]\npub struct FindElementRequest {\n    pub using: String,\n    pub value: String,\n}\n\n#[derive(Debug, Serialize)]\npub struct ExecuteScriptRequest {\n    pub script: String,\n    pub args: Vec<Value>,\n}\n\n#[derive(Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CookieRequest {\n    pub cookie: CookieData,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CookieData {\n    pub name: String,\n    pub value: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub domain: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub path: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub secure: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub http_only: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub expiry: Option<u64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub same_site: Option<String>,\n}\n"
  },
  {
    "path": "cli/src/output.rs",
    "content": "use std::sync::OnceLock;\n\nuse crate::color;\nuse crate::connection::Response;\n\nstatic BOUNDARY_NONCE: OnceLock<String> = OnceLock::new();\n\n/// Per-process nonce for content boundary markers. Uses a CSPRNG (getrandom) so\n/// that untrusted page content cannot predict or spoof the boundary delimiter.\n/// Process ID or timestamps would be insufficient since pages can read those.\nfn get_boundary_nonce() -> &'static str {\n    BOUNDARY_NONCE.get_or_init(|| {\n        let mut buf = [0u8; 16];\n        getrandom::getrandom(&mut buf).expect(\"failed to generate random nonce\");\n        buf.iter().map(|b| format!(\"{:02x}\", b)).collect()\n    })\n}\n\n#[derive(Default)]\npub struct OutputOptions {\n    pub json: bool,\n    pub content_boundaries: bool,\n    pub max_output: Option<usize>,\n}\n\nfn truncate_if_needed(content: &str, max: Option<usize>) -> String {\n    let Some(limit) = max else {\n        return content.to_string();\n    };\n    // Fast path: byte length is a lower bound on char count, so if the\n    // byte length is within the limit the char count must be too.\n    if content.len() <= limit {\n        return content.to_string();\n    }\n    // Find the byte offset of the limit-th character.\n    match content.char_indices().nth(limit).map(|(i, _)| i) {\n        Some(byte_offset) => {\n            let total_chars = content.chars().count();\n            format!(\n                \"{}\\n[truncated: showing {} of {} chars. Use --max-output to adjust]\",\n                &content[..byte_offset],\n                limit,\n                total_chars\n            )\n        }\n        // Content has fewer than `limit` chars despite more bytes\n        None => content.to_string(),\n    }\n}\n\nfn print_with_boundaries(content: &str, origin: Option<&str>, opts: &OutputOptions) {\n    let content = truncate_if_needed(content, opts.max_output);\n    if opts.content_boundaries {\n        let origin_str = origin.unwrap_or(\"unknown\");\n        let nonce = get_boundary_nonce();\n        println!(\n            \"--- AGENT_BROWSER_PAGE_CONTENT nonce={} origin={} ---\",\n            nonce, origin_str\n        );\n        println!(\"{}\", content);\n        println!(\"--- END_AGENT_BROWSER_PAGE_CONTENT nonce={} ---\", nonce);\n    } else {\n        println!(\"{}\", content);\n    }\n}\n\nfn format_storage_value(value: &serde_json::Value) -> String {\n    value\n        .as_str()\n        .map(ToString::to_string)\n        .unwrap_or_else(|| serde_json::to_string(value).unwrap_or_default())\n}\n\nfn format_storage_text(data: &serde_json::Value) -> Option<String> {\n    if let Some(entries) = data.get(\"data\").and_then(|v| v.as_object()) {\n        if entries.is_empty() {\n            return Some(\"No storage entries\".to_string());\n        }\n\n        let lines = entries\n            .iter()\n            .map(|(key, value)| format!(\"{}: {}\", key, format_storage_value(value)))\n            .collect::<Vec<_>>();\n        return Some(lines.join(\"\\n\"));\n    }\n\n    let key = data.get(\"key\").and_then(|v| v.as_str())?;\n    let value = data.get(\"value\")?;\n    Some(format!(\"{}: {}\", key, format_storage_value(value)))\n}\n\npub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &OutputOptions) {\n    if opts.json {\n        if opts.content_boundaries {\n            let mut json_val = serde_json::to_value(resp).unwrap_or_default();\n            if let Some(obj) = json_val.as_object_mut() {\n                let nonce = get_boundary_nonce();\n                let origin = obj\n                    .get(\"data\")\n                    .and_then(|d| d.get(\"origin\"))\n                    .and_then(|v| v.as_str())\n                    .unwrap_or(\"unknown\");\n                obj.insert(\n                    \"_boundary\".to_string(),\n                    serde_json::json!({\n                        \"nonce\": nonce,\n                        \"origin\": origin,\n                    }),\n                );\n            }\n            println!(\"{}\", serde_json::to_string(&json_val).unwrap_or_default());\n        } else {\n            println!(\"{}\", serde_json::to_string(resp).unwrap_or_default());\n        }\n        return;\n    }\n\n    if !resp.success {\n        eprintln!(\n            \"{} {}\",\n            color::error_indicator(),\n            resp.error.as_deref().unwrap_or(\"Unknown error\")\n        );\n        return;\n    }\n\n    if let Some(data) = &resp.data {\n        if action == Some(\"storage_get\") {\n            if let Some(output) = format_storage_text(data) {\n                println!(\"{}\", output);\n                return;\n            }\n        }\n        // Inspect response (check before generic URL handler since it also has a \"url\" field)\n        if action == Some(\"inspect\") {\n            let opened = data\n                .get(\"opened\")\n                .and_then(|v| v.as_bool())\n                .unwrap_or(false);\n            if opened {\n                if let Some(url) = data.get(\"url\").and_then(|v| v.as_str()) {\n                    println!(\"{} Opened DevTools: {}\", color::success_indicator(), url);\n                } else {\n                    println!(\"{} Opened DevTools\", color::success_indicator());\n                }\n            } else if let Some(err) = data.get(\"error\").and_then(|v| v.as_str()) {\n                eprintln!(\"Could not open DevTools: {}\", err);\n            }\n            return;\n        }\n        // Navigation response\n        if let Some(url) = data.get(\"url\").and_then(|v| v.as_str()) {\n            if let Some(title) = data.get(\"title\").and_then(|v| v.as_str()) {\n                println!(\"{} {}\", color::success_indicator(), color::bold(title));\n                println!(\"  {}\", color::dim(url));\n                return;\n            }\n            println!(\"{}\", url);\n            return;\n        }\n        if let Some(cdp_url) = data.get(\"cdpUrl\").and_then(|v| v.as_str()) {\n            println!(\"{}\", cdp_url);\n            return;\n        }\n        // Diff responses -- route by action to avoid fragile shape probing\n        if let Some(obj) = data.as_object() {\n            match action {\n                Some(\"diff_snapshot\") => {\n                    print_snapshot_diff(obj);\n                    return;\n                }\n                Some(\"diff_screenshot\") => {\n                    print_screenshot_diff(obj);\n                    return;\n                }\n                Some(\"diff_url\") => {\n                    if let Some(snap_data) = obj.get(\"snapshot\").and_then(|v| v.as_object()) {\n                        println!(\"{}\", color::bold(\"Snapshot diff:\"));\n                        print_snapshot_diff(snap_data);\n                    }\n                    if let Some(ss_data) = obj.get(\"screenshot\").and_then(|v| v.as_object()) {\n                        println!(\"\\n{}\", color::bold(\"Screenshot diff:\"));\n                        print_screenshot_diff(ss_data);\n                    }\n                    return;\n                }\n                _ => {}\n            }\n        }\n        let origin = data.get(\"origin\").and_then(|v| v.as_str());\n        // Snapshot\n        if let Some(snapshot) = data.get(\"snapshot\").and_then(|v| v.as_str()) {\n            print_with_boundaries(snapshot, origin, opts);\n            return;\n        }\n        // Title\n        if let Some(title) = data.get(\"title\").and_then(|v| v.as_str()) {\n            println!(\"{}\", title);\n            return;\n        }\n        // Text\n        if let Some(text) = data.get(\"text\").and_then(|v| v.as_str()) {\n            print_with_boundaries(text, origin, opts);\n            return;\n        }\n        // HTML\n        if let Some(html) = data.get(\"html\").and_then(|v| v.as_str()) {\n            print_with_boundaries(html, origin, opts);\n            return;\n        }\n        // Value\n        if let Some(value) = data.get(\"value\").and_then(|v| v.as_str()) {\n            println!(\"{}\", value);\n            return;\n        }\n        // Count\n        if let Some(count) = data.get(\"count\").and_then(|v| v.as_i64()) {\n            println!(\"{}\", count);\n            return;\n        }\n        // Boolean results\n        if let Some(visible) = data.get(\"visible\").and_then(|v| v.as_bool()) {\n            println!(\"{}\", visible);\n            return;\n        }\n        if let Some(enabled) = data.get(\"enabled\").and_then(|v| v.as_bool()) {\n            println!(\"{}\", enabled);\n            return;\n        }\n        if let Some(checked) = data.get(\"checked\").and_then(|v| v.as_bool()) {\n            println!(\"{}\", checked);\n            return;\n        }\n        // Eval result\n        if let Some(result) = data.get(\"result\") {\n            let formatted = serde_json::to_string_pretty(result).unwrap_or_default();\n            print_with_boundaries(&formatted, origin, opts);\n            return;\n        }\n        // iOS Devices\n        if let Some(devices) = data.get(\"devices\").and_then(|v| v.as_array()) {\n            if devices.is_empty() {\n                println!(\"No iOS devices available. Open Xcode to download simulator runtimes.\");\n                return;\n            }\n\n            // Separate real devices from simulators\n            let real_devices: Vec<_> = devices\n                .iter()\n                .filter(|d| {\n                    d.get(\"isRealDevice\")\n                        .and_then(|v| v.as_bool())\n                        .unwrap_or(false)\n                })\n                .collect();\n            let simulators: Vec<_> = devices\n                .iter()\n                .filter(|d| {\n                    !d.get(\"isRealDevice\")\n                        .and_then(|v| v.as_bool())\n                        .unwrap_or(false)\n                })\n                .collect();\n\n            if !real_devices.is_empty() {\n                println!(\"Connected Devices:\\n\");\n                for device in real_devices.iter() {\n                    let name = device\n                        .get(\"name\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"Unknown\");\n                    let runtime = device.get(\"runtime\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let udid = device.get(\"udid\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    println!(\"  {} {} ({})\", color::green(\"●\"), name, runtime);\n                    println!(\"    {}\", color::dim(udid));\n                }\n                println!();\n            }\n\n            if !simulators.is_empty() {\n                println!(\"Simulators:\\n\");\n                for device in simulators.iter() {\n                    let name = device\n                        .get(\"name\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"Unknown\");\n                    let runtime = device.get(\"runtime\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let state = device\n                        .get(\"state\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"Unknown\");\n                    let udid = device.get(\"udid\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let state_indicator = if state == \"Booted\" {\n                        color::green(\"●\")\n                    } else {\n                        color::dim(\"○\")\n                    };\n                    println!(\"  {} {} ({})\", state_indicator, name, runtime);\n                    println!(\"    {}\", color::dim(udid));\n                }\n            }\n            return;\n        }\n        // Tabs\n        if let Some(tabs) = data.get(\"tabs\").and_then(|v| v.as_array()) {\n            for (i, tab) in tabs.iter().enumerate() {\n                let title = tab\n                    .get(\"title\")\n                    .and_then(|v| v.as_str())\n                    .unwrap_or(\"Untitled\");\n                let url = tab.get(\"url\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                let active = tab.get(\"active\").and_then(|v| v.as_bool()).unwrap_or(false);\n                let marker = if active {\n                    color::cyan(\"→\")\n                } else {\n                    \" \".to_string()\n                };\n                println!(\"{} [{}] {} - {}\", marker, i, title, url);\n            }\n            return;\n        }\n        // Console logs\n        if let Some(logs) = data.get(\"messages\").and_then(|v| v.as_array()) {\n            if opts.content_boundaries {\n                let mut console_output = String::new();\n                for log in logs {\n                    let level = log.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"log\");\n                    let text = log.get(\"text\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    console_output.push_str(&format!(\n                        \"{} {}\\n\",\n                        color::console_level_prefix(level),\n                        text\n                    ));\n                }\n                if console_output.ends_with('\\n') {\n                    console_output.pop();\n                }\n                print_with_boundaries(&console_output, origin, opts);\n            } else {\n                for log in logs {\n                    let level = log.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"log\");\n                    let text = log.get(\"text\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    println!(\"{} {}\", color::console_level_prefix(level), text);\n                }\n            }\n            return;\n        }\n        // Errors\n        if let Some(errors) = data.get(\"errors\").and_then(|v| v.as_array()) {\n            for err in errors {\n                let msg = err.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                println!(\"{} {}\", color::error_indicator(), msg);\n            }\n            return;\n        }\n        // Cookies\n        if let Some(cookies) = data.get(\"cookies\").and_then(|v| v.as_array()) {\n            for cookie in cookies {\n                let name = cookie.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                let value = cookie.get(\"value\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                println!(\"{}={}\", name, value);\n            }\n            return;\n        }\n        // Network requests\n        if let Some(requests) = data.get(\"requests\").and_then(|v| v.as_array()) {\n            if requests.is_empty() {\n                println!(\"No requests captured\");\n            } else {\n                for req in requests {\n                    let method = req.get(\"method\").and_then(|v| v.as_str()).unwrap_or(\"GET\");\n                    let url = req.get(\"url\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let resource_type = req\n                        .get(\"resourceType\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n                    println!(\"{} {} ({})\", method, url, resource_type);\n                }\n            }\n            return;\n        }\n        // Cleared (cookies or request log)\n        if let Some(cleared) = data.get(\"cleared\").and_then(|v| v.as_bool()) {\n            if cleared {\n                let label = match action {\n                    Some(\"cookies_clear\") => \"Cookies cleared\",\n                    _ => \"Request log cleared\",\n                };\n                println!(\"{} {}\", color::success_indicator(), label);\n                return;\n            }\n        }\n        // Bounding box\n        if let Some(box_data) = data.get(\"box\") {\n            println!(\n                \"{}\",\n                serde_json::to_string_pretty(box_data).unwrap_or_default()\n            );\n            return;\n        }\n        // Element styles\n        if let Some(elements) = data.get(\"elements\").and_then(|v| v.as_array()) {\n            for (i, el) in elements.iter().enumerate() {\n                let tag = el.get(\"tag\").and_then(|v| v.as_str()).unwrap_or(\"?\");\n                let text = el.get(\"text\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                println!(\"[{}] {} \\\"{}\\\"\", i, tag, text);\n\n                if let Some(box_data) = el.get(\"box\") {\n                    let w = box_data.get(\"width\").and_then(|v| v.as_i64()).unwrap_or(0);\n                    let h = box_data.get(\"height\").and_then(|v| v.as_i64()).unwrap_or(0);\n                    let x = box_data.get(\"x\").and_then(|v| v.as_i64()).unwrap_or(0);\n                    let y = box_data.get(\"y\").and_then(|v| v.as_i64()).unwrap_or(0);\n                    println!(\"    box: {}x{} at ({}, {})\", w, h, x, y);\n                }\n\n                if let Some(styles) = el.get(\"styles\") {\n                    let font_size = styles\n                        .get(\"fontSize\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n                    let font_weight = styles\n                        .get(\"fontWeight\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n                    let font_family = styles\n                        .get(\"fontFamily\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n                    let color = styles.get(\"color\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let bg = styles\n                        .get(\"backgroundColor\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n                    let radius = styles\n                        .get(\"borderRadius\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n\n                    println!(\"    font: {} {} {}\", font_size, font_weight, font_family);\n                    println!(\"    color: {}\", color);\n                    println!(\"    background: {}\", bg);\n                    if radius != \"0px\" {\n                        println!(\"    border-radius: {}\", radius);\n                    }\n                }\n                println!();\n            }\n            return;\n        }\n        // Closed (browser or tab)\n        if data.get(\"closed\").is_some() {\n            let label = match action {\n                Some(\"tab_close\") => \"Tab closed\",\n                _ => \"Browser closed\",\n            };\n            println!(\"{} {}\", color::success_indicator(), label);\n            return;\n        }\n        // Started actions (profiling, HAR, recording)\n        if let Some(started) = data.get(\"started\").and_then(|v| v.as_bool()) {\n            if started {\n                match action {\n                    Some(\"profiler_start\") => {\n                        println!(\"{} Profiling started\", color::success_indicator());\n                    }\n                    Some(\"har_start\") => {\n                        println!(\"{} HAR recording started\", color::success_indicator());\n                    }\n                    _ => {\n                        if let Some(path) = data.get(\"path\").and_then(|v| v.as_str()) {\n                            println!(\"{} Recording started: {}\", color::success_indicator(), path);\n                        } else {\n                            println!(\"{} Recording started\", color::success_indicator());\n                        }\n                    }\n                }\n                return;\n            }\n        }\n        // Recording restart (has \"stopped\" field - from recording_restart action)\n        if data.get(\"stopped\").is_some() {\n            let path = data\n                .get(\"path\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"unknown\");\n            if let Some(prev_path) = data.get(\"previousPath\").and_then(|v| v.as_str()) {\n                println!(\n                    \"{} Recording restarted: {} (previous saved to {})\",\n                    color::success_indicator(),\n                    path,\n                    prev_path\n                );\n            } else {\n                println!(\"{} Recording started: {}\", color::success_indicator(), path);\n            }\n            return;\n        }\n        // Recording stop (has \"frames\" field - from recording_stop action)\n        if data.get(\"frames\").is_some() {\n            if let Some(path) = data.get(\"path\").and_then(|v| v.as_str()) {\n                if let Some(error) = data.get(\"error\").and_then(|v| v.as_str()) {\n                    println!(\n                        \"{} Recording saved to {} - {}\",\n                        color::warning_indicator(),\n                        path,\n                        error\n                    );\n                } else {\n                    println!(\"{} Recording saved to {}\", color::success_indicator(), path);\n                }\n            } else {\n                println!(\"{} Recording stopped\", color::success_indicator());\n            }\n            return;\n        }\n        // Download response (has \"suggestedFilename\" or \"filename\" field)\n        if data.get(\"suggestedFilename\").is_some() || data.get(\"filename\").is_some() {\n            if let Some(path) = data.get(\"path\").and_then(|v| v.as_str()) {\n                let filename = data\n                    .get(\"suggestedFilename\")\n                    .or_else(|| data.get(\"filename\"))\n                    .and_then(|v| v.as_str())\n                    .unwrap_or(\"\");\n                if filename.is_empty() {\n                    println!(\n                        \"{} Downloaded to {}\",\n                        color::success_indicator(),\n                        color::green(path)\n                    );\n                } else {\n                    println!(\n                        \"{} Downloaded to {} ({})\",\n                        color::success_indicator(),\n                        color::green(path),\n                        filename\n                    );\n                }\n                return;\n            }\n        }\n        // Trace stop without path\n        if data.get(\"traceStopped\").is_some() {\n            println!(\"{} Trace stopped\", color::success_indicator());\n            return;\n        }\n        // Path-based operations (screenshot/pdf/trace/har/download/state/video)\n        if let Some(path) = data.get(\"path\").and_then(|v| v.as_str()) {\n            match action.unwrap_or(\"\") {\n                \"screenshot\" => {\n                    println!(\n                        \"{} Screenshot saved to {}\",\n                        color::success_indicator(),\n                        color::green(path)\n                    );\n                    if let Some(annotations) = data.get(\"annotations\").and_then(|v| v.as_array()) {\n                        for ann in annotations {\n                            let num = ann.get(\"number\").and_then(|n| n.as_u64()).unwrap_or(0);\n                            let ref_id = ann.get(\"ref\").and_then(|r| r.as_str()).unwrap_or(\"\");\n                            let role = ann.get(\"role\").and_then(|r| r.as_str()).unwrap_or(\"\");\n                            let name = ann.get(\"name\").and_then(|n| n.as_str()).unwrap_or(\"\");\n                            if name.is_empty() {\n                                println!(\n                                    \"   {} @{} {}\",\n                                    color::dim(&format!(\"[{}]\", num)),\n                                    ref_id,\n                                    role,\n                                );\n                            } else {\n                                println!(\n                                    \"   {} @{} {} {:?}\",\n                                    color::dim(&format!(\"[{}]\", num)),\n                                    ref_id,\n                                    role,\n                                    name,\n                                );\n                            }\n                        }\n                    }\n                }\n                \"pdf\" => println!(\n                    \"{} PDF saved to {}\",\n                    color::success_indicator(),\n                    color::green(path)\n                ),\n                \"trace_stop\" => println!(\n                    \"{} Trace saved to {}\",\n                    color::success_indicator(),\n                    color::green(path)\n                ),\n                \"profiler_stop\" => println!(\n                    \"{} Profile saved to {} ({} events)\",\n                    color::success_indicator(),\n                    color::green(path),\n                    data.get(\"eventCount\").and_then(|c| c.as_u64()).unwrap_or(0)\n                ),\n                \"har_stop\" => println!(\n                    \"{} HAR saved to {} ({} requests)\",\n                    color::success_indicator(),\n                    color::green(path),\n                    data.get(\"requestCount\")\n                        .and_then(|c| c.as_u64())\n                        .unwrap_or(0)\n                ),\n                \"download\" | \"waitfordownload\" => println!(\n                    \"{} Download saved to {}\",\n                    color::success_indicator(),\n                    color::green(path)\n                ),\n                \"video_stop\" => println!(\n                    \"{} Video saved to {}\",\n                    color::success_indicator(),\n                    color::green(path)\n                ),\n                \"state_save\" => println!(\n                    \"{} State saved to {}\",\n                    color::success_indicator(),\n                    color::green(path)\n                ),\n                \"state_load\" => {\n                    if let Some(note) = data.get(\"note\").and_then(|v| v.as_str()) {\n                        println!(\"{}\", note);\n                    }\n                    println!(\n                        \"{} State path set to {}\",\n                        color::success_indicator(),\n                        color::green(path)\n                    );\n                }\n                // video_start and other commands that provide a path with a note\n                \"video_start\" => {\n                    if let Some(note) = data.get(\"note\").and_then(|v| v.as_str()) {\n                        println!(\"{}\", note);\n                    }\n                    println!(\"Path: {}\", path);\n                }\n                _ => println!(\n                    \"{} Saved to {}\",\n                    color::success_indicator(),\n                    color::green(path)\n                ),\n            }\n            return;\n        }\n\n        // State list\n        if let Some(files) = data.get(\"files\").and_then(|v| v.as_array()) {\n            if let Some(dir) = data.get(\"directory\").and_then(|v| v.as_str()) {\n                println!(\"{}\", color::bold(&format!(\"Saved states in {}\", dir)));\n            }\n            if files.is_empty() {\n                println!(\"{}\", color::dim(\"  No state files found\"));\n            } else {\n                for file in files {\n                    let filename = file.get(\"filename\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let size = file.get(\"size\").and_then(|v| v.as_i64()).unwrap_or(0);\n                    let modified = file.get(\"modified\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let encrypted = file\n                        .get(\"encrypted\")\n                        .and_then(|v| v.as_bool())\n                        .unwrap_or(false);\n                    let size_str = if size > 1024 {\n                        format!(\"{:.1}KB\", size as f64 / 1024.0)\n                    } else {\n                        format!(\"{}B\", size)\n                    };\n                    let date_str = modified.split('T').next().unwrap_or(modified);\n                    let enc_str = if encrypted { \" [encrypted]\" } else { \"\" };\n                    println!(\n                        \"  {} {}\",\n                        filename,\n                        color::dim(&format!(\"({}, {}){}\", size_str, date_str, enc_str))\n                    );\n                }\n            }\n            return;\n        }\n\n        // State rename\n        if let Some(true) = data.get(\"renamed\").and_then(|v| v.as_bool()) {\n            let old_name = data.get(\"oldName\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            let new_name = data.get(\"newName\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            println!(\n                \"{} Renamed {} -> {}\",\n                color::success_indicator(),\n                old_name,\n                new_name\n            );\n            return;\n        }\n\n        // State clear\n        if let Some(cleared) = data.get(\"cleared\").and_then(|v| v.as_i64()) {\n            println!(\n                \"{} Cleared {} state file(s)\",\n                color::success_indicator(),\n                cleared\n            );\n            return;\n        }\n\n        // State show summary\n        if let Some(summary) = data.get(\"summary\") {\n            let cookies = summary.get(\"cookies\").and_then(|v| v.as_i64()).unwrap_or(0);\n            let origins = summary.get(\"origins\").and_then(|v| v.as_i64()).unwrap_or(0);\n            let encrypted = data\n                .get(\"encrypted\")\n                .and_then(|v| v.as_bool())\n                .unwrap_or(false);\n            let enc_str = if encrypted { \" (encrypted)\" } else { \"\" };\n            println!(\"State file summary{}:\", enc_str);\n            println!(\"  Cookies: {}\", cookies);\n            println!(\"  Origins with localStorage: {}\", origins);\n            return;\n        }\n\n        // State clean\n        if let Some(cleaned) = data.get(\"cleaned\").and_then(|v| v.as_i64()) {\n            println!(\n                \"{} Cleaned {} old state file(s)\",\n                color::success_indicator(),\n                cleaned\n            );\n            return;\n        }\n\n        // Informational note\n        if let Some(note) = data.get(\"note\").and_then(|v| v.as_str()) {\n            println!(\"{}\", note);\n            return;\n        }\n        // Auth list\n        if let Some(profiles) = data.get(\"profiles\").and_then(|v| v.as_array()) {\n            if profiles.is_empty() {\n                println!(\"{}\", color::dim(\"No auth profiles saved\"));\n            } else {\n                println!(\"{}\", color::bold(\"Auth profiles:\"));\n                for p in profiles {\n                    let name = p.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let url = p.get(\"url\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    let user = p.get(\"username\").and_then(|v| v.as_str()).unwrap_or(\"\");\n                    println!(\n                        \"  {} {} {}\",\n                        color::green(name),\n                        color::dim(user),\n                        color::dim(url)\n                    );\n                }\n            }\n            return;\n        }\n\n        // Auth show\n        if let Some(profile) = data.get(\"profile\").and_then(|v| v.as_object()) {\n            let name = profile.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            let url = profile.get(\"url\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            let user = profile\n                .get(\"username\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"\");\n            let created = profile\n                .get(\"createdAt\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"\");\n            let last_login = profile.get(\"lastLoginAt\").and_then(|v| v.as_str());\n            println!(\"Name: {}\", name);\n            println!(\"URL: {}\", url);\n            println!(\"Username: {}\", user);\n            println!(\"Created: {}\", created);\n            if let Some(ll) = last_login {\n                println!(\"Last login: {}\", ll);\n            }\n            return;\n        }\n\n        // Auth save/update/login/delete\n        if data.get(\"saved\").and_then(|v| v.as_bool()).unwrap_or(false) {\n            let name = data.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            println!(\n                \"{} Auth profile '{}' saved\",\n                color::success_indicator(),\n                name\n            );\n            return;\n        }\n        if data\n            .get(\"updated\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n            && !data.get(\"saved\").and_then(|v| v.as_bool()).unwrap_or(false)\n        {\n            let name = data.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            println!(\n                \"{} Auth profile '{}' updated\",\n                color::success_indicator(),\n                name\n            );\n            return;\n        }\n        if data\n            .get(\"loggedIn\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            let name = data.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            if let Some(title) = data.get(\"title\").and_then(|v| v.as_str()) {\n                println!(\n                    \"{} Logged in as '{}' - {}\",\n                    color::success_indicator(),\n                    name,\n                    title\n                );\n            } else {\n                println!(\"{} Logged in as '{}'\", color::success_indicator(), name);\n            }\n            return;\n        }\n        if data\n            .get(\"deleted\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            if let Some(name) = data.get(\"name\").and_then(|v| v.as_str()) {\n                println!(\n                    \"{} Auth profile '{}' deleted\",\n                    color::success_indicator(),\n                    name\n                );\n                return;\n            }\n        }\n\n        // Confirmation required (for orchestrator use)\n        if data\n            .get(\"confirmation_required\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            let category = data.get(\"category\").and_then(|v| v.as_str()).unwrap_or(\"\");\n            let description = data\n                .get(\"description\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"\");\n            let cid = data\n                .get(\"confirmation_id\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"\");\n            println!(\"Confirmation required:\");\n            println!(\"  {}: {}\", category, description);\n            println!(\"  Run: agent-browser confirm {}\", cid);\n            println!(\"  Or:  agent-browser deny {}\", cid);\n            return;\n        }\n        if data\n            .get(\"confirmed\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            println!(\"{} Action confirmed\", color::success_indicator());\n            return;\n        }\n        if data\n            .get(\"denied\")\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false)\n        {\n            println!(\"{} Action denied\", color::success_indicator());\n            return;\n        }\n\n        // Default success\n        println!(\"{} Done\", color::success_indicator());\n    }\n}\n\n/// Print command-specific help. Returns true if help was printed, false if command unknown.\npub fn print_command_help(command: &str) -> bool {\n    let help = match command {\n        // === Navigation ===\n        \"open\" | \"goto\" | \"navigate\" => {\n            r##\"\nagent-browser open - Navigate to a URL\n\nUsage: agent-browser open <url>\n\nNavigates the browser to the specified URL. If no protocol is provided,\nhttps:// is automatically prepended.\n\nAliases: goto, navigate\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n  --headers <json>     Set HTTP headers (scoped to this origin)\n  --headed             Show browser window\n\nExamples:\n  agent-browser open example.com\n  agent-browser open https://github.com\n  agent-browser open localhost:3000\n  agent-browser open api.example.com --headers '{\"Authorization\": \"Bearer token\"}'\n    # ^ Headers only sent to api.example.com, not other domains\n\"##\n        }\n        \"back\" => {\n            r##\"\nagent-browser back - Navigate back in history\n\nUsage: agent-browser back\n\nGoes back one page in the browser history, equivalent to clicking\nthe browser's back button.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser back\n\"##\n        }\n        \"forward\" => {\n            r##\"\nagent-browser forward - Navigate forward in history\n\nUsage: agent-browser forward\n\nGoes forward one page in the browser history, equivalent to clicking\nthe browser's forward button.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser forward\n\"##\n        }\n        \"reload\" => {\n            r##\"\nagent-browser reload - Reload the current page\n\nUsage: agent-browser reload\n\nReloads the current page, equivalent to pressing F5 or clicking\nthe browser's reload button.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser reload\n\"##\n        }\n\n        // === Core Actions ===\n        \"click\" => {\n            r##\"\nagent-browser click - Click an element\n\nUsage: agent-browser click <selector> [--new-tab]\n\nClicks on the specified element. The selector can be a CSS selector,\nXPath, or an element reference from snapshot (e.g., @e1).\n\nOptions:\n  --new-tab            Open link in a new tab instead of navigating current tab\n                       (only works on elements with href attribute)\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser click \"#submit-button\"\n  agent-browser click @e1\n  agent-browser click \"button.primary\"\n  agent-browser click \"//button[@type='submit']\"\n  agent-browser click @e3 --new-tab\n\"##\n        }\n        \"dblclick\" => {\n            r##\"\nagent-browser dblclick - Double-click an element\n\nUsage: agent-browser dblclick <selector>\n\nDouble-clicks on the specified element. Useful for text selection\nor triggering double-click handlers.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser dblclick \"#editable-text\"\n  agent-browser dblclick @e5\n\"##\n        }\n        \"fill\" => {\n            r##\"\nagent-browser fill - Clear and fill an input field\n\nUsage: agent-browser fill <selector> <text>\n\nClears the input field and fills it with the specified text.\nThis replaces any existing content in the field.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser fill \"#email\" \"user@example.com\"\n  agent-browser fill @e3 \"Hello World\"\n  agent-browser fill \"input[name='search']\" \"query\"\n\"##\n        }\n        \"type\" => {\n            r##\"\nagent-browser type - Type text into an element\n\nUsage: agent-browser type <selector> <text>\n\nTypes text into the specified element character by character.\nUnlike fill, this does not clear existing content first.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser type \"#search\" \"hello\"\n  agent-browser type @e2 \"additional text\"\n\nSee Also:\n  For typing into contenteditable editors (Lexical, ProseMirror, etc.)\n  without a selector, use 'keyboard type' instead:\n    agent-browser keyboard type \"# My Heading\"\n\"##\n        }\n        \"hover\" => {\n            r##\"\nagent-browser hover - Hover over an element\n\nUsage: agent-browser hover <selector>\n\nMoves the mouse to hover over the specified element. Useful for\ntriggering hover states or dropdown menus.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser hover \"#dropdown-trigger\"\n  agent-browser hover @e4\n\"##\n        }\n        \"focus\" => {\n            r##\"\nagent-browser focus - Focus an element\n\nUsage: agent-browser focus <selector>\n\nSets keyboard focus to the specified element.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser focus \"#input-field\"\n  agent-browser focus @e2\n\"##\n        }\n        \"check\" => {\n            r##\"\nagent-browser check - Check a checkbox\n\nUsage: agent-browser check <selector>\n\nChecks a checkbox element. If already checked, no action is taken.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser check \"#terms-checkbox\"\n  agent-browser check @e7\n\"##\n        }\n        \"uncheck\" => {\n            r##\"\nagent-browser uncheck - Uncheck a checkbox\n\nUsage: agent-browser uncheck <selector>\n\nUnchecks a checkbox element. If already unchecked, no action is taken.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser uncheck \"#newsletter-opt-in\"\n  agent-browser uncheck @e8\n\"##\n        }\n        \"select\" => {\n            r##\"\nagent-browser select - Select a dropdown option\n\nUsage: agent-browser select <selector> <value...>\n\nSelects one or more options in a <select> dropdown by value.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser select \"#country\" \"US\"\n  agent-browser select @e5 \"option2\"\n  agent-browser select \"#menu\" \"opt1\" \"opt2\" \"opt3\"\n\"##\n        }\n        \"drag\" => {\n            r##\"\nagent-browser drag - Drag and drop\n\nUsage: agent-browser drag <source> <target>\n\nDrags an element from source to target location.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser drag \"#draggable\" \"#drop-zone\"\n  agent-browser drag @e1 @e2\n\"##\n        }\n        \"upload\" => {\n            r##\"\nagent-browser upload - Upload files\n\nUsage: agent-browser upload <selector> <files...>\n\nUploads one or more files to a file input element.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser upload \"#file-input\" ./document.pdf\n  agent-browser upload @e3 ./image1.png ./image2.png\n\"##\n        }\n        \"download\" => {\n            r##\"\nagent-browser download - Download a file by clicking an element\n\nUsage: agent-browser download <selector> <path>\n\nClicks an element that triggers a download and saves the file to the specified path.\n\nArguments:\n  selector             Element to click (CSS selector or @ref)\n  path                 Path where the downloaded file will be saved\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser download \"#download-btn\" ./file.pdf\n  agent-browser download @e5 ./report.xlsx\n  agent-browser download \"a[href$='.zip']\" ./archive.zip\n\"##\n        }\n\n        // === Keyboard ===\n        \"press\" | \"key\" => {\n            r##\"\nagent-browser press - Press a key or key combination\n\nUsage: agent-browser press <key>\n\nPresses a key or key combination. Supports special keys and modifiers.\n\nAliases: key\n\nSpecial Keys:\n  Enter, Tab, Escape, Backspace, Delete, Space\n  ArrowUp, ArrowDown, ArrowLeft, ArrowRight\n  Home, End, PageUp, PageDown\n  F1-F12\n\nModifiers (combine with +):\n  Control, Alt, Shift, Meta\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser press Enter\n  agent-browser press Tab\n  agent-browser press Control+a\n  agent-browser press Control+Shift+s\n  agent-browser press Escape\n\"##\n        }\n        \"keydown\" => {\n            r##\"\nagent-browser keydown - Press a key down (without release)\n\nUsage: agent-browser keydown <key>\n\nPresses a key down without releasing it. Use keyup to release.\nUseful for holding modifier keys.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser keydown Shift\n  agent-browser keydown Control\n\"##\n        }\n        \"keyup\" => {\n            r##\"\nagent-browser keyup - Release a key\n\nUsage: agent-browser keyup <key>\n\nReleases a key that was pressed with keydown.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser keyup Shift\n  agent-browser keyup Control\n\"##\n        }\n        \"keyboard\" => {\n            r##\"\nagent-browser keyboard - Raw keyboard input (no selector needed)\n\nUsage: agent-browser keyboard <subcommand> <text>\n\nSends keyboard input to whatever element currently has focus.\nUnlike 'type' which requires a selector, 'keyboard' operates on\nthe current focus — essential for contenteditable editors like\nLexical, ProseMirror, CodeMirror, and Monaco.\n\nSubcommands:\n  type <text>          Type text character-by-character with real\n                       key events (keydown, keypress, keyup per char)\n  inserttext <text>    Insert text without key events (like paste)\n\nNote: For key combos (Enter, Control+a), use the 'press' command\ndirectly — it already operates on the current focus.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser keyboard type \"Hello, World!\"\n  agent-browser keyboard type \"# My Heading\"\n  agent-browser keyboard inserttext \"pasted content\"\n\nUse Cases:\n  # Type into a Lexical/ProseMirror contenteditable editor:\n  agent-browser click \"[contenteditable]\"\n  agent-browser keyboard type \"# My Heading\"\n  agent-browser press Enter\n  agent-browser keyboard type \"Some paragraph text\"\n\"##\n        }\n\n        // === Scroll ===\n        \"scroll\" => {\n            r##\"\nagent-browser scroll - Scroll the page\n\nUsage: agent-browser scroll [direction] [amount] [options]\n\nScrolls the page or a specific element in the specified direction.\n\nArguments:\n  direction            up, down, left, right (default: down)\n  amount               Pixels to scroll (default: 300)\n\nOptions:\n  -s, --selector <sel> CSS selector for a scrollable container\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser scroll\n  agent-browser scroll down 500\n  agent-browser scroll up 200\n  agent-browser scroll left 100\n  agent-browser scroll down 500 --selector \"div.scroll-container\"\n\"##\n        }\n        \"scrollintoview\" | \"scrollinto\" => {\n            r##\"\nagent-browser scrollintoview - Scroll element into view\n\nUsage: agent-browser scrollintoview <selector>\n\nScrolls the page until the specified element is visible in the viewport.\n\nAliases: scrollinto\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser scrollintoview \"#footer\"\n  agent-browser scrollintoview @e15\n\"##\n        }\n\n        // === Wait ===\n        \"wait\" => {\n            r##\"\nagent-browser wait - Wait for condition\n\nUsage: agent-browser wait <selector|ms|option>\n\nWaits for an element to appear, a timeout, or other conditions.\n\nModes:\n  <selector>           Wait for element to appear\n  <ms>                 Wait for specified milliseconds\n  --url <pattern>      Wait for URL to match pattern\n  --load <state>       Wait for load state (load, domcontentloaded, networkidle)\n  --fn <expression>    Wait for JavaScript expression to be truthy\n  --text <text>        Wait for text to appear on page (substring match)\n  --download [path]    Wait for a download to complete (optionally save to path)\n\nDownload Options (with --download):\n  --timeout <ms>       Timeout in milliseconds for download to start\n\nWait for text to disappear:\n  Use --fn or --state hidden to wait for text or elements to go away:\n  wait --fn \"!document.body.innerText.includes('Loading...')\"\n  wait \"#spinner\" --state hidden\n  wait @e5 --state detached\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser wait \"#loading-spinner\"\n  agent-browser wait 2000\n  agent-browser wait --url \"**/dashboard\"\n  agent-browser wait --load networkidle\n  agent-browser wait --fn \"window.appReady === true\"\n  agent-browser wait --text \"Welcome back\"\n  agent-browser wait --download ./file.pdf\n  agent-browser wait --download ./report.xlsx --timeout 30000\n  agent-browser wait --fn \"!document.body.innerText.includes('Loading...')\"\n\"##\n        }\n\n        // === Screenshot/PDF ===\n        \"screenshot\" => {\n            r##\"\nagent-browser screenshot - Take a screenshot\n\nUsage: agent-browser screenshot [selector] [path]\n\nCaptures a screenshot of the current page. If no path is provided,\nsaves to a temporary directory with a generated filename.\n\nOptions:\n  --full, -f           Capture full page (not just viewport)\n  --annotate           Overlay numbered labels on interactive elements.\n                       Each label [N] corresponds to ref @eN from snapshot.\n                       Prints a legend mapping labels to element roles/names.\n                       With --json, annotations are included in the response.\n                       Supported on Chromium and Lightpanda.\n  --screenshot-dir <path>  Default output directory for screenshots\n                       (or AGENT_BROWSER_SCREENSHOT_DIR env)\n  --screenshot-quality <0-100>  JPEG quality (0-100, only applies to jpeg format)\n                       (or AGENT_BROWSER_SCREENSHOT_QUALITY env)\n  --screenshot-format <fmt>  Image format: png (default) or jpeg\n                       (or AGENT_BROWSER_SCREENSHOT_FORMAT env)\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser screenshot\n  agent-browser screenshot ./screenshot.png\n  agent-browser screenshot --full ./full-page.png\n  agent-browser screenshot --annotate              # Labeled screenshot + legend\n  agent-browser screenshot --annotate ./page.png   # Save annotated screenshot\n  agent-browser screenshot --annotate --json       # JSON output with annotations\n  agent-browser screenshot --screenshot-dir ./shots # Save to custom directory\n  agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80\n\"##\n        }\n        \"pdf\" => {\n            r##\"\nagent-browser pdf - Save page as PDF\n\nUsage: agent-browser pdf <path>\n\nSaves the current page as a PDF file.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser pdf ./page.pdf\n  agent-browser pdf ~/Documents/report.pdf\n\"##\n        }\n\n        // === Snapshot ===\n        \"snapshot\" => {\n            r##\"\nagent-browser snapshot - Get accessibility tree snapshot\n\nUsage: agent-browser snapshot [options]\n\nReturns an accessibility tree representation of the page with element\nreferences (like @e1, @e2) that can be used in subsequent commands.\nDesigned for AI agents to understand page structure.\n\nOptions:\n  -i, --interactive    Only include interactive elements\n  -C, --cursor         Include cursor-interactive elements (cursor:pointer, onclick, tabindex)\n  -c, --compact        Remove empty structural elements\n  -d, --depth <n>      Limit tree depth\n  -s, --selector <sel> Scope snapshot to CSS selector\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser snapshot\n  agent-browser snapshot -i\n  agent-browser snapshot -i -C         # Interactive + cursor-interactive elements\n  agent-browser snapshot --compact --depth 5\n  agent-browser snapshot -s \"#main-content\"\n\"##\n        }\n\n        // === Eval ===\n        \"eval\" => {\n            r##\"\nagent-browser eval - Execute JavaScript\n\nUsage: agent-browser eval [options] <script>\n\nExecutes JavaScript code in the browser context and returns the result.\n\nOptions:\n  -b, --base64         Decode script from base64 (avoids shell escaping issues)\n  --stdin              Read script from stdin (useful for heredocs/multiline)\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser eval \"document.title\"\n  agent-browser eval \"window.location.href\"\n  agent-browser eval \"document.querySelectorAll('a').length\"\n  agent-browser eval -b \"ZG9jdW1lbnQudGl0bGU=\"\n\n  # Read from stdin with heredoc\n  cat <<'EOF' | agent-browser eval --stdin\n  const links = document.querySelectorAll('a');\n  links.length;\n  EOF\n\"##\n        }\n\n        // === Close ===\n        \"close\" | \"quit\" | \"exit\" => {\n            r##\"\nagent-browser close - Close the browser\n\nUsage: agent-browser close\n\nCloses the browser instance for the current session.\n\nAliases: quit, exit\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser close\n  agent-browser close --session mysession\n\"##\n        }\n\n        // === Inspect ===\n        \"inspect\" => {\n            r##\"\nagent-browser inspect - Open Chrome DevTools for the active page\n\nStarts a local WebSocket proxy and opens Chrome's DevTools frontend in your\ndefault browser. The proxy routes DevTools traffic through the daemon's\nexisting CDP connection, so both DevTools and agent-browser commands work\nsimultaneously.\n\nUsage: agent-browser inspect\n\nExamples:\n  agent-browser open example.com\n  agent-browser inspect          # opens DevTools in your browser\n  agent-browser click \"Submit\"   # commands still work while DevTools is open\n\"##\n        }\n\n        // === Get ===\n        \"get\" => {\n            r##\"\nagent-browser get - Retrieve information from elements or page\n\nUsage: agent-browser get <subcommand> [args]\n\nRetrieves various types of information from elements or the page.\n\nSubcommands:\n  text <selector>            Get text content of element\n  html <selector>            Get inner HTML of element\n  value <selector>           Get value of input element\n  attr <selector> <name>     Get attribute value\n  title                      Get page title\n  url                        Get current URL\n  count <selector>           Count matching elements\n  box <selector>             Get bounding box (x, y, width, height)\n  styles <selector>          Get computed styles of elements\n  cdp-url                    Get Chrome DevTools Protocol WebSocket URL\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser get text @e1\n  agent-browser get html \"#content\"\n  agent-browser get value \"#email-input\"\n  agent-browser get attr \"#link\" href\n  agent-browser get title\n  agent-browser get url\n  agent-browser get count \"li.item\"\n  agent-browser get box \"#header\"\n  agent-browser get styles \"button\"\n  agent-browser get styles @e1\n\"##\n        }\n\n        // === Is ===\n        \"is\" => {\n            r##\"\nagent-browser is - Check element state\n\nUsage: agent-browser is <subcommand> <selector>\n\nChecks the state of an element and returns true/false.\n\nSubcommands:\n  visible <selector>   Check if element is visible\n  enabled <selector>   Check if element is enabled (not disabled)\n  checked <selector>   Check if checkbox/radio is checked\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser is visible \"#modal\"\n  agent-browser is enabled \"#submit-btn\"\n  agent-browser is checked \"#agree-checkbox\"\n\"##\n        }\n\n        // === Find ===\n        \"find\" => {\n            r##\"\nagent-browser find - Find and interact with elements by locator\n\nUsage: agent-browser find <locator> <value> [action] [text]\n\nFinds elements using semantic locators and optionally performs an action.\n\nLocators:\n  role <role>              Find by ARIA role (--name <n>, --exact)\n  text <text>              Find by text content (--exact)\n  label <label>            Find by associated label (--exact)\n  placeholder <text>       Find by placeholder text (--exact)\n  alt <text>               Find by alt text (--exact)\n  title <text>             Find by title attribute (--exact)\n  testid <id>              Find by data-testid attribute\n  first <selector>         First matching element\n  last <selector>          Last matching element\n  nth <index> <selector>   Nth matching element (0-based)\n\nActions (default: click):\n  click, fill, type, hover, focus, check, uncheck\n\nOptions:\n  --name <name>        Filter role by accessible name\n  --exact              Require exact text match\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser find role button click --name Submit\n  agent-browser find text \"Sign In\" click\n  agent-browser find label \"Email\" fill \"user@example.com\"\n  agent-browser find placeholder \"Search...\" type \"query\"\n  agent-browser find testid \"login-form\" click\n  agent-browser find first \"li.item\" click\n  agent-browser find nth 2 \".card\" hover\n\"##\n        }\n\n        // === Mouse ===\n        \"mouse\" => {\n            r##\"\nagent-browser mouse - Low-level mouse operations\n\nUsage: agent-browser mouse <subcommand> [args]\n\nPerforms low-level mouse operations for precise control.\n\nSubcommands:\n  move <x> <y>         Move mouse to coordinates\n  down [button]        Press mouse button (left, right, middle)\n  up [button]          Release mouse button\n  wheel <dy> [dx]      Scroll mouse wheel\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser mouse move 100 200\n  agent-browser mouse down\n  agent-browser mouse up\n  agent-browser mouse down right\n  agent-browser mouse wheel 100\n  agent-browser mouse wheel -50 0\n\"##\n        }\n\n        // === Set ===\n        \"set\" => {\n            r##\"\nagent-browser set - Configure browser settings\n\nUsage: agent-browser set <setting> [args]\n\nConfigures various browser settings and emulation options.\n\nSettings:\n  viewport <w> <h> [scale]   Set viewport size (scale = deviceScaleFactor, e.g. 2 for retina)\n  device <name>              Emulate device (e.g., \"iPhone 12\")\n  geo <lat> <lng>            Set geolocation\n  offline [on|off]           Toggle offline mode\n  headers <json>             Set extra HTTP headers\n  credentials <user> <pass>  Set HTTP authentication\n  media [dark|light]         Set color scheme preference\n        [reduced-motion]     Enable reduced motion\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser set viewport 1920 1080\n  agent-browser set viewport 1920 1080 2    # 2x retina\n  agent-browser set device \"iPhone 12\"\n  agent-browser set geo 37.7749 -122.4194\n  agent-browser set offline on\n  agent-browser set headers '{\"X-Custom\": \"value\"}'\n  agent-browser set credentials admin secret123\n  agent-browser set media dark\n  agent-browser set media light reduced-motion\n\"##\n        }\n\n        // === Network ===\n        \"network\" => {\n            r##\"\nagent-browser network - Network interception and monitoring\n\nUsage: agent-browser network <subcommand> [args]\n\nIntercept, mock, or monitor network requests.\n\nSubcommands:\n  route <url> [options]      Intercept requests matching URL pattern\n    --abort                  Abort matching requests\n    --body <json>            Respond with custom body\n  unroute [url]              Remove route (all if no URL)\n  requests [options]         List captured requests\n    --clear                  Clear request log\n    --filter <pattern>       Filter by URL pattern\n  har <start|stop> [path]    Record and export a HAR file\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser network route \"**/api/*\" --abort\n  agent-browser network route \"**/data.json\" --body '{\"mock\": true}'\n  agent-browser network unroute\n  agent-browser network requests\n  agent-browser network requests --filter \"api\"\n  agent-browser network requests --clear\n  agent-browser network har start\n  agent-browser network har stop ./capture.har\n\"##\n        }\n\n        // === Storage ===\n        \"storage\" => {\n            r##\"\nagent-browser storage - Manage web storage\n\nUsage: agent-browser storage <type> [operation] [key] [value]\n\nManage localStorage and sessionStorage.\n\nTypes:\n  local                localStorage\n  session              sessionStorage\n\nOperations:\n  get [key]            Get all storage or specific key\n  set <key> <value>    Set a key-value pair\n  clear                Clear all storage\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser storage local\n  agent-browser storage local get authToken\n  agent-browser storage local set theme \"dark\"\n  agent-browser storage local clear\n  agent-browser storage session get userId\n\"##\n        }\n\n        // === Cookies ===\n        \"cookies\" => {\n            r##\"\nagent-browser cookies - Manage browser cookies\n\nUsage: agent-browser cookies [operation] [args]\n\nManage browser cookies for the current context.\n\nOperations:\n  get                                Get all cookies (default)\n  set <name> <value> [options]       Set a cookie with optional properties\n  clear                              Clear all cookies\n\nCookie Set Options:\n  --url <url>                        URL for the cookie (allows setting before page load)\n  --domain <domain>                  Cookie domain (e.g., \".example.com\")\n  --path <path>                      Cookie path (e.g., \"/api\")\n  --httpOnly                         Set HttpOnly flag (prevents JavaScript access)\n  --secure                           Set Secure flag (HTTPS only)\n  --sameSite <Strict|Lax|None>       SameSite policy\n  --expires <timestamp>              Expiration time (Unix timestamp in seconds)\n\nNote: If --url, --domain, and --path are all omitted, the cookie will be set\nfor the current page URL.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  # Simple cookie for current page\n  agent-browser cookies set session_id \"abc123\"\n\n  # Set cookie for a URL before loading it (useful for authentication)\n  agent-browser cookies set session_id \"abc123\" --url https://app.example.com\n\n  # Set secure, httpOnly cookie with domain and path\n  agent-browser cookies set auth_token \"xyz789\" --domain example.com --path /api --httpOnly --secure\n\n  # Set cookie with SameSite policy\n  agent-browser cookies set tracking_consent \"yes\" --sameSite Strict\n\n  # Set cookie with expiration (Unix timestamp)\n  agent-browser cookies set temp_token \"temp123\" --expires 1735689600\n\n  # Get all cookies\n  agent-browser cookies\n\n  # Clear all cookies\n  agent-browser cookies clear\n\"##\n        }\n\n        // === Tabs ===\n        \"tab\" => {\n            r##\"\nagent-browser tab - Manage browser tabs\n\nUsage: agent-browser tab [operation] [args]\n\nManage browser tabs in the current window.\n\nOperations:\n  list                 List all tabs (default)\n  new [url]            Open new tab\n  close [index]        Close tab (current if no index)\n  <index>              Switch to tab by index\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser tab\n  agent-browser tab list\n  agent-browser tab new\n  agent-browser tab new https://example.com\n  agent-browser tab 2\n  agent-browser tab close\n  agent-browser tab close 1\n\"##\n        }\n\n        // === Window ===\n        \"window\" => {\n            r##\"\nagent-browser window - Manage browser windows\n\nUsage: agent-browser window <operation>\n\nManage browser windows.\n\nOperations:\n  new                  Open new browser window\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser window new\n\"##\n        }\n\n        // === Frame ===\n        \"frame\" => {\n            r##\"\nagent-browser frame - Switch frame context\n\nUsage: agent-browser frame <selector|main>\n\nSwitch to an iframe or back to the main frame.\n\nArguments:\n  <selector>           CSS selector for iframe\n  main                 Switch back to main frame\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser frame \"#embed-iframe\"\n  agent-browser frame \"iframe[name='content']\"\n  agent-browser frame main\n\"##\n        }\n\n        // === Auth ===\n        \"auth\" => {\n            r##\"\nagent-browser auth - Manage authentication profiles\n\nUsage: agent-browser auth <subcommand> [args]\n\nSubcommands:\n  save <name>              Save credentials for a login profile\n  login <name>             Login using saved credentials\n  list                     List saved profiles (names and URLs only)\n  show <name>              Show profile metadata (no passwords)\n  delete <name>            Delete a saved profile\n\nSave Options:\n  --url <url>              Login page URL (required)\n  --username <user>        Username (required)\n  --password <pass>        Password (required unless --password-stdin)\n  --password-stdin          Read password from stdin (recommended)\n  --username-selector <s>  Custom CSS selector for username field\n  --password-selector <s>  Custom CSS selector for password field\n  --submit-selector <s>    Custom CSS selector for submit button\n\nGlobal Options:\n  --json                   Output as JSON\n  --session <name>         Use specific session\n\nExamples:\n  echo \"pass\" | agent-browser auth save github --url https://github.com/login --username user --password-stdin\n  agent-browser auth save github --url https://github.com/login --username user --password pass\n  agent-browser auth login github\n  agent-browser auth list\n  agent-browser auth show github\n  agent-browser auth delete github\n\"##\n        }\n\n        // === Confirm/Deny ===\n        \"confirm\" | \"deny\" => {\n            r##\"\nagent-browser confirm/deny - Approve or deny pending actions\n\nUsage:\n  agent-browser confirm <confirmation-id>\n  agent-browser deny <confirmation-id>\n\nWhen --confirm-actions is set, certain action categories return a\nconfirmation_required response with a confirmation ID. Use confirm/deny\nto approve or reject the action.\n\nPending confirmations auto-deny after 60 seconds.\n\nExamples:\n  agent-browser confirm c_8f3a1234\n  agent-browser deny c_8f3a1234\n\"##\n        }\n\n        // === Dialog ===\n        \"dialog\" => {\n            r##\"\nagent-browser dialog - Handle browser dialogs\n\nUsage: agent-browser dialog <response> [text]\n\nRespond to browser dialogs (alert, confirm, prompt).\n\nOperations:\n  accept [text]        Accept dialog, optionally with prompt text\n  dismiss              Dismiss/cancel dialog\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser dialog accept\n  agent-browser dialog accept \"my input\"\n  agent-browser dialog dismiss\n\"##\n        }\n\n        // === Trace ===\n        \"trace\" => {\n            r##\"\nagent-browser trace - Record execution trace\n\nUsage: agent-browser trace <operation> [path]\n\nRecord a Chrome DevTools trace for debugging.\n\nOperations:\n  start [path]         Start recording trace\n  stop [path]          Stop recording and save trace\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser trace start\n  agent-browser trace start ./my-trace\n  agent-browser trace stop\n  agent-browser trace stop ./debug-trace.zip\n\"##\n        }\n\n        // === Profile (CDP Tracing) ===\n        \"profiler\" => {\n            r##\"\nagent-browser profiler - Record Chrome DevTools performance profile\n\nUsage: agent-browser profiler <operation> [options]\n\nRecord a performance profile using Chrome DevTools Protocol (CDP) Tracing.\nThe output JSON file can be loaded into Chrome DevTools Performance panel,\nPerfetto UI (https://ui.perfetto.dev/), or other trace analysis tools.\n\nOperations:\n  start                Start profiling\n  stop [path]          Stop profiling and save to file\n\nStart Options:\n  --categories <list>  Comma-separated trace categories (default includes\n                       devtools.timeline, v8.execute, blink, and others)\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  # Basic profiling\n  agent-browser profiler start\n  agent-browser navigate https://example.com\n  agent-browser click \"#button\"\n  agent-browser profiler stop ./trace.json\n\n  # With custom categories\n  agent-browser profiler start --categories \"devtools.timeline,v8.execute,blink.user_timing\"\n  agent-browser profiler stop ./custom-trace.json\n\nThe output file can be viewed in:\n  - Chrome DevTools: Performance panel > Load profile\n  - Perfetto: https://ui.perfetto.dev/\n\"##\n        }\n\n        // === Record (video) ===\n        \"record\" => {\n            r##\"\nagent-browser record - Record browser session to video\n\nUsage: agent-browser record start <path.webm> [url]\n       agent-browser record stop\n       agent-browser record restart <path.webm> [url]\n\nRecord the browser to a WebM video file.\nCreates a fresh browser context but preserves cookies and localStorage.\nIf no URL is provided, automatically navigates to your current page.\n\nOperations:\n  start <path> [url]     Start recording (defaults to current URL if omitted)\n  stop                   Stop recording and save video\n  restart <path> [url]   Stop current recording (if any) and start a new one\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  # Record from current page (preserves login state)\n  agent-browser open https://app.example.com/dashboard\n  agent-browser snapshot -i            # Explore and plan\n  agent-browser record start ./demo.webm\n  agent-browser click @e3              # Execute planned actions\n  agent-browser record stop\n\n  # Or specify a different URL\n  agent-browser record start ./demo.webm https://example.com\n\n  # Restart recording with a new file (stops previous, starts new)\n  agent-browser record restart ./take2.webm\n\"##\n        }\n\n        // === Console/Errors ===\n        \"console\" => {\n            r##\"\nagent-browser console - View console logs\n\nUsage: agent-browser console [--clear]\n\nView browser console output (log, warn, error, info).\n\nOptions:\n  --clear              Clear console log buffer\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser console\n  agent-browser console --clear\n\"##\n        }\n        \"errors\" => {\n            r##\"\nagent-browser errors - View page errors\n\nUsage: agent-browser errors [--clear]\n\nView JavaScript errors and uncaught exceptions.\n\nOptions:\n  --clear              Clear error buffer\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser errors\n  agent-browser errors --clear\n\"##\n        }\n\n        // === Highlight ===\n        \"highlight\" => {\n            r##\"\nagent-browser highlight - Highlight an element\n\nUsage: agent-browser highlight <selector>\n\nVisually highlights an element on the page for debugging.\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser highlight \"#target-element\"\n  agent-browser highlight @e5\n\"##\n        }\n\n        // === Clipboard ===\n        \"clipboard\" => {\n            r##\"\nagent-browser clipboard - Read and write clipboard\n\nUsage: agent-browser clipboard <operation> [text]\n\nRead from or write to the browser clipboard.\n\nOperations:\n  read                 Read text from clipboard\n  write <text>         Write text to clipboard\n  copy                 Copy current selection (simulates Ctrl+C)\n  paste                Paste from clipboard (simulates Ctrl+V)\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser clipboard read\n  agent-browser clipboard write \"Hello, World!\"\n  agent-browser clipboard copy\n  agent-browser clipboard paste\n\"##\n        }\n\n        // === State ===\n        \"state\" => {\n            r##\"\nagent-browser state - Manage browser state\n\nUsage: agent-browser state <operation> [args]\n\nSave, restore, list, and manage browser state (cookies, localStorage, sessionStorage).\n\nOperations:\n  save <path>                        Save current state to file\n  load <path>                        Load state from file\n  list                               List saved state files\n  show <filename>                    Show state summary\n  rename <old-name> <new-name>       Rename state file\n  clear [session-name] [--all]       Clear saved states\n  clean --older-than <days>          Delete expired state files\n\nAutomatic State Persistence:\n  Use --session-name to auto-save/restore state across restarts:\n  agent-browser --session-name myapp open https://example.com\n  Or set AGENT_BROWSER_SESSION_NAME environment variable.\n\nState Encryption:\n  Set AGENT_BROWSER_ENCRYPTION_KEY (64-char hex) for AES-256-GCM encryption.\n  Generate a key: openssl rand -hex 32\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser state save ./auth-state.json\n  agent-browser state load ./auth-state.json\n  agent-browser state list\n  agent-browser state show myapp-default.json\n  agent-browser state rename old-name new-name\n  agent-browser state clear --all\n  agent-browser state clean --older-than 7\n\"##\n        }\n\n        // === Session ===\n        \"session\" => {\n            r##\"\nagent-browser session - Manage sessions\n\nUsage: agent-browser session [operation]\n\nManage isolated browser sessions. Each session has its own browser\ninstance with separate cookies, storage, and state.\n\nOperations:\n  (none)               Show current session name\n  list                 List all active sessions\n\nEnvironment:\n  AGENT_BROWSER_SESSION    Default session name\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser session\n  agent-browser session list\n  agent-browser --session test open example.com\n\"##\n        }\n\n        // === Install ===\n        \"install\" => {\n            r##\"\nagent-browser install - Install browser binaries\n\nUsage: agent-browser install [--with-deps]\n\nDownloads and installs browser binaries required for automation.\n\nOptions:\n  -d, --with-deps      Also install system dependencies (Linux only)\n\nExamples:\n  agent-browser install\n  agent-browser install --with-deps\n\"##\n        }\n\n        // === Upgrade ===\n        \"upgrade\" => {\n            r##\"\nagent-browser upgrade - Upgrade to the latest version\n\nUsage: agent-browser upgrade\n\nDetects the current installation method (npm, Homebrew, or Cargo) and runs\nthe appropriate update command. Displays the version change on success, or\ninforms you if you are already on the latest version.\n\nExamples:\n  agent-browser upgrade\n\"##\n        }\n\n        // === Connect ===\n        \"connect\" => {\n            r##\"\nagent-browser connect - Connect to browser via CDP\n\nUsage: agent-browser connect <port|url>\n\nConnects to a running browser instance via Chrome DevTools Protocol (CDP).\nThis allows controlling browsers, Electron apps, or remote browser services.\n\nArguments:\n  <port>               Local port number (e.g., 9222)\n  <url>                Full WebSocket URL (ws://, wss://, http://, https://)\n\nSupported URL formats:\n  - Port number: 9222 (connects to http://localhost:9222)\n  - WebSocket URL: ws://localhost:9222/devtools/browser/...\n  - Remote service: wss://remote-browser.example.com/cdp?token=...\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  # Connect to local Chrome with remote debugging\n  # Start Chrome: google-chrome --remote-debugging-port=9222\n  agent-browser connect 9222\n\n  # Connect using WebSocket URL from /json/version endpoint\n  agent-browser connect \"ws://localhost:9222/devtools/browser/abc123\"\n\n  # Connect to remote browser service\n  agent-browser connect \"wss://browser-service.example.com/cdp?token=xyz\"\n\n  # After connecting, run commands normally\n  agent-browser snapshot\n  agent-browser click @e1\n\"##\n        }\n\n        // === iOS Commands ===\n        \"tap\" => {\n            r##\"\nagent-browser tap - Tap an element (touch gesture)\n\nUsage: agent-browser tap <selector>\n\nTaps an element. This is an alias for 'click' that provides semantic clarity\nfor touch-based interfaces like iOS Safari.\n\nOptions:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser tap \"#submit-button\"\n  agent-browser tap @e1\n  agent-browser -p ios tap \"button:has-text('Sign In')\"\n\"##\n        }\n        \"swipe\" => {\n            r##\"\nagent-browser swipe - Swipe gesture (iOS)\n\nUsage: agent-browser swipe <direction> [distance]\n\nPerforms a swipe gesture on iOS Safari. The direction determines\nwhich way the content moves (swipe up scrolls down, etc.).\n\nArguments:\n  direction    up, down, left, or right\n  distance     Optional distance in pixels (default: 300)\n\nOptions:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser -p ios swipe up\n  agent-browser -p ios swipe down 500\n  agent-browser -p ios swipe left\n\"##\n        }\n        \"device\" => {\n            r##\"\nagent-browser device - Manage iOS simulators\n\nUsage: agent-browser device <subcommand>\n\nSubcommands:\n  list    List available iOS simulators\n\nOptions:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser device list\n  agent-browser -p ios device list\n\"##\n        }\n\n        \"diff\" => {\n            r##\"\nagent-browser diff - Compare page states\n\nSubcommands:\n\n  diff snapshot                   Compare current snapshot to last snapshot in session\n  diff screenshot --baseline <f>  Visual pixel diff against a baseline image\n  diff url <url1> <url2>          Compare two pages\n\nSnapshot Diff:\n\n  Usage: agent-browser diff snapshot [options]\n\n  Options:\n    -b, --baseline <file>    Compare against a saved snapshot file\n    -s, --selector <sel>     Scope snapshot to a CSS selector or @ref\n    -c, --compact            Use compact snapshot format\n    -d, --depth <n>          Limit snapshot tree depth\n\n  Without --baseline, compares against the last snapshot taken in this session.\n\nScreenshot Diff:\n\n  Usage: agent-browser diff screenshot --baseline <file> [options]\n\n  Options:\n    -b, --baseline <file>    Baseline image to compare against (required)\n    -o, --output <file>      Path for the diff image (default: temp dir)\n    -t, --threshold <0-1>    Color distance threshold (default: 0.1)\n    -s, --selector <sel>     Scope screenshot to element\n        --full               Full page screenshot\n\nURL Diff:\n\n  Usage: agent-browser diff url <url1> <url2> [options]\n\n  Options:\n    --screenshot             Also compare screenshots (default: snapshot only)\n    --full                   Full page screenshots\n    --wait-until <strategy>  Navigation wait strategy: load, domcontentloaded, networkidle (default: load)\n    -s, --selector <sel>     Scope snapshots to a CSS selector or @ref\n    -c, --compact            Use compact snapshot format\n    -d, --depth <n>          Limit snapshot tree depth\n\nGlobal Options:\n  --json               Output as JSON\n  --session <name>     Use specific session\n\nExamples:\n  agent-browser diff snapshot\n  agent-browser diff snapshot --baseline before.txt\n  agent-browser diff screenshot --baseline before.png\n  agent-browser diff screenshot --baseline before.png --output diff.png --threshold 0.2\n  agent-browser diff url https://staging.example.com https://prod.example.com\n  agent-browser diff url https://v1.example.com https://v2.example.com --screenshot\n\"##\n        }\n\n        \"batch\" => {\n            r##\"\nagent-browser batch - Execute multiple commands from stdin\n\nUsage: echo '<json>' | agent-browser batch [options]\n\nReads a JSON array of commands from stdin and executes them sequentially.\nEach command is an array of strings matching normal CLI arguments.\nResults are printed in order, separated by blank lines (or as a JSON array\nwith --json).\n\nOptions:\n  --bail               Stop on first error (default: continue all commands)\n  --json               Output results as a JSON array\n\nInput Format:\n  A JSON array of string arrays. Each inner array is one command:\n  [\n    [\"open\", \"https://example.com\"],\n    [\"snapshot\", \"-i\"],\n    [\"click\", \"@e1\"],\n    [\"fill\", \"@e2\", \"test@example.com\"],\n    [\"screenshot\", \"result.png\"]\n  ]\n\nExamples:\n  echo '[[\"open\", \"https://example.com\"], [\"snapshot\"]]' | agent-browser batch\n  echo '[[\"open\", \"https://example.com\"], [\"get\", \"title\"]]' | agent-browser batch --json\n  agent-browser batch --bail < commands.json\n\"##\n        }\n\n        _ => return false,\n    };\n    println!(\"{}\", help.trim());\n    true\n}\n\npub fn print_help() {\n    println!(\n        r#\"\nagent-browser - fast browser automation CLI for AI agents\n\nUsage: agent-browser <command> [args] [options]\n\nCore Commands:\n  open <url>                 Navigate to URL\n  click <sel>                Click element (or @ref)\n  dblclick <sel>             Double-click element\n  type <sel> <text>          Type into element\n  fill <sel> <text>          Clear and fill\n  press <key>                Press key (Enter, Tab, Control+a)\n  keyboard type <text>       Type text with real keystrokes (no selector)\n  keyboard inserttext <text> Insert text without key events\n  hover <sel>                Hover element\n  focus <sel>                Focus element\n  check <sel>                Check checkbox\n  uncheck <sel>              Uncheck checkbox\n  select <sel> <val...>      Select dropdown option\n  drag <src> <dst>           Drag and drop\n  upload <sel> <files...>    Upload files\n  download <sel> <path>      Download file by clicking element\n  scroll <dir> [px]          Scroll (up/down/left/right)\n  scrollintoview <sel>       Scroll element into view\n  wait <sel|ms>              Wait for element or time\n  screenshot [path]          Take screenshot\n  pdf <path>                 Save as PDF\n  snapshot                   Accessibility tree with refs (for AI)\n  eval <js>                  Run JavaScript\n  connect <port|url>         Connect to browser via CDP\n  close                      Close browser\n\nNavigation:\n  back                       Go back\n  forward                    Go forward\n  reload                     Reload page\n\nGet Info:  agent-browser get <what> [selector]\n  text, html, value, attr <name>, title, url, count, box, styles, cdp-url\n\nCheck State:  agent-browser is <what> <selector>\n  visible, enabled, checked\n\nFind Elements:  agent-browser find <locator> <value> <action> [text]\n  role, text, label, placeholder, alt, title, testid, first, last, nth\n\nMouse:  agent-browser mouse <action> [args]\n  move <x> <y>, down [btn], up [btn], wheel <dy> [dx]\n\nBrowser Settings:  agent-browser set <setting> [value]\n  viewport <w> <h>, device <name>, geo <lat> <lng>\n  offline [on|off], headers <json>, credentials <user> <pass>\n  media [dark|light] [reduced-motion]\n\nNetwork:  agent-browser network <action>\n  route <url> [--abort|--body <json>]\n  unroute [url]\n  requests [--clear] [--filter <pattern>]\n  har <start|stop> [path]\n\nStorage:\n  cookies [get|set|clear]    Manage cookies (set supports --url, --domain, --path, --httpOnly, --secure, --sameSite, --expires)\n  storage <local|session>    Manage web storage\n\nTabs:\n  tab [new|list|close|<n>]   Manage tabs\n\nDiff:\n  diff snapshot              Compare current vs last snapshot\n  diff screenshot --baseline Compare current vs baseline image\n  diff url <u1> <u2>         Compare two pages\n\nDebug:\n  trace start|stop [path]    Record Chrome DevTools trace\n  profiler start|stop [path] Record Chrome DevTools profile\n  record start <path> [url]  Start video recording (WebM)\n  record stop                Stop and save video\n  console [--clear]          View console logs\n  errors [--clear]           View page errors\n  highlight <sel>            Highlight element\n  inspect                    Open Chrome DevTools for the active page\n  clipboard <op> [text]      Read/write clipboard (read, write, copy, paste)\n\nBatch:\n  batch [--bail]             Execute commands from stdin (JSON array of string arrays)\n                             --bail stops on first error (default: continue all)\n\nAuth Vault:\n  auth save <name> [opts]    Save auth profile (--url, --username, --password/--password-stdin)\n  auth login <name>          Login using saved credentials\n  auth list                  List saved auth profiles\n  auth show <name>           Show auth profile metadata\n  auth delete <name>         Delete auth profile\n\nConfirmation:\n  confirm <id>               Approve a pending action\n  deny <id>                  Deny a pending action\n\nSessions:\n  session                    Show current session name\n  session list               List active sessions\n\nSetup:\n  install                    Install browser binaries\n  install --with-deps        Also install system dependencies (Linux)\n  upgrade                    Upgrade to the latest version\n\nSnapshot Options:\n  -i, --interactive          Only interactive elements\n  -c, --compact              Remove empty structural elements\n  -d, --depth <n>            Limit tree depth\n  -s, --selector <sel>       Scope to CSS selector\n\nAuthentication:\n  --profile <path>           Persist login sessions across restarts (cookies, IndexedDB, cache)\n                             (or AGENT_BROWSER_PROFILE env)\n  --session-name <name>      Auto-save/restore cookies and localStorage by name\n                             (or AGENT_BROWSER_SESSION_NAME env)\n  --state <path>             Load saved auth state (cookies + storage) from JSON file\n                             (or AGENT_BROWSER_STATE env)\n  --auto-connect             Connect to a running Chrome to reuse its auth state\n                             Tip: agent-browser --auto-connect state save ./auth.json\n  --headers <json>           HTTP headers scoped to URL's origin (e.g., Authorization bearer token)\n\nOptions:\n  --session <name>           Isolated session (or AGENT_BROWSER_SESSION env)\n  --executable-path <path>   Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)\n  --extension <path>         Load browser extensions (repeatable)\n  --args <args>              Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS)\n                             e.g., --args \"--no-sandbox,--disable-blink-features=AutomationControlled\"\n  --user-agent <ua>          Custom User-Agent (or AGENT_BROWSER_USER_AGENT)\n  --proxy <server>           Proxy server URL (or AGENT_BROWSER_PROXY)\n                             e.g., --proxy \"http://user:pass@127.0.0.1:7890\"\n  --proxy-bypass <hosts>     Bypass proxy for these hosts (or AGENT_BROWSER_PROXY_BYPASS)\n                             e.g., --proxy-bypass \"localhost,*.internal.com\"\n  --ignore-https-errors      Ignore HTTPS certificate errors\n  --allow-file-access        Allow file:// URLs to access local files (Chromium only)\n  -p, --provider <name>      Browser provider: ios, browserbase, kernel, browseruse, browserless\n  --device <name>            iOS device name (e.g., \"iPhone 15 Pro\")\n  --json                     JSON output\n  --annotate                 Annotated screenshot with numbered labels and legend\n  --screenshot-dir <path>    Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)\n  --screenshot-quality <n>   JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY)\n  --screenshot-format <fmt>  Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)\n  --headed                   Show browser window (not headless) (or AGENT_BROWSER_HEADED env)\n  --cdp <port>               Connect via CDP (Chrome DevTools Protocol)\n  --color-scheme <scheme>    Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)\n  --download-path <path>     Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)\n  --content-boundaries       Wrap page output in boundary markers (or AGENT_BROWSER_CONTENT_BOUNDARIES)\n  --max-output <chars>       Truncate page output to N chars (or AGENT_BROWSER_MAX_OUTPUT)\n  --allowed-domains <list>   Restrict navigation domains (or AGENT_BROWSER_ALLOWED_DOMAINS)\n  --action-policy <path>     Action policy JSON file (or AGENT_BROWSER_ACTION_POLICY)\n  --confirm-actions <list>   Categories requiring confirmation (or AGENT_BROWSER_CONFIRM_ACTIONS)\n  --confirm-interactive      Interactive confirmation prompts; auto-denies if stdin is not a TTY (or AGENT_BROWSER_CONFIRM_INTERACTIVE)\n  --engine <name>            Browser engine: chrome (default), lightpanda (or AGENT_BROWSER_ENGINE)\n  --config <path>            Use a custom config file (or AGENT_BROWSER_CONFIG env)\n  --debug                    Debug output\n  --version, -V              Show version\n\nConfiguration:\n  agent-browser looks for agent-browser.json in these locations (lowest to highest priority):\n    1. ~/.agent-browser/config.json      User-level defaults\n    2. ./agent-browser.json              Project-level overrides\n    3. Environment variables             Override config file values\n    4. CLI flags                         Override everything\n\n  Use --config <path> to load a specific config file instead of the defaults.\n  If --config points to a missing or invalid file, agent-browser exits with an error.\n\n  Boolean flags accept an optional true/false value to override config:\n    --headed           (same as --headed true)\n    --headed false     (disables \"headed\": true from config)\n\n  Extensions from user and project configs are merged (not replaced).\n\n  Example agent-browser.json:\n    {{\"headed\": true, \"proxy\": \"http://localhost:8080\", \"profile\": \"./browser-data\"}}\n\nEnvironment:\n  AGENT_BROWSER_CONFIG           Path to config file (or use --config)\n  AGENT_BROWSER_SESSION          Session name (default: \"default\")\n  AGENT_BROWSER_SESSION_NAME     Auto-save/restore state persistence name\n  AGENT_BROWSER_ENCRYPTION_KEY   64-char hex key for AES-256-GCM state encryption\n  AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete states older than N days (default: 30)\n  AGENT_BROWSER_EXECUTABLE_PATH  Custom browser executable path\n  AGENT_BROWSER_EXTENSIONS       Comma-separated browser extension paths\n  AGENT_BROWSER_HEADED           Show browser window (not headless)\n  AGENT_BROWSER_JSON             JSON output\n  AGENT_BROWSER_ANNOTATE         Annotated screenshot with numbered labels and legend\n  AGENT_BROWSER_DEBUG            Debug output\n  AGENT_BROWSER_IGNORE_HTTPS_ERRORS Ignore HTTPS certificate errors\n  AGENT_BROWSER_PROVIDER         Browser provider (ios, browserbase, kernel, browseruse, browserless)\n  AGENT_BROWSER_AUTO_CONNECT     Auto-discover and connect to running Chrome\n  AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files\n  AGENT_BROWSER_COLOR_SCHEME     Color scheme preference (dark, light, no-preference)\n  AGENT_BROWSER_DOWNLOAD_PATH    Default download directory for browser downloads\n  AGENT_BROWSER_DEFAULT_TIMEOUT  Default action timeout in ms (default: 25000)\n  AGENT_BROWSER_SESSION_NAME     Auto-save/load state persistence name\n  AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)\n  AGENT_BROWSER_ENCRYPTION_KEY   64-char hex key for AES-256-GCM session encryption\n  AGENT_BROWSER_STREAM_PORT      Enable WebSocket streaming on port (e.g., 9223)\n  AGENT_BROWSER_IDLE_TIMEOUT_MS  Auto-shutdown daemon after N ms of inactivity (disabled by default)\n  AGENT_BROWSER_IOS_DEVICE       Default iOS device name\n  AGENT_BROWSER_IOS_UDID         Default iOS device UDID\n  AGENT_BROWSER_CONTENT_BOUNDARIES Wrap page output in boundary markers\n  AGENT_BROWSER_MAX_OUTPUT       Max characters for page output\n  AGENT_BROWSER_ALLOWED_DOMAINS  Comma-separated allowed domain patterns\n  AGENT_BROWSER_ACTION_POLICY    Path to action policy JSON file\n  AGENT_BROWSER_CONFIRM_ACTIONS  Action categories requiring confirmation\n  AGENT_BROWSER_CONFIRM_INTERACTIVE Enable interactive confirmation prompts\n  AGENT_BROWSER_ENGINE           Browser engine: chrome (default), lightpanda\n  AGENT_BROWSER_SCREENSHOT_DIR   Default screenshot output directory\n  AGENT_BROWSER_SCREENSHOT_QUALITY JPEG quality 0-100\n  AGENT_BROWSER_SCREENSHOT_FORMAT Screenshot format: png, jpeg\n\nInstall:\n  npm install -g agent-browser           # npm\n  brew install agent-browser             # Homebrew\n  cargo install agent-browser            # Cargo\n  agent-browser install                  # Download Chrome (first time)\n\nExamples:\n  agent-browser open example.com\n  agent-browser snapshot -i              # Interactive elements only\n  agent-browser click @e2                # Click by ref from snapshot\n  agent-browser fill @e3 \"test@example.com\"\n  agent-browser find role button click --name Submit\n  agent-browser get text @e1\n  agent-browser screenshot --full\n  agent-browser screenshot --annotate    # Labeled screenshot for vision models\n  agent-browser wait --load networkidle  # Wait for slow pages to load\n  agent-browser --cdp 9222 snapshot      # Connect via CDP port\n  agent-browser --auto-connect snapshot  # Auto-discover running Chrome\n  agent-browser --color-scheme dark open example.com  # Dark mode\n  agent-browser --profile ~/.myapp open example.com    # Persistent profile\n  agent-browser --session-name myapp open example.com  # Auto-save/restore state\n\nCommand Chaining:\n  Chain commands with && in a single shell call (browser persists via daemon):\n\n  agent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i\n  agent-browser fill @e1 \"user@example.com\" && agent-browser fill @e2 \"pass\" && agent-browser click @e3\n  agent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png\n\niOS Simulator (requires Xcode and Appium):\n  agent-browser -p ios open example.com                    # Use default iPhone\n  agent-browser -p ios --device \"iPhone 15 Pro\" open url   # Specific device\n  agent-browser -p ios device list                         # List simulators\n  agent-browser -p ios swipe up                            # Swipe gesture\n  agent-browser -p ios tap @e1                             # Touch element\n\"#\n    );\n}\n\nfn print_snapshot_diff(data: &serde_json::Map<String, serde_json::Value>) {\n    let changed = data\n        .get(\"changed\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false);\n    if !changed {\n        println!(\"{} No changes detected\", color::success_indicator());\n        return;\n    }\n    if let Some(diff) = data.get(\"diff\").and_then(|v| v.as_str()) {\n        for line in diff.lines() {\n            if line.starts_with(\"+ \") {\n                println!(\"{}\", color::green(line));\n            } else if line.starts_with(\"- \") {\n                println!(\"{}\", color::red(line));\n            } else {\n                println!(\"{}\", color::dim(line));\n            }\n        }\n        let additions = data.get(\"additions\").and_then(|v| v.as_i64()).unwrap_or(0);\n        let removals = data.get(\"removals\").and_then(|v| v.as_i64()).unwrap_or(0);\n        let unchanged = data.get(\"unchanged\").and_then(|v| v.as_i64()).unwrap_or(0);\n        println!(\n            \"\\n{} additions, {} removals, {} unchanged\",\n            color::green(&additions.to_string()),\n            color::red(&removals.to_string()),\n            unchanged\n        );\n    }\n}\n\nfn print_screenshot_diff(data: &serde_json::Map<String, serde_json::Value>) {\n    let mismatch = data\n        .get(\"mismatchPercentage\")\n        .and_then(|v| v.as_f64())\n        .unwrap_or(0.0);\n    let is_match = data.get(\"match\").and_then(|v| v.as_bool()).unwrap_or(false);\n    let dim_mismatch = data\n        .get(\"dimensionMismatch\")\n        .and_then(|v| v.as_bool())\n        .unwrap_or(false);\n    if dim_mismatch {\n        println!(\n            \"{} Images have different dimensions\",\n            color::error_indicator()\n        );\n    } else if is_match {\n        println!(\n            \"{} Images match (0% difference)\",\n            color::success_indicator()\n        );\n    } else {\n        println!(\n            \"{} {:.2}% pixels differ\",\n            color::error_indicator(),\n            mismatch\n        );\n    }\n    if let Some(diff_path) = data.get(\"diffPath\").and_then(|v| v.as_str()) {\n        println!(\"  Diff image: {}\", color::green(diff_path));\n    }\n    let total = data\n        .get(\"totalPixels\")\n        .and_then(|v| v.as_i64())\n        .unwrap_or(0);\n    let different = data\n        .get(\"differentPixels\")\n        .and_then(|v| v.as_i64())\n        .unwrap_or(0);\n    println!(\n        \"  {} different / {} total pixels\",\n        color::red(&different.to_string()),\n        total\n    );\n}\n\npub fn print_version() {\n    println!(\"agent-browser {}\", env!(\"CARGO_PKG_VERSION\"));\n}\n\n#[cfg(test)]\nmod tests {\n    use super::format_storage_text;\n    use serde_json::json;\n\n    #[test]\n    fn test_format_storage_text_for_all_entries() {\n        let data = json!({\n            \"data\": {\n                \"token\": \"abc123\",\n                \"user\": \"alice\"\n            }\n        });\n\n        let rendered = format_storage_text(&data).unwrap();\n\n        assert_eq!(rendered, \"token: abc123\\nuser: alice\");\n    }\n\n    #[test]\n    fn test_format_storage_text_for_key_lookup() {\n        let data = json!({\n            \"key\": \"token\",\n            \"value\": \"abc123\"\n        });\n\n        let rendered = format_storage_text(&data).unwrap();\n\n        assert_eq!(rendered, \"token: abc123\");\n    }\n\n    #[test]\n    fn test_format_storage_text_for_empty_store() {\n        let data = json!({\n            \"data\": {}\n        });\n\n        let rendered = format_storage_text(&data).unwrap();\n\n        assert_eq!(rendered, \"No storage entries\");\n    }\n}\n"
  },
  {
    "path": "cli/src/test_utils.rs",
    "content": "use std::sync::{Mutex, MutexGuard};\n\n/// Global mutex shared across all test modules to prevent parallel tests from\n/// interfering with each other when mutating environment variables.\npub static ENV_MUTEX: Mutex<()> = Mutex::new(());\n\n/// RAII guard that locks [`ENV_MUTEX`] and restores environment variables on drop.\npub struct EnvGuard<'a> {\n    _lock: MutexGuard<'a, ()>,\n    vars: Vec<(String, Option<String>)>,\n}\n\nimpl<'a> EnvGuard<'a> {\n    pub fn new(var_names: &[&str]) -> Self {\n        let lock = ENV_MUTEX.lock().unwrap();\n        let vars = var_names\n            .iter()\n            .map(|&name| (name.to_string(), std::env::var(name).ok()))\n            .collect();\n        Self { _lock: lock, vars }\n    }\n\n    pub fn set(&self, name: &str, value: &str) {\n        debug_assert!(\n            self.vars.iter().any(|(n, _)| n == name),\n            \"EnvGuard::set called with unregistered var: {name}\"\n        );\n        std::env::set_var(name, value);\n    }\n\n    pub fn remove(&self, name: &str) {\n        debug_assert!(\n            self.vars.iter().any(|(n, _)| n == name),\n            \"EnvGuard::remove called with unregistered var: {name}\"\n        );\n        std::env::remove_var(name);\n    }\n}\n\nimpl Drop for EnvGuard<'_> {\n    fn drop(&mut self) {\n        for (name, value) in &self.vars {\n            match value {\n                Some(v) => std::env::set_var(name, v),\n                None => std::env::remove_var(name),\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/upgrade.rs",
    "content": "use crate::color;\nuse std::process::{exit, Command, Stdio};\n\nconst CURRENT_VERSION: &str = env!(\"CARGO_PKG_VERSION\");\nconst NPM_REGISTRY_URL: &str = \"https://registry.npmjs.org/agent-browser/latest\";\n\nenum InstallMethod {\n    Npm,\n    Homebrew,\n    Cargo,\n    Unknown,\n}\n\nasync fn fetch_latest_version() -> Result<String, String> {\n    let resp = reqwest::get(NPM_REGISTRY_URL)\n        .await\n        .map_err(|e| format!(\"Failed to fetch version info: {}\", e))?;\n\n    let body: serde_json::Value = resp\n        .json()\n        .await\n        .map_err(|e| format!(\"Failed to parse version info: {}\", e))?;\n\n    body.get(\"version\")\n        .and_then(|v| v.as_str())\n        .map(|s| s.to_string())\n        .ok_or_else(|| \"No version field in registry response\".to_string())\n}\n\nfn detect_install_method() -> InstallMethod {\n    // Check Homebrew (available on macOS and Linux)\n    #[cfg(any(target_os = \"macos\", target_os = \"linux\"))]\n    {\n        let brew_check = Command::new(\"brew\")\n            .args([\"list\", \"agent-browser\"])\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .status();\n        if brew_check.map(|s| s.success()).unwrap_or(false) {\n            return InstallMethod::Homebrew;\n        }\n    }\n\n    // Check Cargo installation by executable path\n    if let Ok(exe) = std::env::current_exe() {\n        let path_str = exe.to_string_lossy();\n        if path_str.contains(\"/.cargo/bin/\") || path_str.contains(\"\\\\.cargo\\\\bin\\\\\") {\n            return InstallMethod::Cargo;\n        }\n    }\n\n    // Check npm global installation\n    let npm_check = Command::new(\"npm\")\n        .args([\"list\", \"-g\", \"agent-browser\", \"--depth=0\"])\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .status();\n    if npm_check.map(|s| s.success()).unwrap_or(false) {\n        return InstallMethod::Npm;\n    }\n\n    InstallMethod::Unknown\n}\n\nfn run_upgrade_command(method: &InstallMethod) -> bool {\n    match method {\n        InstallMethod::Npm => {\n            println!(\"Running: npm install -g agent-browser@latest\");\n            Command::new(\"npm\")\n                .args([\"install\", \"-g\", \"agent-browser@latest\"])\n                .status()\n                .map(|s| s.success())\n                .unwrap_or(false)\n        }\n        InstallMethod::Homebrew => {\n            println!(\"Running: brew upgrade agent-browser\");\n            Command::new(\"brew\")\n                .args([\"upgrade\", \"agent-browser\"])\n                .status()\n                .map(|s| s.success())\n                .unwrap_or(false)\n        }\n        InstallMethod::Cargo => {\n            println!(\"Running: cargo install agent-browser --force\");\n            Command::new(\"cargo\")\n                .args([\"install\", \"agent-browser\", \"--force\"])\n                .status()\n                .map(|s| s.success())\n                .unwrap_or(false)\n        }\n        InstallMethod::Unknown => false,\n    }\n}\n\npub fn run_upgrade() {\n    let current = CURRENT_VERSION;\n\n    let rt = tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()\n        .unwrap_or_else(|e| {\n            eprintln!(\n                \"{} Failed to create runtime: {}\",\n                color::error_indicator(),\n                e\n            );\n            exit(1);\n        });\n\n    let latest = match rt.block_on(fetch_latest_version()) {\n        Ok(v) => v,\n        Err(e) => {\n            eprintln!(\n                \"{} Could not check latest version: {}\",\n                color::warning_indicator(),\n                e\n            );\n            String::new()\n        }\n    };\n\n    if !latest.is_empty() && current == latest.as_str() {\n        println!(\n            \"{} agent-browser is already at the latest version (v{})\",\n            color::success_indicator(),\n            current\n        );\n        return;\n    }\n\n    let method = detect_install_method();\n\n    let method_name = match &method {\n        InstallMethod::Npm => \"npm\",\n        InstallMethod::Homebrew => \"Homebrew\",\n        InstallMethod::Cargo => \"Cargo\",\n        InstallMethod::Unknown => \"\",\n    };\n\n    if matches!(method, InstallMethod::Unknown) {\n        eprintln!(\n            \"{} Could not detect installation method.\",\n            color::error_indicator()\n        );\n        eprintln!(\"  To update manually, run one of:\");\n        eprintln!(\"    npm install -g agent-browser@latest     # npm\");\n        eprintln!(\"    brew upgrade agent-browser               # Homebrew\");\n        eprintln!(\"    cargo install agent-browser --force      # Cargo\");\n        exit(1);\n    }\n\n    println!(\"Detected installation via {}.\", method_name);\n\n    if !latest.is_empty() {\n        println!(\n            \"{}\",\n            color::cyan(&format!(\n                \"Upgrading agent-browser... v{} → v{}\",\n                current, latest\n            ))\n        );\n    } else {\n        println!(\n            \"{}\",\n            color::cyan(&format!(\"Upgrading agent-browser (v{})...\", current))\n        );\n    }\n\n    let success = run_upgrade_command(&method);\n\n    if success {\n        if !latest.is_empty() {\n            println!(\n                \"{} Done! v{} → v{}\",\n                color::success_indicator(),\n                current,\n                latest\n            );\n        } else {\n            println!(\"{} Done!\", color::success_indicator());\n        }\n    } else {\n        eprintln!(\"{} Upgrade failed.\", color::error_indicator());\n        exit(1);\n    }\n}\n"
  },
  {
    "path": "cli/src/validation.rs",
    "content": "/// Check if a session name is valid (alphanumeric, hyphens, and underscores only)\npub fn is_valid_session_name(name: &str) -> bool {\n    !name.is_empty()\n        && name\n            .chars()\n            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')\n}\n\n/// Generate error message for invalid session name\npub fn session_name_error(name: &str) -> String {\n    format!(\n        \"Invalid session name '{}'. Only alphanumeric characters, hyphens, and underscores are allowed.\",\n        name\n    )\n}\n"
  },
  {
    "path": "docker/Dockerfile.build",
    "content": "# Multi-platform Rust cross-compilation image\nFROM rust:1.85-bookworm\n\n# Install cross-compilation toolchains\nRUN apt-get update && apt-get install -y \\\n    gcc-aarch64-linux-gnu \\\n    gcc-x86-64-linux-gnu \\\n    libc6-dev-arm64-cross \\\n    libc6-dev-amd64-cross \\\n    mingw-w64 \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Add Rust targets for all platforms\nRUN rustup target add \\\n    x86_64-unknown-linux-gnu \\\n    aarch64-unknown-linux-gnu \\\n    x86_64-apple-darwin \\\n    aarch64-apple-darwin \\\n    x86_64-pc-windows-gnu\n\n# Install cargo-zigbuild for easier cross-compilation (especially macOS)\nRUN curl -sSL https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ -C /opt \\\n    && ln -s /opt/zig-linux-x86_64-0.13.0/zig /usr/local/bin/zig\nRUN cargo install cargo-zigbuild\n\n# Configure linkers for cross-compilation\nRUN mkdir -p /.cargo\nRUN echo '[target.aarch64-unknown-linux-gnu]\\nlinker = \"aarch64-linux-gnu-gcc\"\\n\\n[target.x86_64-pc-windows-gnu]\\nlinker = \"x86_64-w64-mingw32-gcc\"\\n' > /.cargo/config.toml\n\nWORKDIR /build\n\nENTRYPOINT [\"/bin/bash\"]\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "# Docker Compose for building agent-browser\n# Usage: docker compose -f docker/docker-compose.yml run build-linux\n#        docker compose -f docker/docker-compose.yml run build-windows\n#\n# Note: macOS builds should be done natively on macOS for compatibility.\n\nservices:\n  # Build for Linux platforms\n  build-linux:\n    build:\n      context: ..\n      dockerfile: docker/Dockerfile.build\n    volumes:\n      - ../cli:/build\n      - ../bin:/output\n    command: |\n      -c '\n        set -e\n        echo \"Building for Linux platforms (parallel)...\"\n        \n        # Build both targets in parallel\n        (echo \"→ Linux x64\" && cargo zigbuild --release --target x86_64-unknown-linux-gnu && cp /build/target/x86_64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-x64 && chmod +x /output/agent-browser-linux-x64 && echo \"✓ Linux x64 done\") &\n        PID1=$!\n        \n        (echo \"→ Linux ARM64\" && cargo zigbuild --release --target aarch64-unknown-linux-gnu && cp /build/target/aarch64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-arm64 && chmod +x /output/agent-browser-linux-arm64 && echo \"✓ Linux ARM64 done\") &\n        PID2=$!\n        \n        # Wait for both to complete\n        wait $PID1 $PID2\n        \n        echo \"\"\n        echo \"✓ Linux platforms built successfully!\"\n        ls -la /output/agent-browser-linux-*\n      '\n\n  # Build for Windows\n  build-windows:\n    build:\n      context: ..\n      dockerfile: docker/Dockerfile.build\n    volumes:\n      - ../cli:/build\n      - ../bin:/output\n    command: |\n      -c '\n        set -e\n        echo \"Building for Windows x64...\"\n        \n        cargo build --release --target x86_64-pc-windows-gnu\n        cp /build/target/x86_64-pc-windows-gnu/release/agent-browser.exe /output/agent-browser-win32-x64.exe\n        \n        echo \"\"\n        echo \"✓ Windows build completed!\"\n        ls -la /output/agent-browser-win32-*\n      '\n\n  # Build for a single target (override with TARGET env var)\n  build-single:\n    build:\n      context: ..\n      dockerfile: docker/Dockerfile.build\n    volumes:\n      - ../cli:/build\n      - ../bin:/output\n    environment:\n      - TARGET=${TARGET:-x86_64-unknown-linux-gnu}\n      - OUTPUT_NAME=${OUTPUT_NAME:-agent-browser-linux-x64}\n    command: |\n      -c '\n        cargo zigbuild --release --target $TARGET\n        cp /build/target/$TARGET/release/agent-browser* /output/$OUTPUT_NAME\n        chmod +x /output/$OUTPUT_NAME 2>/dev/null || true\n        echo \"✓ Built $OUTPUT_NAME\"\n      '\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "docs/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "docs/eslint.config.mjs",
    "content": "import { defineConfig, globalIgnores } from \"eslint/config\";\nimport nextVitals from \"eslint-config-next/core-web-vitals\";\nimport nextTs from \"eslint-config-next/typescript\";\n\nconst eslintConfig = defineConfig([\n  ...nextVitals,\n  ...nextTs,\n  // Override default ignores of eslint-config-next.\n  globalIgnores([\n    // Default ignores of eslint-config-next:\n    \".next/**\",\n    \"out/**\",\n    \"build/**\",\n    \"next-env.d.ts\",\n  ]),\n]);\n\nexport default eslintConfig;\n"
  },
  {
    "path": "docs/mdx-components.tsx",
    "content": "import type { MDXComponents } from \"mdx/types\";\nimport Link from \"next/link\";\nimport { CodeBlock } from \"@/components/code-block\";\n\nfunction slugify(text: string): string {\n  return text\n    .toLowerCase()\n    .replace(/[^\\w\\s-]/g, \"\")\n    .replace(/\\s+/g, \"-\")\n    .trim();\n}\n\nfunction extractText(children: React.ReactNode): string {\n  if (typeof children === \"string\") return children;\n  if (typeof children === \"number\") return String(children);\n  if (Array.isArray(children)) return children.map(extractText).join(\"\");\n  if (children && typeof children === \"object\") {\n    const obj = children as unknown as Record<string, unknown>;\n    if (\"props\" in obj) {\n      const props = obj.props as { children?: React.ReactNode } | undefined;\n      return extractText(props?.children);\n    }\n  }\n  return \"\";\n}\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n  return {\n    ...components,\n    h2: ({ children }: { children?: React.ReactNode }) => {\n      const id = slugify(extractText(children));\n      return <h2 id={id}>{children}</h2>;\n    },\n    h3: ({ children }: { children?: React.ReactNode }) => {\n      const id = slugify(extractText(children));\n      return <h3 id={id}>{children}</h3>;\n    },\n    a: ({\n      href,\n      children,\n    }: {\n      href?: string;\n      children?: React.ReactNode;\n    }) => {\n      if (href?.startsWith(\"/\")) {\n        return <Link href={href}>{children}</Link>;\n      }\n      return (\n        <a href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n          {children}\n        </a>\n      );\n    },\n    code: ({\n      children,\n      className,\n    }: {\n      children?: React.ReactNode;\n      className?: string;\n    }) => {\n      if (className) {\n        return <code className={className}>{children}</code>;\n      }\n      return <code>{children}</code>;\n    },\n    pre: async ({ children }: { children?: React.ReactNode }) => {\n      const codeElement = children as React.ReactElement<{\n        className?: string;\n        children?: string;\n      }>;\n      const className = codeElement?.props?.className || \"\";\n      const lang = className.replace(\"language-\", \"\") || \"bash\";\n      const code = codeElement?.props?.children || \"\";\n\n      return (\n        <CodeBlock\n          code={typeof code === \"string\" ? code : String(code)}\n          lang={lang}\n        />\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "docs/next.config.mjs",
    "content": "import createMDX from \"@next/mdx\";\n\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  pageExtensions: [\"js\", \"jsx\", \"ts\", \"tsx\", \"md\", \"mdx\"],\n  serverExternalPackages: [\"just-bash\", \"bash-tool\"],\n};\n\nconst withMDX = createMDX({});\n\nexport default withMDX(nextConfig);\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"docs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"portless agent-browser next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/react\": \"^3.0.80\",\n    \"@mdx-js/loader\": \"^3.1.1\",\n    \"@mdx-js/mdx\": \"^3.1.1\",\n    \"@mdx-js/react\": \"^3.1.1\",\n    \"@next/mdx\": \"^16.1.6\",\n    \"@streamdown/code\": \"^1.0.2\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.36.2\",\n    \"@vercel/analytics\": \"^1.6.1\",\n    \"@vercel/speed-insights\": \"^1.3.1\",\n    \"ai\": \"^6.0.78\",\n    \"bash-tool\": \"^1.3.14\",\n    \"clsx\": \"^2.1.1\",\n    \"geist\": \"^1.7.0\",\n    \"just-bash\": \"^2.9.6\",\n    \"next\": \"16.1.1\",\n    \"next-themes\": \"^0.4.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.3\",\n    \"react-dom\": \"19.2.3\",\n    \"shiki\": \"^3.21.0\",\n    \"streamdown\": \"^2.1.0\",\n    \"tailwind-merge\": \"^3.4.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"16.1.1\",\n    \"tailwindcss\": \"^4\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "docs/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "docs/src/app/api/docs-chat/route.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { convertToModelMessages, stepCountIs, streamText } from \"ai\";\nimport type { ModelMessage, UIMessage } from \"ai\";\nimport { createBashTool } from \"bash-tool\";\nimport { headers } from \"next/headers\";\nimport { allDocsPages } from \"@/lib/docs-navigation\";\nimport { mdxToCleanMarkdown } from \"@/lib/mdx-to-markdown\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\n\nexport const maxDuration = 60;\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nconst SYSTEM_PROMPT = `You are a helpful documentation assistant for agent-browser, a headless browser automation CLI designed for AI agents.\n\nGitHub repository: https://github.com/vercel-labs/agent-browser\nDocumentation: https://agent-browser.dev\nnpm package: agent-browser\n\nYou have access to the full agent-browser documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.\n\nWhen answering questions:\n- Use the bash tool to list files (ls /workspace/) or search for content (grep -r \"keyword\" /workspace/)\n- Use the readFile tool to read specific documentation pages (e.g. readFile with path \"/workspace/index.md\")\n- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only\n- Always base your answers on the actual documentation content\n- Be concise and accurate\n- If the docs don't cover a topic, say so honestly\n- Do NOT include source references or file paths in your response\n- Do NOT use emojis in your responses`;\n\nasync function loadDocsFiles(): Promise<Record<string, string>> {\n  const files: Record<string, string> = {};\n\n  const results = await Promise.allSettled(\n    allDocsPages.map(async (page) => {\n      const slug = page.href === \"/\" ? \"\" : page.href.replace(/^\\//, \"\");\n      const filePath = slug\n        ? join(process.cwd(), \"src\", \"app\", slug, \"page.mdx\")\n        : join(process.cwd(), \"src\", \"app\", \"page.mdx\");\n\n      const raw = await readFile(filePath, \"utf-8\");\n      const md = mdxToCleanMarkdown(raw);\n      const fileName = slug ? `/${slug}.md` : \"/index.md\";\n      return { fileName, md };\n    }),\n  );\n\n  for (const result of results) {\n    if (result.status === \"fulfilled\") {\n      files[result.value.fileName] = result.value.md;\n    }\n  }\n\n  return files;\n}\n\nfunction addCacheControl(messages: ModelMessage[]): ModelMessage[] {\n  if (messages.length === 0) return messages;\n  return messages.map((message, index) => {\n    if (index === messages.length - 1) {\n      return {\n        ...message,\n        providerOptions: {\n          ...message.providerOptions,\n          anthropic: { cacheControl: { type: \"ephemeral\" } },\n        },\n      };\n    }\n    return message;\n  });\n}\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { messages }: { messages: UIMessage[] } = await req.json();\n\n  const docsFiles = await loadDocsFiles();\n  const {\n    tools: { bash, readFile },\n  } = await createBashTool({ files: docsFiles });\n\n  const result = streamText({\n    model: DEFAULT_MODEL,\n    system: SYSTEM_PROMPT,\n    messages: await convertToModelMessages(messages),\n    stopWhen: stepCountIs(5),\n    tools: { bash, readFile },\n    prepareStep: ({ messages: stepMessages }) => ({\n      messages: addCacheControl(stepMessages),\n    }),\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n"
  },
  {
    "path": "docs/src/app/api/docs-markdown/route.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { mdxToCleanMarkdown } from \"@/lib/mdx-to-markdown\";\n\nexport async function GET(req: NextRequest) {\n  const { searchParams } = new URL(req.url);\n  const docPath = searchParams.get(\"path\");\n\n  if (!docPath) {\n    return NextResponse.json(\n      { error: \"Missing ?path= parameter\" },\n      { status: 400 },\n    );\n  }\n\n  const normalized = docPath\n    .replace(/^\\//, \"\")\n    .replace(/\\.\\./g, \"\")\n    .replace(/[^a-zA-Z0-9/_-]/g, \"\");\n\n  const slug = normalized;\n  const filePath = slug\n    ? join(process.cwd(), \"src\", \"app\", ...slug.split(\"/\"), \"page.mdx\")\n    : join(process.cwd(), \"src\", \"app\", \"page.mdx\");\n\n  try {\n    const raw = await readFile(filePath, \"utf-8\");\n    const markdown = mdxToCleanMarkdown(raw);\n\n    return new NextResponse(markdown, {\n      headers: {\n        \"Content-Type\": \"text/markdown; charset=utf-8\",\n        \"Cache-Control\": \"public, max-age=3600\",\n      },\n    });\n  } catch {\n    return NextResponse.json({ error: \"Page not found\" }, { status: 404 });\n  }\n}\n"
  },
  {
    "path": "docs/src/app/api/search/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getSearchIndex } from \"@/lib/search-index\";\n\nexport async function GET(req: NextRequest) {\n  const q = req.nextUrl.searchParams.get(\"q\")?.trim().toLowerCase();\n\n  if (!q) {\n    return NextResponse.json({ results: [] });\n  }\n\n  const index = await getSearchIndex();\n  const terms = q.split(/\\s+/).filter(Boolean);\n\n  const results = index\n    .map((entry) => {\n      const titleLower = entry.title.toLowerCase();\n      const contentLower = entry.content.toLowerCase();\n\n      const titleMatch = terms.every((t) => titleLower.includes(t));\n      const contentMatch = terms.every((t) => contentLower.includes(t));\n\n      if (!titleMatch && !contentMatch) return null;\n\n      let snippet = \"\";\n      if (contentMatch) {\n        const firstTermIdx = Math.min(\n          ...terms.map((t) => {\n            const idx = contentLower.indexOf(t);\n            return idx === -1 ? Infinity : idx;\n          }),\n        );\n        if (firstTermIdx !== Infinity) {\n          const start = Math.max(0, firstTermIdx - 40);\n          const end = Math.min(entry.content.length, firstTermIdx + 120);\n          snippet =\n            (start > 0 ? \"...\" : \"\") +\n            entry.content.slice(start, end).replace(/\\n/g, \" \") +\n            (end < entry.content.length ? \"...\" : \"\");\n        }\n      }\n\n      return {\n        title: entry.title,\n        href: entry.href,\n        section: entry.section,\n        snippet,\n        score: titleMatch ? 2 : 1,\n      };\n    })\n    .filter(\n      (\n        r,\n      ): r is {\n        title: string;\n        href: string;\n        section: string;\n        snippet: string;\n        score: number;\n      } => r !== null,\n    )\n    .sort((a, b) => b.score - a.score)\n    .slice(0, 20)\n    .map(({ score: _, ...rest }) => rest);\n\n  return NextResponse.json(\n    { results },\n    { headers: { \"Cache-Control\": \"public, max-age=60\" } },\n  );\n}\n"
  },
  {
    "path": "docs/src/app/cdp-mode/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"cdp-mode\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/cdp-mode/page.mdx",
    "content": "# CDP Mode\n\nConnect to an existing browser via Chrome DevTools Protocol:\n\n```bash\n# Start Chrome with: google-chrome --remote-debugging-port=9222\n\n# Connect once, then run commands without --cdp\nagent-browser connect 9222\nagent-browser snapshot\nagent-browser tab\nagent-browser close\n\n# Or pass --cdp on each command\nagent-browser --cdp 9222 snapshot\n```\n\n## Remote WebSocket URLs\n\nConnect to remote browser services via WebSocket URL:\n\n```bash\n# Connect to remote browser service\nagent-browser --cdp \"wss://browser-service.com/cdp?token=...\" snapshot\n\n# Works with any CDP-compatible service\nagent-browser --cdp \"ws://localhost:9222/devtools/browser/abc123\" open example.com\n```\n\nThe `--cdp` flag accepts either:\n\n- A port number (e.g., `9222`) for local connections via `http://localhost:{port}`\n- A full WebSocket URL (e.g., `wss://...` or `ws://...`) for remote browser services\n\n## Auto-Connect\n\nUse `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port:\n\n```bash\n# Auto-discover running Chrome with remote debugging\nagent-browser --auto-connect open example.com\nagent-browser --auto-connect snapshot\n\n# Or via environment variable\nAGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot\n```\n\nAuto-connect discovers Chrome by:\n\n1. Reading Chrome's `DevToolsActivePort` file from the default user data directory\n2. Falling back to probing common debugging ports (9222, 9229)\n3. If HTTP-based discovery (`/json/version`, `/json/list`) fails, falling back to a direct WebSocket connection\n\nThis is useful when:\n\n- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port)\n- You want a zero-configuration connection to your existing browser\n- You don't want to track which port Chrome is using\n\n## Color scheme\n\nUse `--color-scheme` to set a persistent preference when connecting via CDP:\n\n```bash\nagent-browser --cdp 9222 --color-scheme dark open https://example.com\nagent-browser --cdp 9222 snapshot  # stays in dark mode\n```\n\nOr set it globally via config or environment variable:\n\n```bash\nAGENT_BROWSER_COLOR_SCHEME=dark agent-browser --cdp 9222 open https://example.com\n```\n\n## Use cases\n\nThis enables control of:\n\n- Electron apps\n- Chrome/Chromium with remote debugging\n- WebView2 applications\n- Remote browser services (via WebSocket URL)\n- Any browser exposing a CDP endpoint\n\n## Global options\n\n<table>\n  <thead>\n    <tr><th>Option</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>--session &lt;name&gt;</code></td><td>Use isolated session</td></tr>\n    <tr><td><code>--profile &lt;path&gt;</code></td><td>Persistent browser profile directory</td></tr>\n    <tr><td><code>-p &lt;provider&gt;</code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>kernel</code>, <code>browserless</code>)</td></tr>\n    <tr><td><code>--headers &lt;json&gt;</code></td><td>HTTP headers scoped to origin</td></tr>\n    <tr><td><code>--executable-path</code></td><td>Custom browser executable</td></tr>\n    <tr><td><code>--args &lt;args&gt;</code></td><td>Browser launch args (comma-separated)</td></tr>\n    <tr><td><code>--user-agent &lt;ua&gt;</code></td><td>Custom User-Agent string</td></tr>\n    <tr><td><code>--proxy &lt;url&gt;</code></td><td>Proxy server URL</td></tr>\n    <tr><td><code>--proxy-bypass &lt;hosts&gt;</code></td><td>Hosts to bypass proxy</td></tr>\n    <tr><td><code>--json</code></td><td>JSON output for scripts</td></tr>\n    <tr><td><code>--name, -n</code></td><td>Locator name filter</td></tr>\n    <tr><td><code>--exact</code></td><td>Exact text match</td></tr>\n    <tr><td><code>--headed</code></td><td>Show browser window</td></tr>\n    <tr><td><code>{\"--cdp <port|url>\"}</code></td><td>CDP connection (port or WebSocket URL)</td></tr>\n    <tr><td><code>--auto-connect</code></td><td>Auto-discover and connect to running Chrome</td></tr>\n    <tr><td><code>--color-scheme &lt;scheme&gt;</code></td><td>Persistent color scheme (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>\n    <tr><td><code>--debug</code></td><td>Debug output</td></tr>\n  </tbody>\n</table>\n\n## Cloud providers\n\nUse the `-p` flag to connect to a cloud browser provider instead of launching a local browser:\n\n```bash\nagent-browser -p browserbase open https://example.com\n```\n\nSee the [Providers](/providers/browser-use) section for setup and configuration of each supported provider: [Browser Use](/providers/browser-use), [Browserbase](/providers/browserbase), [Browserless](/providers/browserless), and [Kernel](/providers/kernel).\n"
  },
  {
    "path": "docs/src/app/changelog/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"changelog\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/changelog/page.mdx",
    "content": "# Changelog\n\n## v0.21.0\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### New Commands\n\n- **`batch`** -- Execute multiple commands in a single invocation. Pipe a JSON array of string arrays to stdin and receive results sequentially. Supports `--bail` to stop on first error and `--json` for structured output.\n\n```bash\necho '[[\"open\",\"example.com\"],[\"snapshot\"]]' | agent-browser batch --json\n```\n\n- **`network har start/stop`** -- Capture and export network traffic in HAR 1.2 format.\n\n```bash\nagent-browser network har start\n# ... interact with the page ...\nagent-browser network har stop ./trace.har\n```\n\n### New Features\n\n- **iframe support** -- CLI interactions and snapshots now traverse into iframe content, enabling automation of cross-frame pages.\n- **`--idle-timeout` flag** -- Automatically shut down the daemon after a period of inactivity. Accepts human-friendly formats such as `10s`, `3m`, `1h`, or raw milliseconds. Also available as `AGENT_BROWSER_IDLE_TIMEOUT_MS`.\n- **Cursor-interactive elements in snapshots** -- Cursor-interactive elements are now embedded directly into the snapshot tree for richer context.\n- **Brave Browser auto-connect** -- Auto-discovery of Brave Browser for CDP connections on macOS, Linux, and Windows.\n- **linux-musl (Alpine) builds** -- Pre-built binaries for linux-musl targeting both x64 and arm64, enabling native support for Alpine Linux and other musl-based distributions.\n- **WebSocket fallback for CDP discovery** -- When HTTP-based CDP endpoint discovery fails, the CLI now falls back to a WebSocket connection automatically.\n\n### Improvements\n\n- **`--full`/`-f` refactored to command-level flag** -- Moved from a global flag to a per-command flag for clearer scoping.\n- **Enhanced Chrome launch** -- Added `--user-data-dir` support and configurable launch timeout for more reliable browser startup. Chrome now retries launching up to 3 times on transient startup failures.\n- **Consecutive `--auto-connect` commands** -- Multiple consecutive auto-connect commands no longer require a full browser relaunch; external connections are correctly identified and reused.\n- **Batched CDP calls** -- `snapshot -C` and `screenshot --annotate` now batch CDP calls instead of issuing sequential round-trips per element, preventing timeouts on high-latency WSS connections.\n\n### Bug Fixes\n\n- Fixed remote CDP (WSS) snapshot and screenshot hangs by removing WebSocket message/frame size limits\n- Fixed Material Design `check`/`uncheck` falling back to JS `.click()` for overlay-based controls\n- Fixed punctuation characters being dropped in the `type` command\n- Fixed WebSocket streaming by keeping the StreamServer instance alive\n- Filtered internal Chrome targets (`chrome://`, `devtools://`) from auto-connect discovery\n- Fixed `snapshot --selector` scoping to the matched element's subtree\n- Fixed network idle detection returning prematurely for cached pages\n- Fixed daemon panic on broken stderr pipe during Chrome launch\n- Fixed broadcast channel lag being treated as stream closure\n- Fixed daemon liveness detection for PID namespace isolation (e.g. `unshare`)\n- Fixed Ubuntu dependency install accidentally removing system packages\n\n---\n\n## v0.20.0\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### Full Native Rust\n\nagent-browser is now 100% native Rust. The Node.js/Playwright daemon has been completely removed -- no Node.js runtime or Playwright dependency is required to run the daemon. The Rust native daemon is now the only implementation.\n\n<table>\n  <thead>\n    <tr>\n      <th>Metric</th>\n      <th>Node.js</th>\n      <th>Rust</th>\n      <th></th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>Cold start</td>\n      <td>1002ms</td>\n      <td>617ms</td>\n      <td>1.6x faster</td>\n    </tr>\n    <tr>\n      <td>Daemon memory</td>\n      <td>143 MB</td>\n      <td>8 MB</td>\n      <td>18x less</td>\n    </tr>\n    <tr>\n      <td>Install size</td>\n      <td>710 MB</td>\n      <td>7 MB</td>\n      <td>99x smaller</td>\n    </tr>\n  </tbody>\n</table>\n\n```bash\nnpm install -g agent-browser      # 7 MB install\nagent-browser install              # download Chrome\nagent-browser open example.com\nagent-browser snapshot\n```\n\n### Improvements\n\n- **Benchmarks** -- Added benchmark suite for comparing native vs Node.js daemon performance across cold start, warm start, memory, and install size.\n- **Chromium installer hardened** -- Fixed zip path traversal vulnerability in Chrome for Testing installer.\n\n### Bug Fixes\n\n- Fixed `--headed false` flag not being respected in CLI\n- Fixed \"not found\" error pattern in `to_ai_friendly_error` incorrectly catching non-element errors\n- Fixed storage local key lookup parsing and text output\n- Fixed Lightpanda engine launch with release binaries\n- Hardened Lightpanda startup timeouts\n\n---\n\n## v0.19.0\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### New Features\n\n- **Browserless.io provider** -- Added browserless.io as a browser provider, supported in both Node.js and native daemon paths. Connect to remote Browserless instances using the `--provider browserless` flag or `AGENT_BROWSER_PROVIDER=browserless` environment variable.\n\n```bash\nexport BROWSERLESS_API_KEY=\"your-api-key\"\nagent-browser --provider browserless open example.com\nagent-browser --provider browserless screenshot ./page.png\n```\n\n- **`clipboard` command** -- Read from and write to the browser clipboard. Supports `read`, `write`, `copy` (simulates Ctrl+C), and `paste` (simulates Ctrl+V) operations.\n\n```bash\nagent-browser clipboard read\nagent-browser clipboard write \"Hello, World!\"\nagent-browser clipboard copy\nagent-browser clipboard paste\n```\n\n- **Screenshot output configuration** -- New global flags for persistent screenshot settings: `--screenshot-dir`, `--screenshot-quality`, and `--screenshot-format`. Also available as environment variables `AGENT_BROWSER_SCREENSHOT_DIR`, `AGENT_BROWSER_SCREENSHOT_QUALITY`, and `AGENT_BROWSER_SCREENSHOT_FORMAT`.\n\n```bash\nagent-browser screenshot --screenshot-dir ./shots\nagent-browser screenshot --screenshot-format jpeg --screenshot-quality 80\n```\n\n### Bug Fixes\n\n- Fixed `wait --text` not working in native daemon path\n- Fixed `BrowserManager.navigate()` and package entry point\n- Fixed extensions not being loaded from `config.json`\n- Fixed scroll on page load\n- Fixed HTML retrieval by using `browser.getLocator()` for selector operations\n\n---\n\n## v0.18.0\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### New Features\n\n- **`inspect` command** -- Opens Chrome DevTools for the active page by launching a local proxy server that forwards the DevTools frontend to the browser's CDP WebSocket. Agent commands continue to work while DevTools is open.\n\n```bash\nagent-browser open example.com\nagent-browser inspect          # opens DevTools in your browser\nagent-browser click \"Submit\"   # commands still work while DevTools is open\n```\n\n- **`get cdp-url` subcommand** -- Retrieve the Chrome DevTools Protocol WebSocket URL for the active page, useful for connecting external debugging tools.\n\n```bash\nagent-browser get cdp-url\n```\n\n- **Screenshot annotate** -- The `--annotate` flag overlays numbered labels on interactive elements in screenshots.\n\n### Improvements\n\n- **KERNEL_API_KEY now optional** -- External credential injection no longer requires `KERNEL_API_KEY` to be set, making it easier to use Kernel with pre-configured environments.\n- **Browserbase simplified** -- Removed the `BROWSERBASE_PROJECT_ID` requirement, reducing setup friction for Browserbase users.\n\n### Bug Fixes\n\n- Fixed Browserbase API using incorrect endpoint to release sessions\n- Fixed CDP connect paths using hardcoded 10s timeout instead of the configurable default timeout\n- Fixed lone Unicode surrogates causing errors by sanitizing with `toWellFormed()`\n- Fixed CDP connection failure on IPv6-first systems\n- Fixed recordings not inheriting the current viewport settings\n\n---\n\n## v0.17.1\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### Improvements\n\n- **Viewport scale factor** -- Added support for device scale factor (retina display) in the viewport command via an optional `scale` parameter.\n- **Webview target support** -- Added webview target type support for better Electron application compatibility. The pages list now includes target type information.\n\n---\n\n## v0.17.0\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### New Features\n\n- **Lightpanda browser engine support** -- Added `--engine <name>` flag to select the browser engine (`chrome` by default, or `lightpanda`), implying `--native` mode. Configurable via `AGENT_BROWSER_ENGINE` environment variable.\n\n```bash\nagent-browser --engine lightpanda open example.com\n```\n\n- **Dialog dismiss command** -- Added support for `dismiss` subcommand in dialog command parsing.\n\n### Improvements\n\n- **Daemon startup error reporting** -- Daemon startup errors are now surfaced directly instead of showing an opaque timeout message.\n- **CDP port discovery** -- Replaced hand-rolled HTTP client with `reqwest` for more reliable CDP port discovery.\n- **Chrome extensions** -- Extensions now load correctly by forcing headed mode when extensions are present.\n- **Google Translate bar suppression** -- Suppressed the Google Translate bar in native headless mode to avoid interference.\n- **Auth cookie persistence** -- Auth cookies are now persisted on browser close in native mode.\n\n### Bug Fixes\n\n- Fixed native auth login failing due to incompatible encryption format.\n\n### Performance\n\n- Added benchmarks to the CLI codebase.\n\n---\n\n## v0.16.0\n\n<p className=\"text-[#888] text-sm\">March 2026</p>\n\n### New Features\n\n- **Native Rust daemon (experimental).** A pure Rust daemon that communicates with Chrome directly via the Chrome DevTools Protocol (CDP), eliminating Node.js and Playwright dependencies entirely. Enable with `--native`, `AGENT_BROWSER_NATIVE=1`, or `\"native\": true` in your config file. Supports 150+ commands with full parity to the default Node.js daemon.\n\n```bash\n# Via flag\nagent-browser --native open example.com\n\n# Via environment variable\nexport AGENT_BROWSER_NATIVE=1\nagent-browser open example.com\n```\n\nOr add to `agent-browser.json`:\n\n```json\n{\"native\": true}\n```\n\n### Architecture\n\n<table>\n  <thead>\n    <tr><th></th><th>Default (Node.js)</th><th>Native (<code>--native</code>)</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><strong>Runtime</strong></td><td>Node.js + Playwright</td><td>Pure Rust binary</td></tr>\n    <tr><td><strong>Protocol</strong></td><td>Playwright protocol</td><td>Direct CDP / WebDriver</td></tr>\n    <tr><td><strong>Install size</strong></td><td>Larger (Node.js + npm deps)</td><td>Smaller (single binary)</td></tr>\n    <tr><td><strong>Browser support</strong></td><td>Chromium, Firefox, WebKit</td><td>Chromium, Safari (via WebDriver)</td></tr>\n    <tr><td><strong>Stability</strong></td><td>Stable</td><td>Experimental</td></tr>\n  </tbody>\n</table>\n\n### What's Supported\n\nAll core commands work in native mode: navigation, interaction (click, fill, type, press, hover, scroll, drag), observation (snapshot, screenshot, eval), state management (cookies, storage, state save/load), tabs, emulation (viewport, device, timezone, locale, geolocation), streaming, diffing, recording, and profiling.\n\nThe native daemon also includes a WebDriver backend for Safari and iOS support.\n\n### Known Limitations\n\n- Firefox and WebKit are not yet supported (Chromium and Safari only)\n- Playwright trace format is not available (uses Chrome's built-in tracing)\n- HAR export is not available\n- Network route interception uses CDP Fetch domain instead of Playwright's route API\n- The native and Node.js daemons share the same session socket. Use `agent-browser close` before switching between modes.\n\nSee the [Native Mode](/native-mode) page for full details.\n\n---\n\n## v0.15.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **Authentication vault** -- Store credentials locally (always AES-256-GCM encrypted) and reference them by name. The LLM never sees passwords. Commands: `auth save`, `auth login`, `auth list`, `auth show`, `auth delete`. Passwords can be piped via stdin (`--password-stdin`) to avoid shell history exposure.\n- **Content boundary markers** -- `--content-boundaries` wraps page-sourced output in structural delimiters with a per-process CSPRNG nonce, so LLMs can distinguish trusted tool output from untrusted page content. In `--json` mode, a `_boundary` object is injected with `nonce` and `origin` fields.\n- **Domain allowlist** -- `--allowed-domains` restricts navigation, sub-resource requests, WebSocket connections, and EventSource streams to trusted domains. Supports exact match and wildcard prefix patterns (e.g., `*.example.com`).\n- **Action policy** -- `--action-policy` gates actions using a static JSON policy file with `allow`/`deny` lists across 13 action categories. Auth vault operations bypass policy enforcement.\n- **Action confirmation** -- `--confirm-actions` requires explicit approval for sensitive action categories. New `confirm` and `deny` commands for orchestrator use. `--confirm-interactive` enables human-in-the-loop terminal prompts (auto-denies if stdin is not a TTY). Pending confirmations auto-deny after 60 seconds.\n- **Output length limits** -- `--max-output` truncates large page outputs to prevent LLM context flooding.\n- **`--download-path` option** -- Set a default download directory via flag, `AGENT_BROWSER_DOWNLOAD_PATH` env var, or `downloadPath` config key. Without it, downloads go to a temporary directory deleted when the browser closes.\n- **`--selector` flag for scroll** -- Scroll within a specific container element instead of the page: `agent-browser scroll down 500 --selector \"div.scroll-container\"`\n\n```bash\n# Auth vault\necho \"pass\" | agent-browser auth save github --url https://github.com/login --username user --password-stdin\nagent-browser auth login github\n\n# Security flags\nagent-browser --content-boundaries --allowed-domains \"example.com,*.example.com\" --max-output 50000 open https://example.com\n\n# Download path\nagent-browser --download-path ./downloads open https://example.com\n\n# Scroll within container\nagent-browser scroll down 500 --selector \"div.content\"\n```\n\n### Environment Variables\n\nSix new environment variables for security configuration: `AGENT_BROWSER_CONTENT_BOUNDARIES`, `AGENT_BROWSER_MAX_OUTPUT`, `AGENT_BROWSER_ALLOWED_DOMAINS`, `AGENT_BROWSER_ACTION_POLICY`, `AGENT_BROWSER_CONFIRM_ACTIONS`, `AGENT_BROWSER_CONFIRM_INTERACTIVE`.\n\n---\n\n## v0.14.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **`keyboard` command** -- Type with real keystrokes, insert text, and press shortcuts at the currently focused element without needing a selector (`keyboard type`, `keyboard inserttext`).\n- **`--color-scheme` flag** -- Persistent dark/light mode preference across browser sessions via flag or `AGENT_BROWSER_COLOR_SCHEME` env var.\n\n```bash\nagent-browser keyboard type \"Hello world\"\nagent-browser keyboard inserttext \"pasted text\"\nagent-browser --color-scheme dark open https://example.com\n```\n\n### Bug Fixes\n\n- Fixed IPC EAGAIN errors (os error 35/11) with backpressure-aware socket writes, command serialization, and lowered default Playwright timeout to 25s (configurable via `AGENT_BROWSER_DEFAULT_TIMEOUT`).\n- Fixed remote debugging (CDP) reconnection.\n- Fixed state load failing when no browser is running.\n- Fixed `--annotate` flag warning appearing when not explicitly passed via CLI.\n\n---\n\n## v0.13.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **Diff commands** -- Compare snapshots, screenshots, and URLs between page states. Run visual pixel diffs against baseline images, compare accessibility tree snapshots with customizable depth and selectors, and diff two URLs side-by-side with optional screenshot comparison.\n\n```bash\nagent-browser diff snapshot\nagent-browser diff screenshot --baseline before.png\nagent-browser diff url https://staging.example.com https://prod.example.com\n```\n\n---\n\n## v0.12.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **Annotated screenshots** -- `--annotate` flag overlays numbered labels on interactive elements and prints a legend mapping each label to its element ref. Enables multimodal AI models to reason about visual layout while using the same `@eN` refs for subsequent interactions. Also settable via `AGENT_BROWSER_ANNOTATE` env var.\n\n```bash\nagent-browser screenshot --annotate\n```\n\n---\n\n## v0.11.1\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### Documentation\n\n- Added documentation for command chaining with `&&` across README, CLI help output, docs, and skill files.\n\n---\n\n## v0.11.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **Configuration file support** -- Automatic loading from user (`~/.agent-browser/config.json`) and project (`./agent-browser.json`) directories with priority-based merging.\n- **Profiler commands** -- Chrome DevTools profiling with `profiler start` and `profiler stop`.\n- **Browser extension loading** -- `--extension` flag to load browser extensions.\n- **Storage state management** -- `state save` and `state load` commands for auth state persistence.\n- **iOS device emulation** -- `--device` flag for device emulation.\n- **Enhanced click** -- `--new-tab` option for click commands.\n- **Enhanced find** -- Additional actions and filtering options.\n- **CDP WebSocket URLs** -- `--cdp` now accepts WebSocket URLs in addition to ports.\n\n---\n\n## v0.10.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **Session persistence** - Automatic save/restore of cookies and localStorage across browser restarts using `--session-name` flag\n- **Encrypted state** - Optional AES-256-GCM encryption for saved session state data\n- **State management commands** - New commands for listing, showing, renaming, clearing, and cleaning up session state files\n- **New tab on click** - Added `--new-tab` option for click commands to open links in new tabs\n\n```bash\n# Persist session state\nagent-browser --session-name myapp open https://example.com\n\n# Manage saved states\nagent-browser state list\nagent-browser state show myapp\nagent-browser state clear myapp\n```\n\n---\n\n## v0.9.4\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### Bug Fixes\n\n- Fixed all Clippy lint warnings in the Rust CLI\n\n---\n\n## v0.9.3\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### Improvements\n\n- Added support for custom executable path in CLI browser launch options\n- Documentation site UI improvements including a new chat component with sheet-based interface\n\n---\n\n## v0.9.2\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### Improvements\n\n- Migrated documentation site to MDX for improved content authoring\n- Added AI-powered docs chat feature\n- Updated README with Homebrew installation instructions for macOS users\n\n---\n\n## v0.9.1\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **`--allow-file-access` flag** - Enable opening and interacting with local `file://` URLs (PDFs, HTML files) by passing Chromium flags that allow JavaScript access to local files\n- **`-C`/`--cursor` flag for snapshots** - Include cursor-interactive elements like divs with onclick handlers or `cursor:pointer` styles\n\n```bash\nagent-browser --allow-file-access open file:///path/to/document.pdf\nagent-browser snapshot -C\n```\n\n---\n\n## v0.9.0\n\n<p className=\"text-[#888] text-sm\">February 2026</p>\n\n### New Features\n\n- **iOS Simulator support** - Mobile Safari testing via Appium with real device and simulator support\n\n```bash\n# List available iOS simulators\nagent-browser device list\n\n# Launch on iOS device\nagent-browser -p ios --device \"iPhone 16 Pro\" open https://example.com\n\n# Touch interactions\nagent-browser tap @e1\nagent-browser swipe up\n```\n\n---\n\n## v0.8.10\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Improvements\n\n- Added `--stdin` flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts\n- Fixed binary permission issues on macOS/Linux when postinstall scripts don't run\n\n---\n\n## v0.8.9\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Improvements\n\n- Added `--stdin` flag for eval command to read JavaScript from stdin\n\n---\n\n## v0.8.8\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Improvements\n\n- Added base64 encoding support for the eval command with `-b`/`--base64` flag to avoid shell escaping issues\n- Updated documentation with AI agent setup instructions\n\n---\n\n## v0.8.7\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Bug Fixes\n\n- Fixed browser launch options not being passed correctly when using persistent profiles\n- Added pre-flight checks for socket path length limits and directory write permissions\n- Improved error handling to properly exit with failure status when browser launch fails\n\n---\n\n## v0.8.6\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Bug Fixes\n\n- Improved daemon connection reliability with automatic retry logic for transient errors\n- CLI now cleans up stale socket and PID files before starting a new daemon\n\n---\n\n## v0.8.5\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Bug Fixes\n\n- Fixed version synchronization to automatically update Cargo.lock alongside Cargo.toml during releases\n- Made the CLI binary executable in the npm package\n\n---\n\n## v0.8.4\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Bug Fixes\n\n- Fixed \"Daemon not found\" error when running through AI agents by resolving symlinks in the executable path\n\n---\n\n## v0.8.3\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Improvements\n\n- Replaced shell-based CLI wrappers with a cross-platform Node.js wrapper to enable npx support on Windows\n- Added postinstall logic to patch npm bin entry on global installs for zero-overhead native binary invocation\n- Added CI tests to verify global installation across all platforms\n\n---\n\n## v0.8.2\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Bug Fixes\n\n- Fixed the Windows CMD wrapper to use the native binary directly instead of routing through Node.js\n- Added retry logic to CI install command for transient browser installation failures\n\n---\n\n## v0.8.1\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Improvements\n\n- Improved release workflow to validate binary file sizes and ensure binaries are executable after npm install\n- Updated documentation site with a new mobile navigation system\n\n---\n\n## v0.8.0\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### New Features\n\n- **Kernel cloud browser provider** - Connect to Kernel (kernel.sh) for remote browser infrastructure with stealth mode and persistent profiles\n\n```bash\n# Via -p flag\nagent-browser -p kernel open https://example.com\n\n# Via environment variable\nexport AGENT_BROWSER_PROVIDER=kernel\nexport KERNEL_API_KEY=your-api-key\nagent-browser open https://example.com\n\n# With persistent profile\nexport KERNEL_PROFILE_NAME=my-profile\nagent-browser open https://example.com\n```\n\n- **Ignore HTTPS certificate errors** - New flag for working with self-signed certificates and development environments\n\n```bash\nagent-browser --ignore-https-errors open https://localhost:3000\n```\n\n- **Enhanced cookie management** - Extended `cookies set` command with additional flags for setting cookies before page load\n\n```bash\nagent-browser cookies set session_id \"abc123\" --url https://app.example.com --httpOnly --secure\nagent-browser cookies set token \"xyz\" --domain .example.com --path /api --expires 1735689600\n```\n\n### Bug Fixes\n\n- Fixed tab list command not recognizing new pages opened via clicks or `target=\"_blank\"` links\n- Fixed `check` command hanging indefinitely\n- Fixed `set device` not applying deviceScaleFactor - HiDPI screenshots now work correctly\n- Fixed state load and profile persistence not working in v0.7.6\n- Screenshots now save to temp directory when no path is provided\n\n### Security\n\n- Daemon and stream server now reject cross-origin connections\n\n---\n\n## v0.7.1\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### Bug Fixes\n\n- **Fix native binary distribution** - Native binaries for all platforms (Linux x64/arm64, macOS x64/arm64, Windows x64) are now included in the npm package. Previously, the release workflow published to npm before building binaries, causing \"No binary found\" errors on installation.\n\n---\n\n## v0.7.0\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### New Features\n\n- **Cloud browser providers** - Connect to Browserbase or Browser Use for remote browser infrastructure\n\n```bash\n# Via -p flag (recommended)\nagent-browser -p browserbase open https://example.com\nagent-browser -p browseruse open https://example.com\n\n# Via environment variable\nexport AGENT_BROWSER_PROVIDER=browserbase\nagent-browser open https://example.com\n```\n\n- **Persistent browser profiles** - Store cookies, localStorage, and login sessions across browser restarts\n\n```bash\nagent-browser --profile ~/.myapp-profile open myapp.com\n# Login persists across restarts\n```\n\n- **Remote CDP WebSocket URLs** - Connect to remote browser services via WebSocket\n\n```bash\nagent-browser --cdp \"wss://browser-service.com/cdp?token=...\" snapshot\n```\n\n- **`download` command** - Trigger downloads and wait for completion\n\n```bash\nagent-browser download @e1 ./file.pdf\nagent-browser wait --download ./output.zip --timeout 30000\n```\n\n- **Browser launch configuration** - Fine-grained control over browser startup\n\n```bash\nagent-browser --args \"--disable-gpu,--no-sandbox\" open example.com\nagent-browser --user-agent \"Custom UA\" open example.com\nagent-browser --proxy-bypass \"localhost,*.internal\" open example.com\n```\n\n- **Enhanced skills** - Hierarchical structure with references and templates for Claude Code\n\n### Bug Fixes\n\n- Screenshot command now supports refs and has improved error messages\n- WebSocket URLs work in `connect` command\n- Fixed socket file location (uses `~/.agent-browser` instead of TMPDIR)\n- Windows binary path fix (.exe extension)\n- State load and path-based actions now show correct output messages\n\n### Documentation\n\n- Added Claude Code marketplace plugin installation instructions\n- Updated skill documentation with references and templates\n- Improved error documentation\n\n---\n\n## v0.6.0\n\n<p className=\"text-[#888] text-sm\">January 2026</p>\n\n### New Features\n\n- **Video recording** - Record browser sessions to WebM using Playwright's native recording\n\n```bash\nagent-browser record start ./demo.webm\nagent-browser click @e1\nagent-browser record stop\n```\n\n- **`connect` command** - Connect to a browser via CDP and persist the connection for subsequent commands\n\n```bash\nagent-browser connect 9222\nagent-browser snapshot  # No --cdp needed after connect\n```\n\n- **`--proxy` flag** - Configure browser proxy with optional authentication\n\n```bash\nagent-browser --proxy http://user:pass@proxy.com:8080 open example.com\n```\n\n- **`get styles` command** - Extract computed styles from elements\n\n```bash\nagent-browser get styles \"button\"\n```\n\n- **Claude marketplace plugin** - Added `.claude-plugin/marketplace.json` for Claude Code integration\n- **Enhanced network output** - `network requests` now shows method, URL, and resource type\n- **`--version` flag** - Display CLI version\n\n### Bug Fixes\n\n- Fix Windows daemon startup and port calculation\n- Support `libasound2t64` on newer Ubuntu versions (24.04+)\n- Prevent CDP timeout on empty URL tabs\n- Output screenshot as base64 when no path provided\n- Resolve refs in `get value` command\n- Support URL parameter in `tab new` command\n- Allow `about:`, `data:`, and `file:` URL schemes\n- Detect stale unix socket by attempting connection\n- Respect `AGENT_BROWSER_HEADED` environment variable\n- Handle SIGPIPE to prevent panic when piping to `head`/`tail`\n- Fix null path validation in screenshot command\n\n### Protocol Alignment\n\nThese changes align the CLI with the daemon protocol for consistency:\n\n- `select` command now uses `values` field (supports multiple selections)\n- `frame main` uses `mainframe` action\n- `mouse wheel` uses `wheel` action\n- `set media` uses `emulatemedia` action\n- Console output uses `messages` field\n\n### Documentation\n\n- Expanded SKILL.md with comprehensive command reference\n- Updated README with new commands and options\n- Updated CDP mode documentation with `connect` workflow\n"
  },
  {
    "path": "docs/src/app/commands/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"commands\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/commands/page.mdx",
    "content": "# Commands\n\n## Core\n\n```bash\nagent-browser open <url>              # Navigate (aliases: goto, navigate)\nagent-browser click <sel>             # Click element (--new-tab to open in new tab)\nagent-browser dblclick <sel>          # Double-click\nagent-browser fill <sel> <text>       # Clear and fill\nagent-browser type <sel> <text>       # Type into element\nagent-browser press <key>             # Press key (Enter, Tab, Control+a) (alias: key)\nagent-browser keyboard type <text>    # Type at current focus (no selector needed)\nagent-browser keyboard inserttext <text>  # Insert text without key events\nagent-browser keydown <key>           # Hold key down\nagent-browser keyup <key>             # Release key\nagent-browser hover <sel>             # Hover element\nagent-browser focus <sel>             # Focus element\nagent-browser select <sel> <val>      # Select dropdown option\nagent-browser check <sel>             # Check checkbox\nagent-browser uncheck <sel>           # Uncheck checkbox\nagent-browser scroll <dir> [px]       # Scroll (up/down/left/right, --selector <sel>)\nagent-browser scrollintoview <sel>    # Scroll element into view\nagent-browser drag <src> <dst>        # Drag and drop\nagent-browser upload <sel> <files>    # Upload files\nagent-browser screenshot [path]       # Screenshot (--full for full page)\nagent-browser screenshot --annotate   # Annotated screenshot with numbered element labels\nagent-browser screenshot --screenshot-dir ./shots    # Save to custom directory\nagent-browser screenshot --screenshot-format jpeg --screenshot-quality 80\nagent-browser pdf <path>              # Save page as PDF\nagent-browser snapshot                # Accessibility tree with refs\nagent-browser eval <js>               # Run JavaScript\nagent-browser connect <port|url>      # Connect to browser via CDP\nagent-browser close                   # Close browser (aliases: quit, exit)\n```\n\n## Get info\n\n```bash\nagent-browser get text <sel>          # Get text content\nagent-browser get html <sel>          # Get innerHTML\nagent-browser get value <sel>         # Get input value\nagent-browser get attr <sel> <attr>   # Get attribute\nagent-browser get title               # Get page title\nagent-browser get url                 # Get current URL\nagent-browser get cdp-url             # Get CDP WebSocket URL\nagent-browser get count <sel>         # Count matching elements\nagent-browser get box <sel>           # Get bounding box\nagent-browser get styles <sel>        # Get computed styles\n```\n\n## Check state\n\n```bash\nagent-browser is visible <sel>        # Check if visible\nagent-browser is enabled <sel>        # Check if enabled\nagent-browser is checked <sel>        # Check if checked\n```\n\n## Find elements\n\nSemantic locators with actions (`click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`):\n\n```bash\nagent-browser find role <role> <action> [value]\nagent-browser find text <text> <action>\nagent-browser find label <label> <action> [value]\nagent-browser find placeholder <ph> <action> [value]\nagent-browser find alt <text> <action>\nagent-browser find title <text> <action>\nagent-browser find testid <id> <action> [value]\nagent-browser find first <sel> <action> [value]\nagent-browser find last <sel> <action> [value]\nagent-browser find nth <n> <sel> <action> [value]\n```\n\nOptions:\n\n- `--name <name>` -- filter role by accessible name\n- `--exact` -- require exact text match\n\nExamples:\n\n```bash\nagent-browser find role button click --name \"Submit\"\nagent-browser find label \"Email\" fill \"test@test.com\"\nagent-browser find alt \"Logo\" click\nagent-browser find first \".item\" click\nagent-browser find last \".item\" text\nagent-browser find nth 2 \".card\" hover\n```\n\n## Wait\n\n```bash\nagent-browser wait <selector>         # Wait for element\nagent-browser wait <ms>               # Wait for time\nagent-browser wait --text \"Welcome\"   # Wait for text (substring match)\nagent-browser wait --url \"**/dash\"    # Wait for URL pattern\nagent-browser wait --load networkidle # Wait for load state\nagent-browser wait --fn \"condition\"   # Wait for JS condition\nagent-browser wait --download [path]  # Wait for download\nagent-browser wait --fn \"!document.body.innerText.includes('Loading...')\"  # Wait for text to disappear\nagent-browser wait \"#spinner\" --state hidden           # Wait for element to disappear\n```\n\n## Downloads\n\n```bash\nagent-browser download <sel> <path>   # Click element to trigger download\nagent-browser wait --download [path]  # Wait for any download to complete\n```\n\nUse `--download-path <dir>` (or `AGENT_BROWSER_DOWNLOAD_PATH` env) to set a default download directory. Without it, downloads go to a temporary directory that is deleted when the browser closes.\n\n## Mouse\n\n```bash\nagent-browser mouse move <x> <y>      # Move mouse\nagent-browser mouse down [button]     # Press button\nagent-browser mouse up [button]       # Release button\nagent-browser mouse wheel <dy> [dx]   # Scroll wheel\n```\n\n## Clipboard\n\n```bash\nagent-browser clipboard read                      # Read text from clipboard\nagent-browser clipboard write \"Hello, World!\"     # Write text to clipboard\nagent-browser clipboard copy                      # Copy current selection (Ctrl+C)\nagent-browser clipboard paste                     # Paste from clipboard (Ctrl+V)\n```\n\n## Settings\n\n```bash\nagent-browser set viewport <w> <h> [scale]  # Set viewport size (scale for retina, e.g. 2)\nagent-browser set device <name>       # Emulate device (\"iPhone 14\")\nagent-browser set geo <lat> <lng>     # Set geolocation\nagent-browser set offline [on|off]    # Toggle offline mode\nagent-browser set headers <json>      # Extra HTTP headers\nagent-browser set credentials <u> <p> # HTTP basic auth\nagent-browser set media [dark|light]  # Emulate color scheme (persists for session)\n```\n\nUse `--color-scheme` for persistent dark/light mode across all commands:\n\n```bash\nagent-browser --color-scheme dark open https://example.com\n```\n\n## Cookies & storage\n\n```bash\nagent-browser cookies                 # Get all cookies\nagent-browser cookies set <name> <val> # Set cookie\nagent-browser cookies clear           # Clear cookies\n\nagent-browser storage local           # Get all localStorage\nagent-browser storage local <key>     # Get specific key\nagent-browser storage local set <k> <v>  # Set value\nagent-browser storage local clear     # Clear all\n\nagent-browser storage session         # Same for sessionStorage\n```\n\n## Network\n\n```bash\nagent-browser network route <url>              # Intercept requests\nagent-browser network route <url> --abort      # Block requests\nagent-browser network route <url> --body <json>  # Mock response\nagent-browser network unroute [url]            # Remove routes\nagent-browser network requests                 # View tracked requests\nagent-browser network requests --clear         # Clear request log\nagent-browser network requests --filter <pat>  # Filter by URL pattern\nagent-browser network har start                # Start HAR recording\nagent-browser network har stop [output.har]    # Stop and save HAR (temp path if omitted)\n```\n\n## Tabs & frames\n\n```bash\nagent-browser tab                     # List tabs\nagent-browser tab new [url]           # New tab\nagent-browser tab <n>                 # Switch to tab\nagent-browser tab close [n]           # Close tab\nagent-browser window new              # Open new browser window\nagent-browser frame <sel>             # Switch to iframe by CSS selector\nagent-browser frame @e3               # Switch to iframe by element ref\nagent-browser frame main              # Back to main frame\n```\n\n### Iframe support\n\nIframes are detected automatically during snapshots. `Iframe` nodes are resolved and their content is\ninlined beneath the iframe element in the snapshot output. Refs assigned to elements inside iframes carry\nframe context, so `click`, `fill`, and other interactions work without manually switching frames.\n\n```bash\nagent-browser snapshot -i\n# @e3 [Iframe] \"payment-frame\"\n#   @e4 [input] \"Card number\"\n#   @e5 [button] \"Pay\"\n\n# Interact directly using refs — no frame switch needed\nagent-browser fill @e4 \"4111111111111111\"\nagent-browser click @e5\n\n# Or switch frame context for scoped snapshots\nagent-browser frame @e3\nagent-browser snapshot -i             # Only elements inside that iframe\nagent-browser frame main              # Return to main frame\n```\n\nThe `frame` command accepts element refs (`@e3`), CSS selectors (`\"#my-iframe\"`), or frame name/URL.\n\n## Dialogs\n\n```bash\nagent-browser dialog accept [text]    # Accept dialog (with optional prompt text)\nagent-browser dialog dismiss          # Dismiss dialog\n```\n\n## Debug\n\n```bash\nagent-browser trace start [path]      # Start trace\nagent-browser trace stop [path]       # Stop and save trace\nagent-browser profiler start          # Start Chrome DevTools profiling\nagent-browser profiler stop [path]    # Stop and save profile (.json)\nagent-browser record start <path>     # Start video recording (WebM)\nagent-browser record stop             # Stop and save video\nagent-browser record restart <path>   # Stop current and start new recording\nagent-browser console                 # View console messages\nagent-browser console --clear         # Clear console log\nagent-browser errors                  # View page errors\nagent-browser errors --clear          # Clear error log\nagent-browser highlight <sel>         # Highlight element\nagent-browser inspect                 # Open Chrome DevTools for the active page\n```\n\n## Auth vault\n\n```bash\nagent-browser auth save <name> [opts]    # Save auth profile\nagent-browser auth login <name>          # Login using saved credentials\nagent-browser auth list                  # List saved profiles (names and URLs only)\nagent-browser auth show <name>           # Show profile metadata (no passwords)\nagent-browser auth delete <name>         # Delete a saved profile\n```\n\nSave options:\n\n- `--url <url>` -- login page URL (required)\n- `--username <user>` -- username (required)\n- `--password <pass>` -- password (required unless `--password-stdin`)\n- `--password-stdin` -- read password from stdin (recommended to avoid shell history exposure)\n- `--username-selector <sel>` -- custom CSS selector for username field\n- `--password-selector <sel>` -- custom CSS selector for password field\n- `--submit-selector <sel>` -- custom CSS selector for submit button\n\n```bash\necho \"pass\" | agent-browser auth save github --url https://github.com/login --username user --password-stdin\nagent-browser auth login github\nagent-browser auth list\n```\n\n## Confirmation\n\nWhen `--confirm-actions` is set, certain action categories return a `confirmation_required` response instead of executing immediately. Use `confirm` or `deny` to approve or reject the action.\n\n```bash\nagent-browser confirm <confirmation-id>  # Approve a pending action\nagent-browser deny <confirmation-id>     # Deny a pending action\n```\n\nPending confirmations auto-deny after 60 seconds.\n\n```bash\nagent-browser --confirm-actions eval,download eval \"document.title\"\n# Returns confirmation_required with ID\nagent-browser confirm c_8f3a1234\n```\n\n## State management\n\n```bash\nagent-browser state save <path>       # Save auth state to file\nagent-browser state load <path>       # Load auth state from file\nagent-browser state list              # List saved state files\nagent-browser state show <file>       # Show state summary\nagent-browser state rename <old> <new> # Rename state file\nagent-browser state clear [name]      # Clear states for session name\nagent-browser state clear --all       # Clear all saved states\nagent-browser state clean --older-than <days>  # Delete old states\n```\n\n## Sessions\n\n```bash\nagent-browser session                 # Show current session name\nagent-browser session list            # List active sessions\n```\n\n## Navigation\n\n```bash\nagent-browser back                    # Go back\nagent-browser forward                 # Go forward\nagent-browser reload                  # Reload page\n```\n\n## Global options\n\n```bash\n--session <name>         # Isolated browser session\n--session-name <name>    # Auto-save/restore session state (cookies, localStorage)\n--profile <path>         # Persistent browser profile directory\n--state <path>           # Load storage state from JSON file\n--headers <json>         # HTTP headers scoped to URL's origin\n--executable-path <path> # Custom browser executable\n--extension <path>       # Load browser extension (repeatable)\n--args <args>            # Browser launch args (comma separated)\n--user-agent <ua>        # Custom User-Agent string\n--proxy <url>            # Proxy server URL\n--proxy-bypass <hosts>   # Hosts to bypass proxy\n--ignore-https-errors    # Ignore HTTPS certificate errors\n--allow-file-access      # Allow file:// URLs to access local files (Chromium only)\n-p, --provider <name>    # Browser provider (ios, browserbase, kernel, browseruse, browserless)\n--device <name>          # iOS device name (e.g., \"iPhone 15 Pro\")\n--json                   # JSON output (for scripts)\n--annotate               # Annotated screenshot with numbered element labels\n--screenshot-dir <path>   # Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)\n--screenshot-quality <n>  # JPEG quality 0-100 (or AGENT_BROWSER_SCREENSHOT_QUALITY)\n--screenshot-format <fmt> # Format: png (default), jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)\n--headed                 # Show browser window (not headless)\n--cdp <port|url>         # Connect via Chrome DevTools Protocol (port or WebSocket URL)\n--auto-connect           # Auto-discover and connect to running Chrome\n--color-scheme <scheme>  # Color scheme: dark, light, no-preference\n--download-path <path>   # Default download directory\n--content-boundaries     # Wrap page output in boundary markers for LLM safety\n--max-output <chars>     # Truncate page output to N characters\n--allowed-domains <list> # Comma-separated allowed domain patterns\n--action-policy <path>   # Path to action policy JSON file\n--confirm-actions <list> # Action categories requiring confirmation\n--confirm-interactive    # Interactive confirmation prompts (auto-denies if stdin is not a TTY)\n--config <path>          # Use a custom config file\n--debug                  # Debug output\n```\n\n## Batch execution\n\nExecute multiple commands in a single invocation by piping a JSON array of string arrays to `batch`:\n\n```bash\necho '[\n  [\"open\", \"https://example.com\"],\n  [\"snapshot\", \"-i\"],\n  [\"click\", \"@e1\"],\n  [\"screenshot\", \"result.png\"]\n]' | agent-browser batch --json\n\n# Stop on first error\nagent-browser batch --bail < commands.json\n```\n\n<table>\n  <thead>\n    <tr><th>Option</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>--bail</code></td><td>Stop on first error (default: continue all commands)</td></tr>\n    <tr><td><code>--json</code></td><td>Output results as a JSON array</td></tr>\n  </tbody>\n</table>\n\n## Command chaining\n\nChain commands with `&&` in a single shell invocation. The browser persists via a background daemon, so chaining works naturally and is more efficient than separate calls:\n\n```bash\nagent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i\nagent-browser fill @e1 \"user@example.com\" && agent-browser fill @e2 \"pass\" && agent-browser click @e3\nagent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png\n```\n\nUse `&&` when you don't need to read intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs, then interact with those refs).\n\n## Local files\n\nOpen local files (PDFs, HTML) using `file://` URLs:\n\n```bash\nagent-browser --allow-file-access open file:///path/to/document.pdf\nagent-browser --allow-file-access open file:///path/to/page.html\nagent-browser screenshot output.png\n```\n\nThe `--allow-file-access` flag enables JavaScript to access other local files. Chromium only.\n"
  },
  {
    "path": "docs/src/app/configuration/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"configuration\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/configuration/page.mdx",
    "content": "# Configuration\n\nCreate an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.\n\n## Config File Locations\n\nagent-browser checks two locations, merged in priority order:\n\n<table>\n  <thead>\n    <tr><th>Priority</th><th>Location</th><th>Scope</th></tr>\n  </thead>\n  <tbody>\n    <tr><td>1 (lowest)</td><td><code>~/.agent-browser/config.json</code></td><td>User-level defaults</td></tr>\n    <tr><td>2</td><td><code>./agent-browser.json</code></td><td>Project-level overrides</td></tr>\n    <tr><td>3</td><td><code>AGENT_BROWSER_*</code> env vars</td><td>Override config values</td></tr>\n    <tr><td>4 (highest)</td><td>CLI flags</td><td>Override everything</td></tr>\n  </tbody>\n</table>\n\nProject-level values override user-level values. Environment variables override both. CLI flags always win.\n\nUse `--config <path>` or the `AGENT_BROWSER_CONFIG` environment variable to load a specific config file instead of the default locations:\n\n```bash\nagent-browser --config ./ci-config.json open example.com\nAGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com\n```\n\n## Example Config\n\n```json\n{\n  \"headed\": true,\n  \"proxy\": \"http://localhost:8080\",\n  \"profile\": \"./browser-data\",\n  \"userAgent\": \"my-agent/1.0\",\n  \"ignoreHttpsErrors\": true\n}\n```\n\n## All Options\n\nEvery CLI flag can be set in the config file using its camelCase equivalent:\n\n<table>\n  <thead>\n    <tr><th>Config Key</th><th>CLI Flag</th><th>Type</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>headed</code></td><td><code>--headed</code></td><td>boolean</td></tr>\n    <tr><td><code>json</code></td><td><code>--json</code></td><td>boolean</td></tr>\n    <tr><td><code>full</code></td><td><code>--full, -f</code></td><td>boolean</td></tr>\n    <tr><td><code>debug</code></td><td><code>--debug</code></td><td>boolean</td></tr>\n    <tr><td><code>session</code></td><td><code>--session</code></td><td>string</td></tr>\n    <tr><td><code>sessionName</code></td><td><code>--session-name</code></td><td>string</td></tr>\n    <tr><td><code>executablePath</code></td><td><code>--executable-path</code></td><td>string</td></tr>\n    <tr><td><code>extensions</code></td><td><code>--extension</code></td><td>string[]</td></tr>\n    <tr><td><code>profile</code></td><td><code>--profile</code></td><td>string</td></tr>\n    <tr><td><code>state</code></td><td><code>--state</code></td><td>string</td></tr>\n    <tr><td><code>proxy</code></td><td><code>--proxy</code></td><td>string</td></tr>\n    <tr><td><code>proxyBypass</code></td><td><code>--proxy-bypass</code></td><td>string</td></tr>\n    <tr><td><code>args</code></td><td><code>--args</code></td><td>string</td></tr>\n    <tr><td><code>userAgent</code></td><td><code>--user-agent</code></td><td>string</td></tr>\n    <tr><td><code>provider</code></td><td><code>-p, --provider</code></td><td>string</td></tr>\n    <tr><td><code>device</code></td><td><code>--device</code></td><td>string</td></tr>\n    <tr><td><code>ignoreHttpsErrors</code></td><td><code>--ignore-https-errors</code></td><td>boolean</td></tr>\n    <tr><td><code>allowFileAccess</code></td><td><code>--allow-file-access</code></td><td>boolean</td></tr>\n    <tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>\n    <tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>\n    <tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>\n    <tr><td><code>downloadPath</code></td><td><code>--download-path</code></td><td>string</td></tr>\n    <tr><td><code>contentBoundaries</code></td><td><code>--content-boundaries</code></td><td>boolean</td></tr>\n    <tr><td><code>maxOutput</code></td><td><code>--max-output</code></td><td>number</td></tr>\n    <tr><td><code>allowedDomains</code></td><td><code>--allowed-domains</code></td><td>string[]</td></tr>\n    <tr><td><code>actionPolicy</code></td><td><code>--action-policy</code></td><td>string</td></tr>\n    <tr><td><code>confirmActions</code></td><td><code>--confirm-actions</code></td><td>string</td></tr>\n    <tr><td><code>confirmInteractive</code></td><td><code>--confirm-interactive</code></td><td>boolean</td></tr>\n    <tr><td><code>engine</code></td><td><code>--engine</code></td><td>string (<code>chrome</code>, <code>lightpanda</code>)</td></tr>\n    <tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>\n  </tbody>\n</table>\n\n## Common Configurations\n\n### Local Development\n\n```json\n{\n  \"headed\": true,\n  \"profile\": \"./browser-data\"\n}\n```\n\n### Behind a Proxy\n\n```json\n{\n  \"proxy\": \"http://proxy.corp.example.com:8080\",\n  \"proxyBypass\": \"localhost,*.internal.com\",\n  \"ignoreHttpsErrors\": true\n}\n```\n\n### CI / Devcontainer\n\n```json\n{\n  \"args\": \"--no-sandbox,--disable-gpu\",\n  \"ignoreHttpsErrors\": true\n}\n```\n\n### iOS Testing\n\n```json\n{\n  \"provider\": \"ios\",\n  \"device\": \"iPhone 16 Pro\"\n}\n```\n\n### AI Agent Security\n\n```json\n{\n  \"contentBoundaries\": true,\n  \"maxOutput\": 50000,\n  \"allowedDomains\": [\"your-app.com\", \"*.your-app.com\"],\n  \"actionPolicy\": \"./policy.json\"\n}\n```\n\n## Overriding Boolean Options\n\nBoolean flags accept an optional `true`/`false` value to override config settings:\n\n```bash\nagent-browser --headed false open example.com\n```\n\nA bare flag is equivalent to passing `true`:\n\n```bash\nagent-browser --headed open example.com       # same as --headed true\nagent-browser --headed true open example.com  # explicit\n```\n\nThis applies to all boolean flags: `--headed`, `--debug`, `--json`, `--ignore-https-errors`, `--allow-file-access`, `--auto-connect`, `--content-boundaries`, `--confirm-interactive`.\n\n## Extensions Merging\n\nExtensions from user-level and project-level configs are **concatenated**, not replaced. For example, if `~/.agent-browser/config.json` specifies `[\"/ext1\"]` and `./agent-browser.json` specifies `[\"/ext2\"]`, the result is `[\"/ext1\", \"/ext2\"]`.\n\nThe `AGENT_BROWSER_EXTENSIONS` environment variable and CLI `--extension` flags follow the standard priority rules (env replaces config, CLI appends).\n\n## Environment Variables\n\nThese environment variables configure additional daemon and runtime behavior:\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th><th>Default</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_DOWNLOAD_PATH</code></td><td>Default directory for browser downloads.</td><td>(temp directory)</td></tr>\n    <tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>\n    <tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>\n    <tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM session encryption.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_EXTENSIONS</code></td><td>Comma-separated browser extension paths. Extensions work in both headed and headless mode.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_HEADED</code></td><td>Show browser window instead of running headless (<code>1</code> to enable).</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_STREAM_PORT</code></td><td>Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_IDLE_TIMEOUT_MS</code></td><td>Auto-shutdown the daemon after N ms of inactivity (no commands received). Useful for ephemeral environments.</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Default iOS device name for the <code>ios</code> provider.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Default iOS device UDID for the <code>ios</code> provider.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_DEBUG</code></td><td>Enable debug output (<code>1</code> to enable).</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_CONTENT_BOUNDARIES</code></td><td>Wrap page output in boundary markers for LLM safety.</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_MAX_OUTPUT</code></td><td>Max characters for page output (truncates beyond limit).</td><td>(unlimited)</td></tr>\n    <tr><td><code>AGENT_BROWSER_ALLOWED_DOMAINS</code></td><td>Comma-separated allowed domain patterns (e.g., <code>example.com,*.example.com</code>).</td><td>(unrestricted)</td></tr>\n    <tr><td><code>AGENT_BROWSER_ACTION_POLICY</code></td><td>Path to action policy JSON file.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_CONFIRM_ACTIONS</code></td><td>Comma-separated action categories requiring confirmation.</td><td>(none)</td></tr>\n    <tr><td><code>AGENT_BROWSER_CONFIRM_INTERACTIVE</code></td><td>Enable interactive confirmation prompts (auto-denies if stdin is not a TTY).</td><td>(disabled)</td></tr>\n    <tr><td><code>AGENT_BROWSER_ENGINE</code></td><td>Browser engine to use: <code>chrome</code> (default), <code>lightpanda</code>.</td><td><code>chrome</code></td></tr>\n  </tbody>\n</table>\n\n## Error Handling\n\n- **Auto-discovered config files** (`~/.agent-browser/config.json`, `./agent-browser.json`) that are missing are silently ignored.\n- **`--config <path>`** with a missing or malformed file exits with an error.\n- **Malformed JSON** in auto-discovered files prints a warning to stderr and continues without that file.\n- **Unknown keys** are silently ignored for forward compatibility.\n\n> **Tip:** If your project-level `agent-browser.json` contains environment-specific values (paths, proxies), consider adding it to `.gitignore`.\n"
  },
  {
    "path": "docs/src/app/diffing/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"diffing\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/diffing/page.mdx",
    "content": "import { DiffDemo } from \"@/components/diff-demo\"\n\n# Diffing\n\nCompare page states to detect changes -- structurally via accessibility tree snapshots, visually via pixel comparison, or across two different URLs.\n\n<DiffDemo />\n\n## Commands\n\n<table>\n  <thead>\n    <tr><th>Command</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>diff snapshot</code></td><td>Compare current snapshot to last snapshot in session</td></tr>\n    <tr><td><code>diff snapshot --baseline &lt;file&gt;</code></td><td>Compare current snapshot to a saved file</td></tr>\n    <tr><td><code>diff screenshot --baseline &lt;file&gt;</code></td><td>Visual pixel diff against a baseline image</td></tr>\n    <tr><td><code>diff url &lt;url1&gt; &lt;url2&gt;</code></td><td>Compare two pages (snapshot + optional screenshot)</td></tr>\n  </tbody>\n</table>\n\n## Snapshot diff\n\nCompares the accessibility tree between two points in time using a line-level text diff.\n\n```bash\n# Compare against the last snapshot taken in this session\nagent-browser diff snapshot\n\n# Compare against a saved baseline file\nagent-browser diff snapshot --baseline before.txt\n\n# Scope to a specific part of the page\nagent-browser diff snapshot --selector \"#main\" --compact\n```\n\nWithout `--baseline`, the command automatically compares against the most recent snapshot taken in the current session. This is the primary use case for agents verifying that an action had the intended effect.\n\n### Options\n\n<table>\n  <thead>\n    <tr><th>Flag</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>-b, --baseline &lt;file&gt;</code></td><td>Path to a saved snapshot file to compare against</td></tr>\n    <tr><td><code>-s, --selector &lt;sel&gt;</code></td><td>Scope the current snapshot to a CSS selector or @ref</td></tr>\n    <tr><td><code>-c, --compact</code></td><td>Use compact snapshot format</td></tr>\n    <tr><td><code>-d, --depth &lt;n&gt;</code></td><td>Limit snapshot tree depth</td></tr>\n  </tbody>\n</table>\n\n### Output\n\nThe diff uses `+` for added lines and `-` for removed lines, similar to unified diff format. A summary line shows the count of additions, removals, and unchanged lines.\n\n```\n- button \"Submit\" [ref=e2]\n+ button \"Submit\" [ref=e2] [disabled]\n  3 additions, 2 removals, 41 unchanged\n```\n\n## Screenshot diff\n\nCompares the current page screenshot against a baseline image at the pixel level. Produces a diff image with changed pixels highlighted in red.\n\n```bash\n# Basic visual diff\nagent-browser diff screenshot --baseline before.png\n\n# Save diff image to a specific path\nagent-browser diff screenshot --baseline before.png --output diff.png\n\n# Adjust threshold and scope to element\nagent-browser diff screenshot --baseline before.png --threshold 0.2 --selector \"#hero\"\n```\n\n### Options\n\n<table>\n  <thead>\n    <tr><th>Flag</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>-b, --baseline &lt;file&gt;</code></td><td>Baseline PNG/JPEG image to compare against (required)</td></tr>\n    <tr><td><code>-o, --output &lt;file&gt;</code></td><td>Path for the generated diff image (default: temp dir)</td></tr>\n    <tr><td><code>-t, --threshold &lt;0-1&gt;</code></td><td>Color distance threshold (default: 0.1). Higher = more tolerant</td></tr>\n    <tr><td><code>-s, --selector &lt;sel&gt;</code></td><td>Scope the current screenshot to an element</td></tr>\n    <tr><td><code>--full</code></td><td>Take a full-page screenshot</td></tr>\n  </tbody>\n</table>\n\n### Output\n\nReports the diff image path, number of different pixels, and mismatch percentage. The diff image shows unchanged pixels dimmed with changed pixels in red.\n\nIf the baseline and current images have different dimensions, the command reports a dimension mismatch instead of attempting pixel comparison.\n\n## URL diff\n\nCompares two pages by navigating to each in sequence and diffing the results.\n\n```bash\n# Compare two URLs (snapshot diff)\nagent-browser diff url https://staging.example.com https://prod.example.com\n\n# Include visual comparison\nagent-browser diff url https://v1.example.com https://v2.example.com --screenshot\n\n# Full-page screenshot comparison\nagent-browser diff url https://v1.example.com https://v2.example.com --screenshot --full\n```\n\nThe command navigates to the first URL, captures state, then navigates to the second URL and captures again. Snapshot diff is always included. Screenshot diff requires the `--screenshot` flag.\n\nAfter completion, the browser remains on the second URL.\n\n### Options\n\n<table>\n  <thead>\n    <tr><th>Flag</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>--screenshot</code></td><td>Also perform visual screenshot comparison</td></tr>\n    <tr><td><code>--full</code></td><td>Use full-page screenshots</td></tr>\n    <tr><td><code>--wait-until &lt;strategy&gt;</code></td><td>Navigation wait strategy: <code>load</code>, <code>domcontentloaded</code>, <code>networkidle</code> (default: <code>load</code>)</td></tr>\n    <tr><td><code>-s, --selector &lt;sel&gt;</code></td><td>Scope snapshots to a CSS selector or @ref</td></tr>\n    <tr><td><code>-c, --compact</code></td><td>Use compact snapshot format</td></tr>\n    <tr><td><code>-d, --depth &lt;n&gt;</code></td><td>Limit snapshot tree depth</td></tr>\n  </tbody>\n</table>\n\n## Use cases\n\n### Verifying agent actions\n\nThe most common use case: confirm that an action (click, fill, submit) changed the page as expected.\n\n```bash\nagent-browser snapshot -i          # Take interactive-only snapshot (baseline)\nagent-browser fill @e3 \"test@example.com\"\nagent-browser diff snapshot        # Compare current snapshot to the baseline\n```\n\n### Monitoring for changes\n\nPeriodically compare a page against a saved baseline to detect updates.\n\n```bash\n# Save baseline\nagent-browser open https://example.com && agent-browser snapshot > baseline.txt\n\n# Later, check for changes\nagent-browser open https://example.com && agent-browser diff snapshot --baseline baseline.txt\n```\n\n### Visual regression testing\n\nCompare screenshots before and after a deploy to catch unintended visual changes.\n\n```bash\nagent-browser open https://staging.example.com && agent-browser screenshot baseline.png\n# ... deploy happens ...\nagent-browser open https://staging.example.com && agent-browser diff screenshot --baseline baseline.png\n```\n\n### Comparing environments\n\nDiff staging against production to verify parity.\n\n```bash\nagent-browser diff url https://staging.example.com https://prod.example.com --screenshot\n```\n"
  },
  {
    "path": "docs/src/app/engines/chrome/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"engines/chrome\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/engines/chrome/page.mdx",
    "content": "# Chrome\n\nChrome (and Chromium) is the default browser engine. agent-browser discovers, launches, and manages the Chrome process automatically via the Chrome DevTools Protocol (CDP).\n\n## Binary Discovery\n\nWhen no `--executable-path` is provided, agent-browser searches for Chrome in this order:\n\n<table>\n  <thead>\n    <tr><th>Platform</th><th>Locations checked</th></tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>macOS</td>\n      <td>\n        <code>/Applications/Google Chrome.app</code>,\n        <code>/Applications/Google Chrome Canary.app</code>,\n        <code>/Applications/Chromium.app</code>,\n        Chrome for Testing cache\n      </td>\n    </tr>\n    <tr>\n      <td>Linux</td>\n      <td>\n        <code>google-chrome</code>,\n        <code>google-chrome-stable</code>,\n        <code>chromium-browser</code>,\n        <code>chromium</code> in PATH,\n        Chrome for Testing cache\n      </td>\n    </tr>\n    <tr>\n      <td>Windows</td>\n      <td>\n        <code>%LOCALAPPDATA%\\Google\\Chrome\\Application\\chrome.exe</code>,\n        <code>C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe</code>,\n        <code>C:\\Program Files (x86)\\...\\chrome.exe</code>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\nIf Chrome is not found, run `agent-browser install` to download Chrome from Chrome for Testing.\n\n## Usage\n\nChrome is the default engine -- no `--engine` flag is needed:\n\n```bash\nagent-browser open example.com\n```\n\nTo be explicit:\n\n```bash\nagent-browser --engine chrome open example.com\n```\n\n## Custom Binary\n\nPoint to any Chromium-based browser with `--executable-path`:\n\n```bash\nagent-browser --executable-path /path/to/chromium open example.com\n```\n\nOr via environment variable:\n\n```bash\nexport AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium\nagent-browser open example.com\n```\n\n## Chrome-Specific Features\n\nThese features are available only with Chrome:\n\n<table>\n  <thead>\n    <tr><th>Feature</th><th>Flag</th></tr>\n  </thead>\n  <tbody>\n    <tr><td>Browser extensions</td><td><code>--extension &lt;path&gt;</code></td></tr>\n    <tr><td>Persistent profiles</td><td><code>--profile &lt;path&gt;</code> (sets Chrome's <code>--user-data-dir</code>)</td></tr>\n    <tr><td>Storage state</td><td><code>--state &lt;path&gt;</code></td></tr>\n    <tr><td>File URL access</td><td><code>--allow-file-access</code></td></tr>\n    <tr><td>Headed mode</td><td><code>--headed</code></td></tr>\n    <tr><td>Custom launch args</td><td><code>--args &lt;args&gt;</code></td></tr>\n  </tbody>\n</table>\n\n## Containers and CI\n\nIn Docker, CI runners, or other sandboxed environments, Chrome's user namespace sandbox may need to be disabled:\n\n```bash\nagent-browser --args \"--no-sandbox\" open example.com\n```\n\nagent-browser automatically adds `--no-sandbox` when it detects a container environment (Docker, Podman, running as root).\n"
  },
  {
    "path": "docs/src/app/engines/lightpanda/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"engines/lightpanda\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/engines/lightpanda/page.mdx",
    "content": "# Lightpanda\n\n[Lightpanda](https://lightpanda.io/) is a headless browser engine built from scratch in Zig for machines. It starts instantly, uses 10x less memory than Chrome, and executes 10x faster.\n\nagent-browser manages Lightpanda the same way it manages Chrome -- spawning the process, connecting via CDP, and shutting it down. All downstream commands (snapshot, click, fill, screenshot, etc.) work through the same CDP protocol path.\n\n## Installation\n\nInstall the Lightpanda binary before using it with agent-browser:\n\n<table>\n  <thead>\n    <tr><th>Platform</th><th>Command</th></tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>macOS (Apple Silicon)</td>\n      <td><code>curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && chmod a+x ./lightpanda</code></td>\n    </tr>\n    <tr>\n      <td>Linux (x86_64)</td>\n      <td><code>curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && chmod a+x ./lightpanda</code></td>\n    </tr>\n  </tbody>\n</table>\n\nMove the binary somewhere in your `PATH` (e.g. `/usr/local/bin/lightpanda` or `~/.local/bin/lightpanda`).\n\nSee the [Lightpanda installation docs](https://lightpanda.io/docs/open-source/installation) for more options.\n\n## Usage\n\nUse the `--engine` flag to select Lightpanda:\n\n```bash\nagent-browser --engine lightpanda open example.com\nagent-browser --engine lightpanda snapshot\nagent-browser --engine lightpanda screenshot\n```\n\nOr set it as the default via environment variable:\n\n```bash\nexport AGENT_BROWSER_ENGINE=lightpanda\nagent-browser open example.com\n```\n\nOr in your `agent-browser.json` config:\n\n```json\n{\n  \"engine\": \"lightpanda\"\n}\n```\n\n## Custom Binary Path\n\nIf the `lightpanda` binary is not in your `PATH`, use `--executable-path`:\n\n```bash\nagent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com\n```\n\n## Differences from Chrome\n\nLightpanda is a purpose-built headless engine. Some Chrome-specific features are not available:\n\n<table>\n  <thead>\n    <tr><th>Feature</th><th>Status</th></tr>\n  </thead>\n  <tbody>\n    <tr><td>Extensions (<code>--extension</code>)</td><td>Not supported</td></tr>\n    <tr><td>Persistent profiles (<code>--profile</code>)</td><td>Not supported</td></tr>\n    <tr><td>Storage state (<code>--state</code>)</td><td>Not supported</td></tr>\n    <tr><td>File access (<code>--allow-file-access</code>)</td><td>Not supported</td></tr>\n    <tr><td>Headed mode (<code>--headed</code>)</td><td>Not applicable (headless only)</td></tr>\n    <tr><td>Screenshots</td><td>Depends on Lightpanda CDP support</td></tr>\n  </tbody>\n</table>\n\nagent-browser returns a clear error if you combine `--engine lightpanda` with unsupported flags.\n\n## When to Use Lightpanda\n\nLightpanda is a good fit for:\n\n- Fast web scraping and data extraction\n- AI agent workflows where speed and low memory matter\n- CI/CD environments with constrained resources\n- High-volume parallel automation\n\nUse Chrome when you need full browser fidelity, extensions, or persistent profiles.\n"
  },
  {
    "path": "docs/src/app/globals.css",
    "content": "@import \"tailwindcss\";\n@plugin \"tailwindcss-animate\";\n\n@source \"../../node_modules/streamdown/dist/index.js\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n@theme {\n  --font-sans: \"Inter\", ui-sans-serif, system-ui, -apple-system, sans-serif;\n  --font-mono: var(--font-geist-mono), ui-monospace, \"SF Mono\", \"Cascadia Mono\", \"Segoe UI Mono\", Menlo, Consolas, monospace;\n\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-border: var(--border);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-sidebar: var(--sidebar);\n}\n\n:root {\n  --background: #fff;\n  --foreground: #171717;\n  --border: #e5e5e5;\n  --muted: #f5f5f5;\n  --muted-foreground: #737373;\n  --primary: #171717;\n  --primary-foreground: #fff;\n  --sidebar: #f5f5f5;\n}\n\n.dark {\n  --background: #0a0a0a;\n  --foreground: #f5f5f5;\n  --border: #262626;\n  --muted: #262626;\n  --muted-foreground: #a3a3a3;\n  --primary: #f5f5f5;\n  --primary-foreground: #0a0a0a;\n  --sidebar: #171717;\n}\n\nhtml {\n  scroll-padding-top: 4rem;\n}\n\n::selection {\n  background-color: #000;\n  color: #fff;\n}\n\n@media (prefers-color-scheme: dark) {\n  ::selection {\n    background-color: #fff;\n    color: #000;\n  }\n}\n\n/* Article tables */\narticle table {\n  width: 100%;\n  font-size: 0.875rem;\n  margin-bottom: 1rem;\n  border-collapse: collapse;\n}\n\narticle th {\n  border-bottom: 1px solid #e5e5e5;\n  padding: 0.5rem 0.75rem;\n  text-align: left;\n  font-size: 0.75rem;\n  font-weight: 500;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: #737373;\n}\n\narticle td {\n  border-bottom: 1px solid #f5f5f5;\n  padding: 0.5rem 0.75rem;\n  color: #525252;\n}\n\n:is(.dark) article th {\n  border-bottom-color: #262626;\n  color: #a3a3a3;\n}\n\n:is(.dark) article td {\n  border-bottom-color: rgba(38, 38, 38, 0.5);\n  color: #a3a3a3;\n}\n\nbutton {\n  cursor: pointer;\n}\n\n/* Code blocks */\npre {\n  border: 1px solid var(--border);\n  border-radius: 4px;\n  padding: 0.875rem;\n  overflow-x: auto;\n  font-size: 0.8125rem;\n  line-height: 1.7;\n}\n\npre:not(.shiki) {\n  background: var(--muted);\n}\n\n.code-block pre {\n  margin: 0;\n}\n\n.code-block {\n  margin-bottom: 1.25rem;\n}\n\n@media (max-width: 640px) {\n  pre {\n    font-size: 0.75rem;\n    padding: 0.75rem;\n  }\n}\n\n:not(pre) > code {\n  background: var(--muted);\n  padding: 0.125rem 0.375rem;\n  border-radius: 3px;\n  font-size: 0.875em;\n}\n\n/* Diff line highlighting */\n.diff-add {\n  color: #00952d;\n}\n\n.diff-remove {\n  color: #f32e40;\n}\n\n.dark .diff-add {\n  color: #00ca50;\n}\n\n.dark .diff-remove {\n  color: #f32e40;\n}\n\n/* Shiki dual theme support */\n.shiki,\n.shiki span {\n  color: var(--shiki-light) !important;\n  background-color: var(--shiki-light-bg) !important;\n}\n\n.dark .shiki,\n.dark .shiki span {\n  color: var(--shiki-dark) !important;\n  background-color: var(--shiki-dark-bg) !important;\n}\n\n/* Prose */\n.prose {\n  max-width: 100%;\n}\n\n.prose h1 {\n  font-size: 1.5rem;\n  font-weight: 600;\n  letter-spacing: -0.02em;\n  margin-bottom: 1.5rem;\n  color: var(--foreground);\n}\n\n@media (min-width: 640px) {\n  .prose h1 {\n    font-size: 1.75rem;\n  }\n}\n\n.prose h2 {\n  font-size: 1.125rem;\n  font-weight: 600;\n  margin-top: 3rem;\n  margin-bottom: 1rem;\n  color: var(--foreground);\n}\n\n.prose h2:first-child {\n  margin-top: 0;\n}\n\n.prose h3 {\n  font-size: 1rem;\n  font-weight: 600;\n  margin-top: 2rem;\n  margin-bottom: 0.75rem;\n  color: var(--foreground);\n}\n\n.prose p {\n  margin-bottom: 1rem;\n  line-height: 1.65;\n  color: #525252;\n  font-size: 0.875rem;\n}\n\n:is(.dark) .prose p {\n  color: #a3a3a3;\n}\n\n.prose ul, .prose ol {\n  margin-bottom: 1rem;\n  padding-left: 1.25rem;\n}\n\n.prose ul {\n  list-style-type: disc;\n}\n\n.prose ol {\n  list-style-type: decimal;\n}\n\n.prose li {\n  margin-bottom: 0.25rem;\n  color: #525252;\n  font-size: 0.875rem;\n  line-height: 1.6;\n}\n\n:is(.dark) .prose li {\n  color: #a3a3a3;\n}\n\n.prose li strong {\n  color: var(--foreground);\n  font-weight: 500;\n}\n\n.prose a {\n  color: var(--foreground);\n  text-decoration: underline;\n  text-decoration-color: #d4d4d4;\n  text-underline-offset: 2px;\n}\n\n.prose a:hover {\n  text-decoration-color: var(--foreground);\n}\n\n:is(.dark) .prose a {\n  text-decoration-color: #525252;\n}\n\n:is(.dark) .prose a:hover {\n  text-decoration-color: var(--foreground);\n}\n\n.prose strong {\n  font-weight: 500;\n  color: var(--foreground);\n}\n\n.prose blockquote {\n  margin-bottom: 1rem;\n  border-left: 2px solid #e5e5e5;\n  padding-left: 1rem;\n  font-size: 0.875rem;\n  color: #737373;\n}\n\n:is(.dark) .prose blockquote {\n  border-left-color: #525252;\n  color: #a3a3a3;\n}\n\n.prose table {\n  width: 100%;\n  border-collapse: collapse;\n  margin: 1.5rem 0;\n  font-size: 0.8125rem;\n}\n\n.prose th, .prose td {\n  text-align: left;\n  padding: 0.625rem 0.875rem;\n  border-bottom: 1px solid var(--border);\n}\n\n.prose th {\n  font-weight: 500;\n  color: var(--muted-foreground);\n  text-transform: uppercase;\n  font-size: 0.75rem;\n  letter-spacing: 0.025em;\n}\n\n.prose td {\n  color: var(--muted-foreground);\n}\n\n.prose td code {\n  color: var(--foreground);\n}\n\n/* Tool call shimmer animation */\n@keyframes tool-shimmer {\n  0% { opacity: 0.5; }\n  50% { opacity: 1; }\n  100% { opacity: 0.5; }\n}\n\n.animate-tool-shimmer {\n  animation: tool-shimmer 1.5s ease-in-out infinite;\n}\n\n/* Override prose text color in chat so agent responses use primary foreground */\n.docs-chat-content p,\n.docs-chat-content li,\n.docs-chat-content td,\n.docs-chat-content th,\n.docs-chat-content strong,\n.docs-chat-content code {\n  color: var(--foreground);\n}\n\n/* Reset global pre styles inside chat so Streamdown's own styling takes effect */\n.docs-chat-content pre {\n  border: none;\n  border-radius: 0;\n  padding: revert-layer;\n}\n\n/* Fix list rendering in chat content */\n.docs-chat-content ul,\n.docs-chat-content ol {\n  list-style-position: outside;\n  padding-left: 1.25em;\n}\n\n.docs-chat-content li > p {\n  display: inline;\n  margin: 0;\n}\n\n.docs-chat-content li {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n"
  },
  {
    "path": "docs/src/app/installation/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"installation\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/installation/page.mdx",
    "content": "# Installation\n\n## Global installation (recommended)\n\nInstalls the native Rust binary for maximum performance:\n\n```bash\nnpm install -g agent-browser\nagent-browser install  # Download Chrome from Chrome for Testing (first time)\n```\n\nThis is the fastest option -- commands run through the native Rust CLI directly with sub-millisecond parsing overhead.\n\n## Quick start (no install)\n\n```bash\nnpx agent-browser install   # Download Chrome (first time only)\nnpx agent-browser open example.com\n```\n\n## Project installation (local dependency)\n\nFor projects that want to pin the version in `package.json`:\n\n```bash\nnpm install agent-browser\nnpx agent-browser install  # Download Chrome (first time)\n```\n\nThen use via `npx` or `package.json` scripts.\n\n## Homebrew (macOS)\n\n```bash\nbrew install agent-browser\nagent-browser install  # Download Chrome (first time)\n```\n\n## Cargo (Rust)\n\n```bash\ncargo install agent-browser\nagent-browser install  # Download Chrome (first time)\n```\n\nCompiles from source (~2-3 min). Requires the Rust toolchain ([rustup.rs](https://rustup.rs)).\n\n## From source\n\n```bash\ngit clone https://github.com/vercel-labs/agent-browser\ncd agent-browser\npnpm install\npnpm build\npnpm build:native\n./bin/agent-browser install\npnpm link --global\n```\n\n## Linux dependencies\n\nOn Linux, install system dependencies:\n\n```bash\nagent-browser install --with-deps\n```\n\n## Updating\n\nUpgrade to the latest version:\n\n```bash\nagent-browser upgrade\n```\n\nDetects your installation method (npm, Homebrew, or Cargo) and runs the appropriate update command automatically. Displays the version change on success, or informs you if you are already on the latest version.\n\n## Custom browser\n\nUse a custom browser executable instead of bundled Chromium:\n\n- **Serverless** - Use `@sparticuz/chromium` (~50MB vs ~684MB)\n- **System browser** - Use existing Chrome installation\n- **Custom builds** - Use modified browser builds\n\n```bash\n# Via flag\nagent-browser --executable-path /path/to/chromium open example.com\n\n# Via environment variable\nAGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com\n```\n\n### Serverless example\n\nUse `@sparticuz/chromium` or similar to obtain a Chromium executable path, then pass it via `--executable-path` or `AGENT_BROWSER_EXECUTABLE_PATH`.\n\n## AI agent setup\n\nagent-browser works with any AI agent out of the box. For richer context:\n\n### AI coding assistants (recommended)\n\nInstall the skill for your AI coding assistant:\n\n```bash\nnpx skills add vercel-labs/agent-browser\n```\n\nThis works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf. The skill is fetched from the repository and stays up to date automatically.\n\n> **Do not** copy `SKILL.md` from `node_modules` -- it will become stale as new features are added. Always use `npx skills add` or reference the repository version.\n\n### AGENTS.md / CLAUDE.md\n\nAdd to your instructions file:\n\n```markdown\n## Browser Automation\n\nUse `agent-browser` for web automation. Run `agent-browser --help` for all commands.\n\nCore workflow:\n1. `agent-browser open <url>` - Navigate to page\n2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2)\n3. `agent-browser click @e1` / `fill @e2 \"text\"` - Interact using refs\n4. Re-snapshot after page changes\n```\n"
  },
  {
    "path": "docs/src/app/ios/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"ios\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/ios/page.mdx",
    "content": "# iOS Simulator\n\nControl real Mobile Safari in the iOS Simulator for authentic mobile\nweb testing. Uses Appium with XCUITest for native automation.\n\n## Requirements\n\n- macOS with Xcode installed\n- iOS Simulator runtimes (download via Xcode)\n- Appium with XCUITest driver\n\n## Setup\n\n```bash\n# Install Appium globally\nnpm install -g appium\n\n# Install the XCUITest driver for iOS\nappium driver install xcuitest\n```\n\n## List available devices\n\nSee all iOS simulators available on your system:\n\n```bash\nagent-browser device list\n\n# Output:\n# Available iOS Simulators:\n#\n#   ○ iPhone 16 Pro (iOS 18.0)\n#     F21EEC0D-7618-419F-811B-33AF27A8B2FD\n#   ○ iPhone 16 Pro Max (iOS 18.0)\n#     50402807-C9B8-4D37-9F13-2E00E782C744\n#   ○ iPad Pro 13-inch (M4) (iOS 18.0)\n#     3A6C6436-B909-4593-866D-91D1062BB070\n#   ...\n```\n\n## Basic usage\n\nUse the `-p ios` flag to enable iOS mode. The workflow is\nidentical to desktop:\n\n```bash\n# Launch Safari on iPhone 16 Pro\nagent-browser -p ios --device \"iPhone 16 Pro\" open https://example.com\n\n# Get snapshot with refs (same as desktop)\nagent-browser -p ios snapshot -i\n\n# Interact using refs\nagent-browser -p ios tap @e1\nagent-browser -p ios fill @e2 \"text\"\n\n# Take screenshot\nagent-browser -p ios screenshot mobile.png\n\n# Close session (shuts down simulator)\nagent-browser -p ios close\n```\n\n## Mobile-specific commands\n\n```bash\n# Swipe gestures\nagent-browser -p ios swipe up\nagent-browser -p ios swipe down\nagent-browser -p ios swipe left\nagent-browser -p ios swipe right\n\n# Swipe with distance (pixels)\nagent-browser -p ios swipe up 500\n\n# Tap (alias for click, semantically clearer for touch)\nagent-browser -p ios tap @e1\n```\n\n## Environment variables\n\nConfigure iOS mode via environment variables:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=ios\nexport AGENT_BROWSER_IOS_DEVICE=\"iPhone 16 Pro\"\n\n# Now all commands use iOS\nagent-browser open https://example.com\nagent-browser snapshot -i\nagent-browser tap @e1\n```\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>AGENT_BROWSER_PROVIDER</code></td><td>Set to <code>ios</code> to enable iOS mode</td></tr>\n    <tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Device name (e.g., \"iPhone 16 Pro\")</td></tr>\n    <tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Device UDID (alternative to device name)</td></tr>\n  </tbody>\n</table>\n\n## Supported devices\n\nAll iOS Simulators available in Xcode are supported, including:\n\n- All iPhone models (iPhone 15, 16, 17, SE, etc.)\n- All iPad models (iPad Pro, iPad Air, iPad mini, etc.)\n- Multiple iOS versions (17.x, 18.x, etc.)\n\n**Real devices** are also supported via USB connection (see below).\n\n## Real device support\n\nAppium can control Safari on real iOS devices connected via USB. This\nrequires additional one-time setup.\n\n### 1. Get your device UDID\n\n```bash\n# List connected devices\nxcrun xctrace list devices\n\n# Or via system profiler\nsystem_profiler SPUSBDataType | grep -A 5 \"iPhone\\|iPad\"\n```\n\n### 2. Sign WebDriverAgent (one-time)\n\nWebDriverAgent needs to be signed with your Apple Developer\ncertificate to run on real devices.\n\n```bash\n# Open the WebDriverAgent Xcode project\ncd ~/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent\nopen WebDriverAgent.xcodeproj\n```\n\nIn Xcode:\n\n1. Select the `WebDriverAgentRunner` target\n2. Go to Signing & Capabilities\n3. Select your Team (requires Apple Developer account, free tier works)\n4. Let Xcode manage signing automatically\n\n### 3. Use with agent-browser\n\n```bash\n# Connect device via USB, then use the UDID\nagent-browser -p ios --device \"<DEVICE_UDID>\" open https://example.com\n\n# Or use the device name if unique\nagent-browser -p ios --device \"John's iPhone\" open https://example.com\n```\n\n### Real device notes\n\n- First run installs WebDriverAgent to the device (may require Trust prompt on device)\n- Device must be unlocked and connected via USB\n- Slightly slower initial connection than simulator\n- Tests against real Safari performance and behavior\n- On first install, go to Settings → General → VPN & Device Management to trust the developer certificate\n\n## Performance notes\n\n- **First launch:** Takes 30-60 seconds to boot the simulator and start Appium\n- **Subsequent commands:** Fast (simulator stays running)\n- **Close command:** Shuts down simulator and Appium server\n\n## Differences from desktop\n\n<table>\n  <thead>\n    <tr><th>Feature</th><th>Desktop</th><th>iOS</th></tr>\n  </thead>\n  <tbody>\n    <tr><td>Browser</td><td>Chrome, Lightpanda</td><td>Safari only</td></tr>\n    <tr><td>Tabs</td><td>Supported</td><td>Single tab only</td></tr>\n    <tr><td>PDF export</td><td>Supported</td><td>Not supported</td></tr>\n    <tr><td>Screencast</td><td>Supported</td><td>Not supported</td></tr>\n    <tr><td>Swipe gestures</td><td>Not native</td><td>Native support</td></tr>\n  </tbody>\n</table>\n\n## Troubleshooting\n\n### Appium not found\n\n```bash\n# Make sure Appium is installed globally\nnpm install -g appium\nappium driver install xcuitest\n\n# Verify installation\nappium --version\n```\n\n### No simulators available\n\nOpen Xcode and download iOS Simulator runtimes from **Settings → Platforms**.\n\n### Simulator won't boot\n\nTry booting the simulator manually from Xcode or the Simulator app to\nensure it works, then retry with agent-browser.\n"
  },
  {
    "path": "docs/src/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter, Geist_Mono } from \"next/font/google\";\nimport { GeistPixelSquare } from \"geist/font/pixel\";\nimport \"./globals.css\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Header } from \"@/components/header\";\nimport { DocsSidebar } from \"@/components/docs-sidebar\";\nimport { DocsMobileNav } from \"@/components/docs-mobile-nav\";\nimport { CopyPageButton } from \"@/components/copy-page-button\";\nimport { DocsChat } from \"@/components/docs-chat\";\nimport { cookies } from \"next/headers\";\nimport { SpeedInsights } from \"@vercel/speed-insights/next\";\nimport { Analytics } from \"@vercel/analytics/next\";\n\nconst inter = Inter({\n  variable: \"--font-inter\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(\"https://agent-browser.dev\"),\n  title: {\n    default: \"agent-browser | Headless Browser Automation for AI\",\n    template: \"%s | agent-browser\",\n  },\n  description: \"Headless browser automation CLI for AI agents\",\n  openGraph: {\n    type: \"website\",\n    locale: \"en_US\",\n    url: \"https://agent-browser.dev\",\n    siteName: \"agent-browser\",\n    title: \"agent-browser | Headless Browser Automation for AI\",\n    description: \"Headless browser automation CLI for AI agents\",\n    images: [{ url: \"/og\", width: 1200, height: 630, alt: \"agent-browser\" }],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"agent-browser | Headless Browser Automation for AI\",\n    description: \"Headless browser automation CLI for AI agents\",\n    images: [\"/og\"],\n  },\n};\n\nexport default async function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const cookieStore = await cookies();\n  const chatOpen = cookieStore.get(\"docs-chat-open\")?.value === \"true\";\n  const chatWidth = Number(cookieStore.get(\"docs-chat-width\")?.value) || 400;\n\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <head>\n        {chatOpen && (\n          <style\n            dangerouslySetInnerHTML={{\n              __html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,\n            }}\n          />\n        )}\n      </head>\n      <body\n        className={`${inter.variable} ${geistMono.variable} ${GeistPixelSquare.variable} bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100`}\n      >\n        <ThemeProvider>\n          <Header />\n          <DocsMobileNav />\n          <div className=\"max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16\">\n            <aside className=\"w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto\">\n              <DocsSidebar />\n            </aside>\n            <div className=\"flex-1 min-w-0 max-w-2xl pb-20\">\n              <div className=\"flex justify-end mb-4\">\n                <CopyPageButton />\n              </div>\n              <article className=\"prose\">{children}</article>\n            </div>\n          </div>\n          <DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />\n        </ThemeProvider>\n        <SpeedInsights />\n        <Analytics />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "docs/src/app/native-mode/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"native-mode\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/native-mode/page.mdx",
    "content": "# Native Mode\n\nagent-browser is now 100% native Rust by default. The Node.js/Playwright daemon has been removed.\n\nThis page is no longer relevant. See the main [documentation](/) for current architecture and usage.\n"
  },
  {
    "path": "docs/src/app/next/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"next\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/next/page.mdx",
    "content": "# Next.js + Vercel\n\nRun agent-browser from a Next.js app on Vercel using Vercel Sandbox.\nA Linux microVM spins up on demand, runs agent-browser + Chrome, and\nshuts down. No binary size limits, no Chromium bundling complexity.\n\n## Setup\n\n```bash\npnpm add @vercel/sandbox\n```\n\n## Server action\n\nThe Vercel Sandbox runs Amazon Linux. Chromium requires system libraries\nthat are not installed by default, so fresh sandboxes need a `dnf install`\nstep before agent-browser can launch Chrome. Use a sandbox snapshot\n(below) to skip this entirely in production.\n\n```ts\n\"use server\";\nimport { Sandbox } from \"@vercel/sandbox\";\n\nconst snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;\n\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\", \"nspr\", \"libxkbcommon\", \"atk\", \"at-spi2-atk\", \"at-spi2-core\",\n  \"libXcomposite\", \"libXdamage\", \"libXrandr\", \"libXfixes\", \"libXcursor\",\n  \"libXi\", \"libXtst\", \"libXScrnSaver\", \"libXext\", \"mesa-libgbm\", \"libdrm\",\n  \"mesa-libGL\", \"mesa-libEGL\", \"cups-libs\", \"alsa-lib\", \"pango\", \"cairo\",\n  \"gtk3\", \"dbus-libs\",\n];\n\nfunction getSandboxCredentials() {\n  if (\n    process.env.VERCEL_TOKEN &&\n    process.env.VERCEL_TEAM_ID &&\n    process.env.VERCEL_PROJECT_ID\n  ) {\n    return {\n      token: process.env.VERCEL_TOKEN,\n      teamId: process.env.VERCEL_TEAM_ID,\n      projectId: process.env.VERCEL_PROJECT_ID,\n    };\n  }\n  return {};\n}\n\nasync function withBrowser<T>(\n  fn: (sandbox: InstanceType<typeof Sandbox>) => Promise<T>,\n): Promise<T> {\n  const credentials = getSandboxCredentials();\n\n  const sandbox = snapshotId\n    ? await Sandbox.create({\n        ...credentials,\n        source: { type: \"snapshot\", snapshotId },\n        timeout: 120_000,\n      })\n    : await Sandbox.create({ ...credentials, runtime: \"node24\", timeout: 120_000 });\n\n  if (!snapshotId) {\n    await sandbox.runCommand(\"sh\", [\n      \"-c\",\n      `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n    ]);\n    await sandbox.runCommand(\"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n    await sandbox.runCommand(\"npx\", [\"agent-browser\", \"install\"]);\n  }\n\n  try {\n    return await fn(sandbox);\n  } finally {\n    await sandbox.stop();\n  }\n}\n\nexport async function screenshotUrl(url: string) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const ssResult = await sandbox.runCommand(\"agent-browser\", [\n      \"screenshot\", \"--json\",\n    ]);\n    const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;\n    const b64Result = await sandbox.runCommand(\"base64\", [\"-w\", \"0\", ssPath]);\n    const screenshot = (await b64Result.stdout()).trim();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n    return { ok: true, screenshot };\n  });\n}\n\nexport async function snapshotUrl(url: string) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const result = await sandbox.runCommand(\"agent-browser\", [\n      \"snapshot\", \"-i\", \"-c\",\n    ]);\n    const snapshot = await result.stdout();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n    return { ok: true, snapshot };\n  });\n}\n```\n\n## Sandbox snapshots\n\nWithout optimization, each Sandbox run installs system dependencies +\nagent-browser + Chromium from scratch (~30 seconds). A **sandbox snapshot**\nis a saved VM image with everything pre-installed -- like a Docker image\nfor Vercel Sandbox. When `AGENT_BROWSER_SNAPSHOT_ID` is set, the sandbox\nboots from that image instead of installing, bringing startup down to\nsub-second.\n\nThis is different from an agent-browser *accessibility snapshot* (which\ndumps a page's accessibility tree). A sandbox snapshot is a Vercel\ninfrastructure concept.\n\nCreate a sandbox snapshot by running the helper script once:\n\n```bash\nnpx tsx scripts/create-snapshot.ts\n```\n\nThe script spins up a fresh sandbox, installs system dependencies +\nagent-browser + Chromium, saves the VM state, and prints the snapshot ID:\n\n```\nAGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx\n```\n\nAdd this to your Vercel project environment variables (or `.env.local`\nfor local development). Recommended for any production deployment.\n\n## Authentication\n\nOn Vercel deployments, the Sandbox SDK authenticates automatically via\nOIDC. For local development, provide explicit credentials:\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token</td></tr>\n    <tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID</td></tr>\n    <tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID</td></tr>\n  </tbody>\n</table>\n\nWhen all three are set, they are passed to `Sandbox.create()`. When\nabsent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel).\n\n## Scheduled workflows (cron)\n\nFor recurring tasks like daily monitoring, use Vercel Cron Jobs:\n\n```ts\n// app/api/cron/monitor/route.ts\nexport async function GET() {\n  const result = await withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\n      \"open\", \"https://example.com/pricing\",\n    ]);\n    const snap = await sandbox.runCommand(\"agent-browser\", [\n      \"snapshot\", \"-i\", \"-c\",\n    ]);\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n    return await snap.stdout();\n  });\n\n  // Process results, send alerts, store data...\n  return Response.json({ ok: true, snapshot: result });\n}\n```\n\n```json\n// vercel.json\n{\n  \"crons\": [\n    { \"path\": \"/api/cron/monitor\", \"schedule\": \"0 9 * * *\" }\n  ]\n}\n```\n\n## Environment variables\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>AGENT_BROWSER_SNAPSHOT_ID</code></td><td>Sandbox snapshot ID for sub-second startup (see above)</td></tr>\n    <tr><td><code>VERCEL_TOKEN</code></td><td>Vercel personal access token (for local dev; OIDC is automatic on Vercel)</td></tr>\n    <tr><td><code>VERCEL_TEAM_ID</code></td><td>Vercel team ID (for local dev)</td></tr>\n    <tr><td><code>VERCEL_PROJECT_ID</code></td><td>Vercel project ID (for local dev)</td></tr>\n  </tbody>\n</table>\n\n## Demo app\n\nA working demo with streaming progress UI, rate limiting, and a\ndeploy-to-Vercel button is at\n[`examples/environments/`](https://github.com/vercel-labs/agent-browser/tree/main/examples/environments).\n"
  },
  {
    "path": "docs/src/app/og/[...slug]/route.tsx",
    "content": "import { NextResponse } from \"next/server\";\nimport { getPageTitle, renderOgImage } from \"../og-image\";\n\nexport async function GET(\n  _request: Request,\n  { params }: { params: Promise<{ slug: string[] }> },\n) {\n  const { slug } = await params;\n  const title = getPageTitle(slug.join(\"/\"));\n\n  if (!title) {\n    return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n  }\n\n  return renderOgImage(title);\n}\n"
  },
  {
    "path": "docs/src/app/og/og-image.tsx",
    "content": "import { ImageResponse } from \"next/og\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport { getPageTitle } from \"@/lib/page-titles\";\n\nlet fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null =\n  null;\n\nasync function loadFonts() {\n  if (fontCache) return fontCache;\n  const [geistRegular, geistPixelSquare] = await Promise.all([\n    readFile(join(process.cwd(), \"public/Geist-Regular.ttf\")),\n    readFile(join(process.cwd(), \"public/GeistPixel-Square.ttf\")),\n  ]);\n  fontCache = { geistRegular, geistPixelSquare };\n  return fontCache;\n}\n\nexport async function renderOgImage(title: string) {\n  const { geistRegular, geistPixelSquare } = await loadFonts();\n\n  return new ImageResponse(\n    <div\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        backgroundColor: \"black\",\n        padding: \"60px 80px\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: \"16px\",\n        }}\n      >\n        <svg width=\"36\" height=\"36\" viewBox=\"0 0 16 16\" fill=\"white\">\n          <path fillRule=\"evenodd\" clipRule=\"evenodd\" d=\"M8 1L16 15H0L8 1Z\" />\n        </svg>\n        <span\n          style={{\n            fontSize: 36,\n            color: \"#666\",\n            fontFamily: \"Geist\",\n            fontWeight: 400,\n          }}\n        >\n          /\n        </span>\n        <span\n          style={{\n            fontSize: 36,\n            fontFamily: \"GeistPixelSquare\",\n            fontWeight: 400,\n            color: \"white\",\n          }}\n        >\n          agent-browser\n        </span>\n      </div>\n\n      <div\n        style={{\n          display: \"flex\",\n          flex: 1,\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        {title.split(\"\\n\").map((line, i) => (\n          <span\n            key={i}\n            style={{\n              fontSize: 72,\n              fontFamily: \"Geist\",\n              fontWeight: 400,\n              color: \"white\",\n              letterSpacing: \"-0.02em\",\n              textAlign: \"center\",\n              lineHeight: 1.2,\n            }}\n          >\n            {line}\n          </span>\n        ))}\n      </div>\n    </div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [\n        {\n          name: \"Geist\",\n          data: geistRegular.buffer as ArrayBuffer,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"GeistPixelSquare\",\n          data: geistPixelSquare.buffer as ArrayBuffer,\n          style: \"normal\",\n          weight: 400,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "docs/src/app/og/route.tsx",
    "content": "import { getPageTitle, renderOgImage } from \"./og-image\";\n\nexport async function GET() {\n  const title = getPageTitle(\"\")!;\n  return renderOgImage(title);\n}\n"
  },
  {
    "path": "docs/src/app/page.mdx",
    "content": "# agent-browser\n\nBrowser automation CLI designed for AI agents. Compact text output minimizes context usage. 100% native Rust.\n\n```bash\nnpm install -g agent-browser      # all platforms\nbrew install agent-browser        # macOS\nagent-browser install             # Download Chrome (first time)\n\n# or try without installing\nnpx agent-browser open example.com\n```\n\n## Features\n\n- **Agent-first** - Compact text output uses fewer tokens than JSON, designed for AI context efficiency\n- **Ref-based** - Snapshot returns accessibility tree with refs for deterministic element selection\n- **Fast** - Native Rust CLI for instant command parsing\n- **Complete** - 50+ commands for navigation, forms, screenshots, network, storage\n- **Sessions** - Multiple isolated browser instances with separate auth\n- **Cross-platform** - macOS, Linux, Windows with native binaries\n\n## Works with\n\nClaude Code, Cursor, GitHub Copilot, OpenAI Codex, Google Gemini, opencode, and any agent that can run shell commands.\n\n## Example\n\n```bash\n# Navigate and get snapshot\nagent-browser open example.com\nagent-browser snapshot -i\n\n# Output:\n# - heading \"Example Domain\" [ref=e1]\n# - link \"More information...\" [ref=e2]\n\n# Interact using refs\nagent-browser click @e2\nagent-browser screenshot page.png\nagent-browser close\n```\n\n## Why refs?\n\nThe `snapshot` command returns a compact accessibility tree where each element\nhas a unique ref like `@e1`, `@e2`. This provides:\n\n- **Context-efficient** - Text output uses ~200-400 tokens vs ~3000-5000 for full DOM\n- **Deterministic** - Ref points to exact element from snapshot\n- **Fast** - No DOM re-query needed\n- **AI-friendly** - LLMs parse text output naturally\n\n## Architecture\n\nClient-daemon architecture for optimal performance:\n\n1. **Rust CLI** - Parses commands, communicates with daemon\n2. **Native Daemon** - Pure Rust daemon using direct CDP, manages Chrome via Chrome DevTools Protocol\n\nDaemon starts automatically and persists between commands.\n\n## Platforms\n\nNative Rust binaries for macOS (ARM64, x64), Linux (ARM64, x64), and Windows (x64).\n"
  },
  {
    "path": "docs/src/app/profiler/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"profiler\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/profiler/page.mdx",
    "content": "# Profiler\n\nCapture Chrome DevTools performance profiles during browser automation.\nUse profiles to diagnose slow page loads, expensive JavaScript, layout thrashing,\nand other performance bottlenecks in agentic workflows.\n\n## Basic usage\n\n```bash\n# Start profiling\nagent-browser profiler start\n\n# Perform actions\nagent-browser navigate https://example.com\nagent-browser click \"#button\"\n\n# Stop and save profile\nagent-browser profiler stop ./trace.json\n```\n\nThe output JSON file can be loaded into Chrome DevTools, Perfetto UI, or any\ntool that accepts Chrome Trace Event format.\n\n## Commands\n\n<table>\n  <thead>\n    <tr><th>Command</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>profiler start</code></td><td>Start recording a performance profile</td></tr>\n    <tr><td><code>profiler start --categories &lt;list&gt;</code></td><td>Start with custom trace categories</td></tr>\n    <tr><td><code>profiler stop [path]</code></td><td>Stop profiling and save to file</td></tr>\n  </tbody>\n</table>\n\n## Trace categories\n\nThe `--categories` flag accepts a comma-separated list of Chrome trace categories.\n\n```bash\nagent-browser profiler start --categories \"devtools.timeline,v8.execute,blink.user_timing\"\n```\n\nDefault categories include `devtools.timeline`, `v8.execute`, `blink`,\n`blink.user_timing`, `latencyInfo`, `renderer.scheduler`, `toplevel`, and\nseveral `disabled-by-default-*` categories for detailed CPU profiling and\ncall stack analysis.\n\n### Common categories\n\n<table>\n  <thead>\n    <tr><th>Category</th><th>What it captures</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>devtools.timeline</code></td><td>Standard DevTools performance events</td></tr>\n    <tr><td><code>v8.execute</code></td><td>Time spent running JavaScript</td></tr>\n    <tr><td><code>blink</code></td><td>Renderer events (layout, paint, style)</td></tr>\n    <tr><td><code>blink.user_timing</code></td><td><code>performance.mark()</code> and <code>performance.measure()</code> calls</td></tr>\n    <tr><td><code>latencyInfo</code></td><td>Input-to-display latency</td></tr>\n    <tr><td><code>disabled-by-default-v8.cpu_profiler</code></td><td>Sampling-based JS CPU profiling</td></tr>\n  </tbody>\n</table>\n\n## Output format\n\nThe output is a JSON file in Chrome Trace Event format:\n\n```json\n{\n  \"traceEvents\": [\n    {\n      \"cat\": \"devtools.timeline\",\n      \"name\": \"RunTask\",\n      \"ph\": \"X\",\n      \"ts\": 12345,\n      \"dur\": 100,\n      \"pid\": 1,\n      \"tid\": 1\n    }\n  ],\n  \"metadata\": {\n    \"clock-domain\": \"LINUX_CLOCK_MONOTONIC\"\n  }\n}\n```\n\nThe `metadata.clock-domain` field reflects the host platform (Linux or macOS).\nOn Windows it is omitted.\n\n## Viewing profiles\n\n- **Chrome DevTools** -- Performance panel > Load profile\n- **Perfetto** -- https://ui.perfetto.dev/ (drag and drop the JSON file)\n- **Trace Viewer** -- `chrome://tracing` in any Chromium browser\n\n## Use cases\n\n- **Page load analysis** -- Profile navigation to identify slow resources, long tasks, or layout shifts\n- **Interaction profiling** -- Measure the cost of clicks, form fills, and other user interactions\n- **CI regression checks** -- Capture profiles per build and compare trace data over time\n- **Agent workflow optimization** -- Find which steps in an agentic flow are most expensive\n\n## Limitations\n\n- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.\n- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.\n- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.\n- When no output path is provided, the profile is saved to an auto-generated path under the agent-browser temp directory.\n"
  },
  {
    "path": "docs/src/app/providers/browser-use/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"providers/browser-use\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/providers/browser-use/page.mdx",
    "content": "# Browser Use\n\n[Browser Use](https://browser-use.com) provides cloud browser infrastructure for AI agents. Use it when running agent-browser in environments where a local browser isn't available (serverless, CI/CD, etc.).\n\n## Setup\n\n```bash\nexport BROWSER_USE_API_KEY=\"your-api-key\"\nagent-browser -p browseruse open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=browseruse\nexport BROWSER_USE_API_KEY=\"your-api-key\"\nagent-browser open https://example.com\n```\n\nThe `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.\n\nWhen enabled, agent-browser connects to a Browser Use cloud session instead of launching a local browser. All commands work identically.\n\nGet your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.\n"
  },
  {
    "path": "docs/src/app/providers/browserbase/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"providers/browserbase\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/providers/browserbase/page.mdx",
    "content": "# Browserbase\n\n[Browserbase](https://browserbase.com) provides remote browser infrastructure to make deployment of agentic browsing agents easy. Use it when running agent-browser in environments where a local browser isn't feasible.\n\n## Setup\n\n```bash\nexport BROWSERBASE_API_KEY=\"your-api-key\"\nagent-browser -p browserbase open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=browserbase\nexport BROWSERBASE_API_KEY=\"your-api-key\"\nagent-browser open https://example.com\n```\n\nThe `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.\n\nWhen enabled, agent-browser connects to a Browserbase session instead of launching a local browser. All commands work identically.\n\nGet your API key from the [Browserbase Dashboard](https://browserbase.com/overview).\n"
  },
  {
    "path": "docs/src/app/providers/browserless/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"providers/browserless\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/providers/browserless/page.mdx",
    "content": "# Browserless\n\n[Browserless](https://browserless.io) provides cloud browser infrastructure with a Sessions API. Use it when running agent-browser in environments where a local browser isn't available.\n\n## Setup\n\n```bash\nexport BROWSERLESS_API_KEY=\"your-api-token\"\nagent-browser -p browserless open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=browserless\nexport BROWSERLESS_API_KEY=\"your-api-token\"\nagent-browser open https://example.com\n```\n\nThe `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.\n\n## Configuration\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th><th>Default</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>BROWSERLESS_API_KEY</code></td><td>API token (required)</td><td></td></tr>\n    <tr><td><code>BROWSERLESS_API_URL</code></td><td>Base API URL (for custom regions or self-hosted)</td><td><code>https://production-sfo.browserless.io</code></td></tr>\n    <tr><td><code>BROWSERLESS_BROWSER_TYPE</code></td><td>Type of browser to use (<code>chromium</code> or <code>chrome</code>)</td><td><code>chromium</code></td></tr>\n    <tr><td><code>BROWSERLESS_TTL</code></td><td>Session TTL in milliseconds</td><td><code>300000</code></td></tr>\n    <tr><td><code>BROWSERLESS_STEALTH</code></td><td>Enable stealth mode</td><td><code>true</code></td></tr>\n  </tbody>\n</table>\n\nWhen enabled, agent-browser connects to a Browserless cloud session instead of launching a local browser. All commands work identically.\n\nGet your API token from the [Browserless Dashboard](https://browserless.io).\n"
  },
  {
    "path": "docs/src/app/providers/kernel/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"providers/kernel\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/providers/kernel/page.mdx",
    "content": "# Kernel\n\n[Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles.\n\n## Setup\n\n```bash\nexport KERNEL_API_KEY=\"your-api-key\"\nagent-browser -p kernel open https://example.com\n```\n\nOr use environment variables for CI/scripts:\n\n```bash\nexport AGENT_BROWSER_PROVIDER=kernel\nexport KERNEL_API_KEY=\"your-api-key\"\nagent-browser open https://example.com\n```\n\nThe `-p` flag takes precedence over `AGENT_BROWSER_PROVIDER`.\n\n## Configuration\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th><th>Default</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>KERNEL_API_KEY</code></td><td>API key (required)</td><td></td></tr>\n    <tr><td><code>KERNEL_HEADLESS</code></td><td>Run browser in headless mode</td><td><code>true</code></td></tr>\n    <tr><td><code>KERNEL_STEALTH</code></td><td>Enable stealth mode to avoid bot detection</td><td><code>false</code></td></tr>\n    <tr><td><code>KERNEL_TIMEOUT_SECONDS</code></td><td>Session timeout in seconds</td><td><code>300</code></td></tr>\n    <tr><td><code>KERNEL_PROFILE_NAME</code></td><td>Browser profile name for persistent cookies/logins</td><td>(none)</td></tr>\n  </tbody>\n</table>\n\n**Profile persistence:** When `KERNEL_PROFILE_NAME` is set, the profile will be created if it doesn't already exist. Cookies, logins, and session data are automatically saved back to the profile when the browser session ends, making them available for future sessions.\n\nWhen enabled, agent-browser connects to a Kernel cloud session instead of launching a local browser. All commands work identically.\n\nGet your API key from the [Kernel Dashboard](https://dashboard.onkernel.com).\n"
  },
  {
    "path": "docs/src/app/quick-start/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"quick-start\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/quick-start/page.mdx",
    "content": "# Quick Start\n\n## Core workflow\n\nEvery browser automation follows this pattern:\n\n```bash\n# 1. Navigate\nagent-browser open example.com\n\n# 2. Snapshot to get element refs\nagent-browser snapshot -i\n# Output:\n# @e1 [heading] \"Example Domain\"\n# @e2 [link] \"More information...\"\n\n# 3. Interact using refs\nagent-browser click @e2\n\n# 4. Re-snapshot after page changes\nagent-browser snapshot -i\n```\n\n## Common commands\n\n```bash\nagent-browser open example.com\nagent-browser snapshot -i                # Get interactive elements with refs\nagent-browser click @e2                  # Click by ref\nagent-browser fill @e3 \"test@example.com\" # Fill input by ref\nagent-browser get text @e1               # Get text content\nagent-browser screenshot                 # Save to temp directory\nagent-browser screenshot page.png        # Save to specific path\nagent-browser close\n```\n\n## Traditional selectors\n\nCSS selectors and semantic locators also supported:\n\n```bash\nagent-browser click \"#submit\"\nagent-browser fill \"#email\" \"test@example.com\"\nagent-browser find role button click --name \"Submit\"\n```\n\n## Headed mode\n\nShow browser window for debugging:\n\n```bash\nagent-browser open example.com --headed\n```\n\n## Wait for content\n\n```bash\nagent-browser wait @e1                   # Wait for element\nagent-browser wait --load networkidle    # Wait for network idle\nagent-browser wait --url \"**/dashboard\"  # Wait for URL pattern\nagent-browser wait 2000                  # Wait milliseconds\n```\n\n## Command chaining\n\nChain commands with `&&` in a single shell call. The browser persists via a background daemon, so chaining is safe and efficient:\n\n```bash\n# Open, wait, and snapshot in one call\nagent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i\n\n# Chain multiple interactions\nagent-browser fill @e1 \"user@example.com\" && agent-browser fill @e2 \"pass\" && agent-browser click @e3\n\n# Navigate and capture\nagent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png\n```\n\nUse `&&` when you don't need intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs before interacting).\n\n## JSON output\n\nFor programmatic parsing in scripts:\n\n```bash\nagent-browser snapshot --json\nagent-browser get text @e1 --json\n```\n\nNote: The default text output is more compact and preferred for AI agents.\n"
  },
  {
    "path": "docs/src/app/security/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"security\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/security/page.mdx",
    "content": "# Security\n\nagent-browser includes security features to protect against credential exposure, prompt injection via untrusted page content, and unauthorized browser actions.\n\nAll security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output. Enable these features as needed for your deployment -- existing workflows are unaffected until you explicitly activate a feature.\n\n## Threat Model\n\nThese features are designed to mitigate the following threats when an LLM-based agent drives a browser:\n\n- **Credential exposure** -- Passwords stored in the auth vault are never included in LLM context. The CLI handles vault operations locally; credentials do not pass through the daemon's IPC channel.\n- **Prompt injection via page content** -- Malicious pages can embed text that looks like tool output or system instructions. Content boundary markers (`--content-boundaries`) let the orchestrator distinguish trusted tool output from untrusted page content.\n- **Unauthorized navigation / data exfiltration** -- A compromised or manipulated agent could navigate to attacker-controlled domains to exfiltrate data. The domain allowlist (`--allowed-domains`) blocks navigations, sub-resource requests, WebSocket connections, EventSource streams, and `sendBeacon` calls to non-allowed domains.\n- **Unauthorized destructive actions** -- Action policy (`--action-policy`) and confirmation gating (`--confirm-actions`) prevent the agent from performing dangerous operations (eval, downloads, uploads) without explicit approval.\n- **Context flooding** -- Large page outputs can overwhelm an LLM's context window. Output truncation (`--max-output`) caps the size of page-sourced content.\n\n### Known limitations\n\n- **WebSocket/EventSource blocking is best-effort.** It works by overriding browser constructors via an init script. If the `eval` action category is allowed, page scripts could theoretically restore the original constructors. Deny `eval` via `--action-policy` for maximum protection.\n- **Domain filter timing on remote connections.** When connecting to a pre-existing browser via CDP or a cloud provider, pages may have already loaded content before the domain filter is installed. agent-browser navigates disallowed pages to `about:blank` after the filter is active, but resources loaded before that point are not retroactively blocked.\n- **Content boundaries are defense-in-depth.** They rely on the LLM and orchestrator respecting the structural markers. A sufficiently capable adversarial page could attempt to mimic the boundary format, though the per-process CSPRNG nonce makes this impractical to predict.\n- **Confirmation timeout.** Pending confirmations auto-deny after 60 seconds. Orchestrators must respond within that window.\n- **Non-TTY auto-deny.** When `--confirm-interactive` is set but stdin is not a terminal (e.g., piped input), actions are automatically denied to prevent accidental approval in non-interactive contexts.\n\n## Authentication Vault\n\nStore credentials locally and reference them by name. The LLM never sees passwords.\n\n```bash\n# Save credentials (encrypted if AGENT_BROWSER_ENCRYPTION_KEY is set)\n# Recommended: pipe password via stdin to avoid shell history / process listing exposure\necho \"pass\" | agent-browser auth save github --url https://github.com/login --username user --password-stdin\n\n# Or pass directly (a warning will be shown)\nagent-browser auth save github --url https://github.com/login --username user --password pass\n\n# Login using saved credentials\nagent-browser auth login github\n\n# List saved profiles (names and URLs only, no secrets)\nagent-browser auth list\n\n# Show profile metadata\nagent-browser auth show github\n\n# Delete a profile\nagent-browser auth delete github\n```\n\nCustom selectors can be specified if auto-detection fails:\n\n```bash\nagent-browser auth save myapp \\\n  --url https://app.example.com/login \\\n  --username user --password pass \\\n  --username-selector \"#email\" \\\n  --password-selector \"#password\" \\\n  --submit-selector \"button.login\"\n```\n\nProfiles are stored in `~/.agent-browser/auth/` and always encrypted with AES-256-GCM. If `AGENT_BROWSER_ENCRYPTION_KEY` is not set, a key is auto-generated at `~/.agent-browser/.encryption-key` on first use. Back up this file or set the environment variable explicitly for portability.\n\nFile permissions are enforced on both Unix (`chmod 600`/`700`) and Windows (`icacls` restricted to the current user) to prevent other users from reading encryption keys or auth profiles.\n\n## Content Boundary Markers\n\nWhen `--content-boundaries` is enabled, all page-sourced output is wrapped in structural markers so LLMs can distinguish tool output from untrusted page content:\n\n```\n--- AGENT_BROWSER_PAGE_CONTENT nonce=a1b2c3d4 origin=https://example.com ---\n[snapshot / text / html / eval output here]\n--- END_AGENT_BROWSER_PAGE_CONTENT nonce=a1b2c3d4 ---\n```\n\nThe nonce is a random value generated per CLI process invocation, making it unpredictable to page content that might attempt to spoof the boundary.\n\nEnable via flag or environment variable:\n\n```bash\nagent-browser --content-boundaries snapshot\n# or\nexport AGENT_BROWSER_CONTENT_BOUNDARIES=1\n```\n\nAffected output types: `snapshot`, `get text`, `get html`, `eval`, `console`.\n\nIn `--json` mode, boundary metadata is injected into the JSON response as a `_boundary` object containing `nonce` and `origin` fields, allowing orchestrators to verify provenance programmatically:\n\n```json\n{\n  \"success\": true,\n  \"data\": { \"snapshot\": \"...\", \"origin\": \"https://example.com\" },\n  \"_boundary\": { \"nonce\": \"a1b2c3d4e5f6...\", \"origin\": \"https://example.com\" }\n}\n```\n\n## Domain Allowlist\n\nRestrict which domains the browser can interact with, preventing redirect-based attacks and data exfiltration:\n\n```bash\nagent-browser --allowed-domains \"example.com,*.example.com,github.com\" open https://example.com\n# or\nexport AGENT_BROWSER_ALLOWED_DOMAINS=\"example.com,*.example.com\"\n```\n\nSupports exact match (`github.com`) and wildcard prefix (`*.example.com`, which also matches the bare domain `example.com`). Both page navigations and sub-resource requests (scripts, images, fetch, XHR, etc.) to non-allowed domains are blocked, preventing data exfiltration. WebSocket and EventSource connections are also blocked via constructor-level patching. Non-http(s) sub-resources (data URIs, blobs) are still allowed. When a request is blocked, the command returns an error.\n\n> **Note:** The WebSocket/EventSource blocking is best-effort -- it works by overriding the browser constructors via an init script. If the `eval` action category is allowed, page scripts could theoretically restore the original constructors. For maximum protection, deny the `eval` category via `--action-policy` when using `--allowed-domains`.\n\nConfig file:\n\n```json\n{\n  \"allowedDomains\": [\"example.com\", \"*.example.com\", \"github.com\"]\n}\n```\n\n> **CDN and third-party resources:** The domain filter blocks all sub-resource requests (scripts, stylesheets, images, fonts, fetch/XHR) to non-allowed domains. Most websites load assets from CDN domains. Include these in your allowlist or pages will break. For example:\n>\n> ```bash\n> --allowed-domains \"myapp.com,*.myapp.com,cdn.jsdelivr.net,fonts.googleapis.com,fonts.gstatic.com\"\n> ```\n\n## Action Policy\n\nGate actions using a static policy file. The policy is enforced by the daemon -- denied actions fail immediately.\n\n```bash\nagent-browser --action-policy ./policy.json open https://example.com\n# or\nexport AGENT_BROWSER_ACTION_POLICY=./policy.json\n```\n\nExample policy (permissive with specific denials):\n\n```json\n{\n  \"default\": \"allow\",\n  \"deny\": [\"eval\", \"download\", \"upload\"]\n}\n```\n\nExample policy (restrictive):\n\n```json\n{\n  \"default\": \"deny\",\n  \"allow\": [\"navigate\", \"snapshot\", \"click\", \"scroll\", \"wait\", \"get\"]\n}\n```\n\n<table>\n  <thead>\n    <tr><th>Category</th><th>Actions</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>navigate</code></td><td>open, back, forward, reload, tab new</td></tr>\n    <tr><td><code>click</code></td><td>click, dblclick, tap</td></tr>\n    <tr><td><code>fill</code></td><td>fill, type, keyboard type/inserttext, select, check, uncheck</td></tr>\n    <tr><td><code>eval</code></td><td>eval, evalhandle, addscript, addinitscript, addstyle, expose, setcontent</td></tr>\n    <tr><td><code>download</code></td><td>download, waitfordownload</td></tr>\n    <tr><td><code>upload</code></td><td>upload</td></tr>\n    <tr><td><code>snapshot</code></td><td>snapshot, screenshot, pdf, diff</td></tr>\n    <tr><td><code>scroll</code></td><td>scroll, scrollintoview</td></tr>\n    <tr><td><code>wait</code></td><td>wait, waitforurl, waitforloadstate, waitforfunction</td></tr>\n    <tr><td><code>get</code></td><td>get text/html/url/title, count, isvisible, getbyrole, getbytext, getbylabel, etc.</td></tr>\n    <tr><td><code>interact</code></td><td>hover, focus, drag, press, keydown, keyup, mousemove, dispatch</td></tr>\n    <tr><td><code>network</code></td><td>network route/unroute, requests, har start/stop</td></tr>\n    <tr><td><code>state</code></td><td>state save/load, cookies set, storage set</td></tr>\n  </tbody>\n</table>\n\nAuth vault operations (`auth save`, `auth login`, `auth list`, `auth show`, `auth delete`) and other internal/meta operations bypass action policy enforcement since they are trusted local operations. Domain allowlist restrictions still apply to `auth login` navigations.\n\n## Action Confirmation\n\nFor actions that require explicit approval, use `--confirm-actions` to specify categories that require confirmation:\n\n```bash\n# Orchestrator mode: returns confirmation_required response\nagent-browser --confirm-actions eval,download eval \"document.title\"\n\n# Then approve or deny:\nagent-browser confirm c_8f3a1234\nagent-browser deny c_8f3a1234\n```\n\nFor interactive (human-in-the-loop) confirmation:\n\n```bash\nagent-browser --confirm-actions eval,download --confirm-interactive eval \"document.title\"\n# Prompts: Allow? [y/N]\n```\n\nPending confirmations auto-deny after 60 seconds.\n\n> **Non-TTY behavior:** When `--confirm-interactive` is set but stdin is not a TTY (e.g., piped input or running inside an automated pipeline), actions are automatically denied. This prevents accidental approval in non-interactive contexts.\n\n## Output Length Limits\n\nPrevent context flooding by truncating large page outputs:\n\n```bash\nagent-browser --max-output 50000 get text body\n# or\nexport AGENT_BROWSER_MAX_OUTPUT=50000\n```\n\nAffected output types: `snapshot`, `get text`, `get html`, `eval`, `console`.\n\n## Environment Variables\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>AGENT_BROWSER_CONTENT_BOUNDARIES</code></td><td>Wrap page output in boundary markers</td></tr>\n    <tr><td><code>AGENT_BROWSER_MAX_OUTPUT</code></td><td>Max characters for page output</td></tr>\n    <tr><td><code>AGENT_BROWSER_ALLOWED_DOMAINS</code></td><td>Comma-separated allowed domain patterns</td></tr>\n    <tr><td><code>AGENT_BROWSER_ACTION_POLICY</code></td><td>Path to action policy JSON file</td></tr>\n    <tr><td><code>AGENT_BROWSER_CONFIRM_ACTIONS</code></td><td>Comma-separated action categories requiring confirmation</td></tr>\n    <tr><td><code>AGENT_BROWSER_CONFIRM_INTERACTIVE</code></td><td>Enable interactive confirmation prompts</td></tr>\n    <tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption (auth vault + sessions)</td></tr>\n  </tbody>\n</table>\n\n## Recommended Configuration\n\nFor production AI agent deployments:\n\n```json\n{\n  \"contentBoundaries\": true,\n  \"maxOutput\": 50000,\n  \"allowedDomains\": [\"your-app.com\", \"*.your-app.com\"],\n  \"actionPolicy\": \"./policy.json\"\n}\n```\n"
  },
  {
    "path": "docs/src/app/selectors/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"selectors\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/selectors/page.mdx",
    "content": "# Selectors\n\n## Refs (recommended)\n\nRefs provide deterministic element selection from snapshots. Best for AI agents.\n\n```bash\n# 1. Get snapshot with refs\nagent-browser snapshot\n# Output:\n# - heading \"Example Domain\" [ref=e1] [level=1]\n# - button \"Submit\" [ref=e2]\n# - textbox \"Email\" [ref=e3]\n# - link \"Learn more\" [ref=e4]\n\n# 2. Use refs to interact\nagent-browser click @e2                   # Click the button\nagent-browser fill @e3 \"test@example.com\" # Fill the textbox\nagent-browser get text @e1                # Get heading text\nagent-browser hover @e4                   # Hover the link\n```\n\n### Why refs?\n\n- **Deterministic** - Ref points to exact element from snapshot\n- **Fast** - No DOM re-query needed\n- **AI-friendly** - LLMs can reliably parse and use refs\n\n## CSS selectors\n\n```bash\nagent-browser click \"#id\"\nagent-browser click \".class\"\nagent-browser click \"div > button\"\nagent-browser click \"[data-testid='submit']\"\n```\n\n## Text & XPath\n\n```bash\nagent-browser click \"text=Submit\"\nagent-browser click \"xpath=//button[@type='submit']\"\n```\n\n## Semantic locators\n\nFind elements by role, label, or other semantic properties:\n\n```bash\nagent-browser find role button click --name \"Submit\"\nagent-browser find label \"Email\" fill \"test@test.com\"\nagent-browser find placeholder \"Search...\" fill \"query\"\nagent-browser find testid \"submit-btn\" click\n```\n"
  },
  {
    "path": "docs/src/app/sessions/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"sessions\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/sessions/page.mdx",
    "content": "# Sessions\n\nRun multiple isolated browser instances:\n\n```bash\n# Different sessions\nagent-browser --session agent1 open site-a.com\nagent-browser --session agent2 open site-b.com\n\n# Or via environment variable\nAGENT_BROWSER_SESSION=agent1 agent-browser click \"#btn\"\n\n# List active sessions\nagent-browser session list\n# Output:\n# Active sessions:\n# -> default\n#    agent1\n\n# Show current session\nagent-browser session\n```\n\n## Session isolation\n\nEach session has its own:\n\n- Browser instance\n- Cookies and storage\n- Navigation history\n- Authentication state\n\n## Persistent profiles\n\nBy default, browser state is lost when the browser closes. Use `--profile` to persist state across restarts:\n\n```bash\n# Use a persistent profile directory\nagent-browser --profile ~/.myapp-profile open myapp.com\n\n# Login once, then reuse the authenticated session\nagent-browser --profile ~/.myapp-profile open myapp.com/dashboard\n\n# Or via environment variable\nAGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com\n```\n\nThe profile directory stores:\n\n- Cookies and localStorage\n- IndexedDB data\n- Service workers\n- Browser cache\n- Login sessions\n\n## Import auth from your browser\n\nIf you are already logged in to a site in Chrome, you can grab that auth state and reuse it in agent-browser. This is the fastest way to bypass login flows, OAuth, SSO, or 2FA.\n\n**Step 1:** Start Chrome with remote debugging:\n\n```bash\n# macOS\n\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" --remote-debugging-port=9222\n\n# Linux\ngoogle-chrome --remote-debugging-port=9222\n```\n\nLog in to your target site(s) in this Chrome window.\n\n`--remote-debugging-port` exposes full browser control on localhost. Any local process can connect. Only use on trusted machines and close Chrome when done.\n\n**Step 2:** Connect and save the authenticated state:\n\n```bash\nagent-browser --auto-connect state save ./my-auth.json\n```\n\n**Step 3:** Use the saved auth in future sessions:\n\n```bash\n# Load auth at launch\nagent-browser --state ./my-auth.json open https://app.example.com/dashboard\n\n# Or load into an existing session\nagent-browser state load ./my-auth.json\nagent-browser open https://app.example.com/dashboard\n```\n\nCombine with `--session-name` so the imported auth auto-persists across restarts:\n\n```bash\nagent-browser --session-name myapp state load ./my-auth.json\n# From now on, state auto-saves/restores for \"myapp\"\n```\n\nState files contain session tokens in plaintext. Add them to `.gitignore` and delete when no longer needed. For encryption at rest, see [State encryption](#state-encryption) below.\n\n## Session persistence\n\nUse `--session-name` to automatically save and restore cookies and localStorage across browser restarts:\n\n```bash\n# Auto-save/load state for \"twitter\" session\nagent-browser --session-name twitter open twitter.com\n\n# Login once, then state persists automatically\nagent-browser --session-name twitter click \"#login\"\n\n# Or via environment variable\nexport AGENT_BROWSER_SESSION_NAME=twitter\nagent-browser open twitter.com\n```\n\nState files are stored in `~/.agent-browser/sessions/` and automatically loaded on daemon start.\n\n### Session name rules\n\nSession names must contain only alphanumeric characters, hyphens, and underscores:\n\n```bash\n# Valid session names\nagent-browser --session-name my-project open example.com\nagent-browser --session-name test_session_v2 open example.com\n\n# Invalid (will be rejected)\nagent-browser --session-name \"../bad\" open example.com    # path traversal\nagent-browser --session-name \"my session\" open example.com # spaces\nagent-browser --session-name \"foo/bar\" open example.com    # slashes\n```\n\n## State encryption\n\nEncrypt saved state files (cookies, localStorage) using AES-256-GCM:\n\n```bash\n# Generate a 256-bit key (64 hex characters)\nopenssl rand -hex 32\n\n# Set the encryption key\nexport AGENT_BROWSER_ENCRYPTION_KEY=<your-64-char-hex-key>\n\n# State files are now encrypted automatically\nagent-browser --session-name secure-session open example.com\n\n# List states shows encryption status\nagent-browser state list\n```\n\n## State auto-expiration\n\nAutomatically delete old state files to prevent accumulation:\n\n```bash\n# Set expiration (default: 30 days)\nexport AGENT_BROWSER_STATE_EXPIRE_DAYS=7\n\n# Manually clean old states\nagent-browser state clean --older-than 7\n```\n\n## State management commands\n\n```bash\n# List all saved states\nagent-browser state list\n\n# Show state summary (cookies, origins, domains)\nagent-browser state show my-session-default.json\n\n# Rename a state file\nagent-browser state rename old-name new-name\n\n# Clear states for a specific session name\nagent-browser state clear my-session\n\n# Clear all saved states\nagent-browser state clear --all\n\n# Manual save/load (for custom paths)\nagent-browser state save ./backup.json\nagent-browser state load ./backup.json\n```\n\n## Authenticated sessions\n\nUse `--headers` to set HTTP headers for a specific origin:\n\n```bash\n# Headers scoped to api.example.com only\nagent-browser open api.example.com --headers '{\"Authorization\": \"Bearer <token>\"}'\n\n# Requests to api.example.com include the auth header\nagent-browser snapshot -i --json\nagent-browser click @e2\n\n# Navigate to another domain - headers NOT sent\nagent-browser open other-site.com\n```\n\nUseful for:\n\n- **Skipping login flows** - Authenticate via headers\n- **Switching users** - Different auth tokens per session\n- **API testing** - Access protected endpoints\n- **Security** - Headers scoped to origin, not leaked\n\n## Multiple origins\n\n```bash\nagent-browser open api.example.com --headers '{\"Authorization\": \"Bearer token1\"}'\nagent-browser open api.acme.com --headers '{\"Authorization\": \"Bearer token2\"}'\n```\n\n## Global headers\n\nFor headers on all domains:\n\n```bash\nagent-browser set headers '{\"X-Custom-Header\": \"value\"}'\n```\n\n## Environment variables\n\n<table>\n  <thead>\n    <tr><th>Variable</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>AGENT_BROWSER_SESSION</code></td><td>Browser session ID (default: \"default\")</td></tr>\n    <tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name</td></tr>\n    <tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM encryption</td></tr>\n    <tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete states older than N days (default: 30)</td></tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "docs/src/app/skills/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"skills\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/skills/page.mdx",
    "content": "# Skills\n\nagent-browser ships with skills that teach AI coding agents how to use it for specific workflows. Install a skill and your agent in Cursor, Claude Code, or Codex can automate browser tasks without manual guidance.\n\n## Available Skills\n\n- **agent-browser** — General browser automation: navigation, snapshots, forms, screenshots, data extraction, sessions, authentication, diffing, and the full command reference.\n- **dogfood** — Systematic exploratory testing. Navigates an app like a real user, finds bugs and UX issues, and produces a structured report with screenshots and repro videos.\n- **electron** — Automate any Electron app (VS Code, Slack, Discord, Figma, etc.) by connecting to its built-in Chrome DevTools Protocol port. This is how agent-browser drives native desktop apps like the Slack macOS app.\n- **slack** — Browser-based Slack automation. Check unreads, navigate channels, search conversations, send messages, and extract data — no API tokens needed.\n- **vercel-sandbox** — Run agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs. Works with any Vercel-deployed framework (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.).\n\n## Installation\n\n```bash\nnpx skills add vercel-labs/agent-browser --skill agent-browser\nnpx skills add vercel-labs/agent-browser --skill dogfood\nnpx skills add vercel-labs/agent-browser --skill electron\nnpx skills add vercel-labs/agent-browser --skill slack\nnpx skills add vercel-labs/agent-browser --skill vercel-sandbox\n```\n\nAfter installing, your AI agent will automatically activate the right skill when it encounters a matching request.\n\n## agent-browser\n\nThe core skill. Teaches agents the full agent-browser API: the navigate-snapshot-interact-re-snapshot workflow, all commands, command chaining, authentication (auth vault and state persistence), sessions, diffing, JavaScript evaluation, annotated screenshots, semantic locators, and configuration.\n\nExample agent interactions:\n\n- \"Open example.com and fill out the contact form\"\n- \"Take a screenshot of the dashboard after logging in\"\n- \"Compare staging and production versions of the homepage\"\n\n## dogfood\n\nA structured workflow for exploratory testing. The agent opens a target URL, systematically explores the app (navigating pages, testing forms, clicking buttons, checking console errors), and documents every issue it finds with:\n\n- Numbered repro steps\n- Step-by-step screenshots\n- Repro videos for interactive bugs\n- Severity classification\n\nThe output is a markdown report in an output directory, ready to hand to the responsible team. Run it with a single prompt like \"dogfood vercel.com\" or \"QA http://localhost:3000 — focus on the billing page\".\n\n## electron\n\nElectron apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) are built on Chromium and expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to. This skill teaches agents how to launch or connect to any Electron app, then use the standard snapshot-interact workflow to automate it. Launch the app with `--remote-debugging-port`, connect, and use the standard snapshot-interact workflow. This is the foundation that the **slack** skill builds on.\n\n## slack\n\nBrowser-based Slack automation. Connects to an existing Slack session (via `agent-browser connect 9222`) or opens Slack in a new browser, then uses snapshots and element refs to navigate the UI. Covers checking unreads, navigating channels and DMs, searching conversations, extracting message data, and taking screenshots — all without needing Slack API tokens or bot setup.\n\n## vercel-sandbox\n\nRun agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs. A Linux VM spins up on demand, executes browser commands, and shuts down automatically. Works with any Vercel-deployed framework (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.).\n\nKey features:\n\n- Sandbox snapshots for sub-second startup (pre-install system deps, agent-browser, and Chromium)\n- Multi-step workflows with persistent state between commands\n- Automatic OIDC authentication on Vercel, or explicit credentials for local dev\n- Scheduled workflows via Vercel Cron Jobs\n\nGet started with the `@vercel/sandbox` package and the `withBrowser` helper pattern. See the `examples/environments/` directory in the repo for a working demo app.\n\n## Source\n\nAll skill files are in the [`skills/`](https://github.com/vercel-labs/agent-browser/tree/main/skills) directory of the repository.\n"
  },
  {
    "path": "docs/src/app/snapshots/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"snapshots\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/snapshots/page.mdx",
    "content": "# Snapshots\n\nThe `snapshot` command returns a compact accessibility tree with refs for element interaction.\n\n## Options\n\nFilter output to reduce size:\n\n```bash\nagent-browser snapshot                    # Full accessibility tree\nagent-browser snapshot -i                 # Interactive elements only (recommended)\nagent-browser snapshot -i -C              # Include cursor-interactive elements\nagent-browser snapshot -c                 # Compact (remove empty elements)\nagent-browser snapshot -d 3               # Limit depth to 3 levels\nagent-browser snapshot -s \"#main\"         # Scope to CSS selector\nagent-browser snapshot -i -c -d 5         # Combine options\n```\n\n<table>\n  <thead>\n    <tr><th>Option</th><th>Description</th></tr>\n  </thead>\n  <tbody>\n    <tr><td><code>-i, --interactive</code></td><td>Only interactive elements (buttons, links, inputs)</td></tr>\n    <tr><td><code>-C, --cursor</code></td><td>Include cursor-interactive elements (cursor:pointer, onclick, tabindex)</td></tr>\n    <tr><td><code>-c, --compact</code></td><td>Remove empty structural elements</td></tr>\n    <tr><td><code>-d, --depth</code></td><td>Limit tree depth</td></tr>\n    <tr><td><code>-s, --selector</code></td><td>Scope to CSS selector</td></tr>\n  </tbody>\n</table>\n\n## Cursor-interactive elements\n\nMany modern web apps use custom clickable elements (divs, spans) instead of standard buttons or links.\nThe `-C` flag detects these by looking for:\n\n- `cursor: pointer` CSS style\n- `onclick` attribute or handler\n- `tabindex` attribute (keyboard focusable)\n\n```bash\nagent-browser snapshot -i -C\n# Output includes:\n# @e1 [button] \"Submit\"\n# @e2 [link] \"Learn more\"\n# Cursor-interactive elements:\n# @e3 [clickable] \"Menu Item\" [cursor:pointer, onclick]\n# @e4 [clickable] \"Card\" [cursor:pointer]\n```\n\n## Output format\n\nThe default text output is compact and AI-friendly:\n\n```bash\nagent-browser snapshot -i\n# Output:\n# @e1 [heading] \"Example Domain\" [level=1]\n# @e2 [button] \"Submit\"\n# @e3 [input type=\"email\"] placeholder=\"Email\"\n# @e4 [link] \"Learn more\"\n```\n\n## Using refs\n\nRefs from the snapshot map directly to commands:\n\n```bash\nagent-browser click @e2      # Click the Submit button\nagent-browser fill @e3 \"a@b.com\"  # Fill the email input\nagent-browser get text @e1        # Get heading text\n```\n\n## Ref lifecycle\n\nRefs are invalidated when the page changes. Always re-snapshot after navigation or DOM updates:\n\n```bash\nagent-browser click @e4      # Navigates to new page\nagent-browser snapshot -i    # Get fresh refs\nagent-browser click @e1      # Use new refs\n```\n\n## Annotated screenshots\n\nFor visual context alongside text snapshots, use `screenshot --annotate` to overlay numbered labels on interactive elements. Each label `[N]` maps to ref `@eN`:\n\nIn native mode, annotated screenshots currently work on the CDP-backed browser path (Chromium/Lightpanda). The Safari/WebDriver backend does not yet support `--annotate`.\n\n```bash\nagent-browser screenshot --annotate ./page.png\n# -> Screenshot saved to ./page.png\n#    [1] @e1 button \"Submit\"\n#    [2] @e2 link \"Home\"\n#    [3] @e3 textbox \"Email\"\nagent-browser click @e2\n```\n\nAnnotated screenshots also cache refs, so you can interact with elements immediately. This is useful when the text snapshot is insufficient -- unlabeled icons, canvas content, or visual layout verification.\n\n## Iframes\n\nSnapshots automatically detect and inline iframe content. Each `Iframe` node in the main frame is resolved and its child accessibility tree is included directly beneath it. Refs assigned to elements inside iframes carry frame context, so interactions work without switching frames first.\n\n```bash\nagent-browser snapshot -i\n# @e1 [heading] \"Checkout\"\n# @e2 [Iframe] \"payment-frame\"\n#   @e3 [input] \"Card number\"\n#   @e4 [button] \"Pay\"\n\nagent-browser fill @e3 \"4111111111111111\"\nagent-browser click @e4\n```\n\nOnly one level of iframe nesting is expanded. Cross-origin iframes that block accessibility tree access and empty iframes are silently omitted.\n\nTo scope a snapshot to a single iframe, switch into it first:\n\n```bash\nagent-browser frame @e2\nagent-browser snapshot -i       # Only elements inside that iframe\nagent-browser frame main        # Return to main frame\n```\n\n## Best practices\n\n1. Use `-i` to reduce output to actionable elements\n2. Re-snapshot after page changes to get updated refs\n3. Scope with `-s` for specific page sections\n4. Use `-d` to limit depth on complex pages\n5. Use `screenshot --annotate` when visual context is needed alongside refs\n\n## JSON output\n\nFor programmatic parsing in scripts:\n\n```bash\nagent-browser snapshot --json\n# {\"success\":true,\"data\":{\"snapshot\":\"...\",\"refs\":{\"e1\":{\"role\":\"heading\",\"name\":\"Title\"},...}}}\n```\n\nNote: JSON uses more tokens than text output. The default text format is preferred for AI agents.\n"
  },
  {
    "path": "docs/src/app/streaming/layout.tsx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"streaming\");\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return children;\n}\n"
  },
  {
    "path": "docs/src/app/streaming/page.mdx",
    "content": "# Streaming\n\nStream the browser viewport via WebSocket for live preview or \"pair browsing\"\nwhere a human can watch and interact alongside an AI agent.\n\n## Enable streaming\n\nSet the `AGENT_BROWSER_STREAM_PORT` environment variable to start\na WebSocket server:\n\n```bash\nAGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com\n```\n\nThe server streams viewport frames and accepts input events (mouse, keyboard, touch).\n\n## WebSocket protocol\n\nConnect to `ws://localhost:9223` to receive frames and send input.\n\n### Frame messages\n\nThe server sends frame messages with base64-encoded images:\n\n```json\n{\n  \"type\": \"frame\",\n  \"data\": \"<base64-encoded-jpeg>\",\n  \"metadata\": {\n    \"deviceWidth\": 1280,\n    \"deviceHeight\": 720,\n    \"pageScaleFactor\": 1,\n    \"offsetTop\": 0,\n    \"scrollOffsetX\": 0,\n    \"scrollOffsetY\": 0\n  }\n}\n```\n\n### Status messages\n\nConnection and screencast status:\n\n```json\n{\n  \"type\": \"status\",\n  \"connected\": true,\n  \"screencasting\": true,\n  \"viewportWidth\": 1280,\n  \"viewportHeight\": 720\n}\n```\n\n## Input injection\n\nSend input events to control the browser remotely.\n\n### Mouse events\n\n```json\n// Click\n{\n  \"type\": \"input_mouse\",\n  \"eventType\": \"mousePressed\",\n  \"x\": 100,\n  \"y\": 200,\n  \"button\": \"left\",\n  \"clickCount\": 1\n}\n\n// Release\n{\n  \"type\": \"input_mouse\",\n  \"eventType\": \"mouseReleased\",\n  \"x\": 100,\n  \"y\": 200,\n  \"button\": \"left\"\n}\n\n// Move\n{\n  \"type\": \"input_mouse\",\n  \"eventType\": \"mouseMoved\",\n  \"x\": 150,\n  \"y\": 250\n}\n\n// Scroll\n{\n  \"type\": \"input_mouse\",\n  \"eventType\": \"mouseWheel\",\n  \"x\": 100,\n  \"y\": 200,\n  \"deltaX\": 0,\n  \"deltaY\": 100\n}\n```\n\n### Keyboard events\n\n```json\n// Key down\n{\n  \"type\": \"input_keyboard\",\n  \"eventType\": \"keyDown\",\n  \"key\": \"Enter\",\n  \"code\": \"Enter\"\n}\n\n// Key up\n{\n  \"type\": \"input_keyboard\",\n  \"eventType\": \"keyUp\",\n  \"key\": \"Enter\",\n  \"code\": \"Enter\"\n}\n\n// Type character\n{\n  \"type\": \"input_keyboard\",\n  \"eventType\": \"char\",\n  \"text\": \"a\"\n}\n\n// With modifiers (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)\n{\n  \"type\": \"input_keyboard\",\n  \"eventType\": \"keyDown\",\n  \"key\": \"c\",\n  \"code\": \"KeyC\",\n  \"modifiers\": 2\n}\n```\n\n### Touch events\n\n```json\n// Touch start\n{\n  \"type\": \"input_touch\",\n  \"eventType\": \"touchStart\",\n  \"touchPoints\": [{ \"x\": 100, \"y\": 200 }]\n}\n\n// Touch move\n{\n  \"type\": \"input_touch\",\n  \"eventType\": \"touchMove\",\n  \"touchPoints\": [{ \"x\": 150, \"y\": 250 }]\n}\n\n// Touch end\n{\n  \"type\": \"input_touch\",\n  \"eventType\": \"touchEnd\",\n  \"touchPoints\": []\n}\n\n// Multi-touch (pinch zoom)\n{\n  \"type\": \"input_touch\",\n  \"eventType\": \"touchStart\",\n  \"touchPoints\": [\n    { \"x\": 100, \"y\": 200, \"id\": 0 },\n    { \"x\": 200, \"y\": 200, \"id\": 1 }\n  ]\n}\n```\n\n## Programmatic API\n\nFor advanced use, control streaming directly via the TypeScript API:\n\n```typescript\nimport { BrowserManager } from 'agent-browser';\n\nconst browser = new BrowserManager();\nawait browser.launch({ headless: true });\nawait browser.navigate('https://example.com');\n\n// Start screencast with callback\nawait browser.startScreencast((frame) => {\n  console.log('Frame:', frame.metadata.deviceWidth, 'x', frame.metadata.deviceHeight);\n  // frame.data is base64-encoded image\n}, {\n  format: 'jpeg',  // or 'png'\n  quality: 80,     // 0-100, jpeg only\n  maxWidth: 1280,\n  maxHeight: 720,\n  everyNthFrame: 1\n});\n\n// Inject mouse event\nawait browser.injectMouseEvent({\n  type: 'mousePressed',\n  x: 100,\n  y: 200,\n  button: 'left',\n  clickCount: 1\n});\n\n// Inject keyboard event\nawait browser.injectKeyboardEvent({\n  type: 'keyDown',\n  key: 'Enter',\n  code: 'Enter'\n});\n\n// Inject touch event\nawait browser.injectTouchEvent({\n  type: 'touchStart',\n  touchPoints: [{ x: 100, y: 200 }]\n});\n\n// Check if screencasting\nconsole.log('Active:', browser.isScreencasting());\n\n// Stop screencast\nawait browser.stopScreencast();\n```\n\n## Use cases\n\n- **Pair browsing** - Human watches and assists AI agent in real-time\n- **Remote preview** - View browser output in a separate UI\n- **Recording** - Capture frames for video generation\n- **Mobile testing** - Inject touch events for mobile emulation\n- **Accessibility testing** - Manual interaction during automated tests\n"
  },
  {
    "path": "docs/src/components/code-block.tsx",
    "content": "import { codeToHtml } from \"shiki\";\nimport { CopyButton } from \"./copy-button\";\n\nconst vercelDarkTheme = {\n  name: \"vercel-dark\",\n  type: \"dark\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#EDEDED\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#A1A1A1\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\", \"punctuation.definition.string\"],\n      settings: { foreground: \"#00CA50\" },\n    },\n    {\n      scope: [\"constant.numeric\", \"constant.language.boolean\", \"constant.language.null\"],\n      settings: { foreground: \"#47A8FF\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#FF4D8D\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#FF4D8D\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#C472FB\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"variable.parameter\"],\n      settings: { foreground: \"#FF9300\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#FF4D8D\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n        \"punctuation.support.type.property-name\",\n      ],\n      settings: { foreground: \"#FF4D8D\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#00CA50\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#00CA50\" },\n    },\n  ],\n};\n\nconst vercelLightTheme = {\n  name: \"vercel-light\",\n  type: \"light\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#171717\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#6B7280\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\", \"punctuation.definition.string\"],\n      settings: { foreground: \"#067A6E\" },\n    },\n    {\n      scope: [\"constant.numeric\", \"constant.language.boolean\", \"constant.language.null\"],\n      settings: { foreground: \"#0070C0\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#D6409F\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#D6409F\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#6E56CF\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\"],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"variable.parameter\"],\n      settings: { foreground: \"#B45309\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#D6409F\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#6B7280\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n        \"punctuation.support.type.property-name\",\n      ],\n      settings: { foreground: \"#D6409F\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#067A6E\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#067A6E\" },\n    },\n  ],\n};\n\nconst PLACEHOLDER_PREFIX = \"\\u200B\\u200B\";\nconst PLACEHOLDER_SUFFIX = \"\\u200B\\u200B\";\n\nfunction shieldPlaceholders(code: string): string {\n  return code.replace(/<([\\w|]+)>/g, `${PLACEHOLDER_PREFIX}$1${PLACEHOLDER_SUFFIX}`);\n}\n\nfunction restorePlaceholders(html: string): string {\n  return html.replace(\n    new RegExp(`${PLACEHOLDER_PREFIX}([\\\\w|]+)${PLACEHOLDER_SUFFIX}`, \"g\"),\n    \"&lt;$1&gt;\",\n  );\n}\n\ninterface CodeBlockProps {\n  code: string;\n  lang?: string;\n}\n\nexport async function CodeBlock({ code, lang = \"bash\" }: CodeBlockProps) {\n  const trimmedCode = code.trim();\n  const shielded = shieldPlaceholders(trimmedCode);\n  let html = await codeToHtml(shielded, {\n    lang,\n    themes: {\n      light: vercelLightTheme,\n      dark: vercelDarkTheme,\n    },\n    defaultColor: false,\n  });\n  html = restorePlaceholders(html);\n\n  return (\n    <div className=\"code-block relative group\">\n      <CopyButton code={trimmedCode} />\n      <div dangerouslySetInnerHTML={{ __html: html }} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/copy-button.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\ninterface CopyButtonProps {\n  code: string;\n}\n\nexport function CopyButton({ code }: CopyButtonProps) {\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = async () => {\n    try {\n      await navigator.clipboard.writeText(code);\n      setCopied(true);\n      setTimeout(() => setCopied(false), 2000);\n    } catch (error) {\n      console.error(\"Failed to copy to clipboard:\", error);\n      // Optionally, you could set an error state or show a toast notification here\n    }\n  };\n\n  return (\n    <button\n      onClick={handleCopy}\n      className=\"absolute top-2 right-2 p-1.5 rounded text-[#666] hover:text-[#999] hover:bg-[#333] opacity-0 group-hover:opacity-100 transition-all\"\n      aria-label=\"Copy code\"\n    >\n      {copied ? (\n        <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M5 13l4 4L19 7\" />\n        </svg>\n      ) : (\n        <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/copy-page-button.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\n\nexport function CopyPageButton() {\n  const pathname = usePathname();\n  const [state, setState] = useState<\"idle\" | \"loading\" | \"copied\">(\"idle\");\n\n  const handleCopy = async () => {\n    setState(\"loading\");\n    try {\n      const response = await fetch(\n        `/api/docs-markdown?path=${encodeURIComponent(pathname)}`,\n      );\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch markdown\");\n      }\n      const markdown = await response.text();\n      await navigator.clipboard.writeText(markdown);\n      setState(\"copied\");\n      setTimeout(() => setState(\"idle\"), 2000);\n    } catch {\n      setState(\"idle\");\n    }\n  };\n\n  return (\n    <button\n      onClick={handleCopy}\n      disabled={state === \"loading\"}\n      className=\"flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors disabled:opacity-50\"\n      aria-label=\"Copy page as Markdown\"\n    >\n      {state === \"copied\" ? (\n        <>\n          <svg\n            width=\"14\"\n            height=\"14\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <polyline points=\"20 6 9 17 4 12\" />\n          </svg>\n          Copied\n        </>\n      ) : (\n        <>\n          <svg\n            width=\"14\"\n            height=\"14\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n            <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n          </svg>\n          Copy Page\n        </>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/diff-demo.tsx",
    "content": "\"use client\";\n\nfunction DiffLine({ line }: { line: string }) {\n  if (line.startsWith(\"+ \")) {\n    return <div className=\"text-green-400\">{line}</div>;\n  }\n  if (line.startsWith(\"- \")) {\n    return <div className=\"text-red-400\">{line}</div>;\n  }\n  return <div className=\"opacity-50\">{line}</div>;\n}\n\nfunction CommandLine({ children }: { children: string }) {\n  return (\n    <div>\n      <span className=\"opacity-40\">$ </span>\n      {children}\n    </div>\n  );\n}\n\nfunction Terminal({ children }: { children: React.ReactNode }) {\n  return (\n    <div\n      className=\"rounded border font-mono text-[0.8125rem] leading-[1.7] overflow-x-auto\"\n      style={{\n        background: \"var(--card)\",\n        borderColor: \"var(--border)\",\n        padding: \"0.875rem\",\n      }}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction PageMockup({\n  label,\n  buttonColor,\n  diffMode,\n}: {\n  label: string;\n  buttonColor: string;\n  diffMode?: boolean;\n}) {\n  const dimOpacity = diffMode ? 0.15 : 1;\n  return (\n    <div className=\"flex-1 min-w-0\">\n      <div\n        className=\"text-[0.6875rem] font-medium mb-1.5 text-center\"\n        style={{ color: \"var(--muted-foreground)\" }}\n      >\n        {label}\n      </div>\n      <svg\n        viewBox=\"0 0 160 120\"\n        className=\"w-full rounded border\"\n        style={{ borderColor: \"var(--border)\" }}\n      >\n        <rect width=\"160\" height=\"120\" fill={diffMode ? \"#1a1a1a\" : \"#111\"} />\n\n        {/* Nav bar */}\n        <rect\n          x=\"0\"\n          y=\"0\"\n          width=\"160\"\n          height=\"16\"\n          fill=\"#222\"\n          opacity={dimOpacity}\n        />\n        <rect\n          x=\"8\"\n          y=\"5\"\n          width=\"24\"\n          height=\"6\"\n          rx=\"1\"\n          fill=\"#555\"\n          opacity={dimOpacity}\n        />\n        <rect\n          x=\"120\"\n          y=\"5\"\n          width=\"12\"\n          height=\"6\"\n          rx=\"1\"\n          fill=\"#444\"\n          opacity={dimOpacity}\n        />\n        <rect\n          x=\"136\"\n          y=\"5\"\n          width=\"12\"\n          height=\"6\"\n          rx=\"1\"\n          fill=\"#444\"\n          opacity={dimOpacity}\n        />\n\n        {/* Heading */}\n        <rect\n          x=\"20\"\n          y=\"26\"\n          width=\"80\"\n          height=\"6\"\n          rx=\"1\"\n          fill=\"#666\"\n          opacity={dimOpacity}\n        />\n\n        {/* Subtext */}\n        <rect\n          x=\"30\"\n          y=\"38\"\n          width=\"60\"\n          height=\"4\"\n          rx=\"1\"\n          fill=\"#444\"\n          opacity={dimOpacity}\n        />\n\n        {/* Input field */}\n        <rect\n          x=\"30\"\n          y=\"52\"\n          width=\"100\"\n          height=\"14\"\n          rx=\"2\"\n          fill=\"#1a1a1a\"\n          stroke=\"#333\"\n          strokeWidth=\"0.5\"\n          opacity={dimOpacity}\n        />\n\n        {/* Button -- this is what changes */}\n        {diffMode ? (\n          <>\n            <rect\n              x=\"55\"\n              y=\"76\"\n              width=\"50\"\n              height=\"14\"\n              rx=\"2\"\n              fill=\"#ef4444\"\n              opacity=\"0.85\"\n            />\n            <rect\n              x=\"55\"\n              y=\"76\"\n              width=\"50\"\n              height=\"14\"\n              rx=\"2\"\n              fill=\"none\"\n              stroke=\"#ef4444\"\n              strokeWidth=\"1.5\"\n              strokeDasharray=\"3 2\"\n            />\n          </>\n        ) : (\n          <rect\n            x=\"55\"\n            y=\"76\"\n            width=\"50\"\n            height=\"14\"\n            rx=\"2\"\n            fill={buttonColor}\n          />\n        )}\n        <text\n          x=\"80\"\n          y=\"85.5\"\n          textAnchor=\"middle\"\n          fill=\"white\"\n          fontSize=\"6\"\n          fontFamily=\"system-ui, sans-serif\"\n          opacity={diffMode ? 0.9 : 1}\n        >\n          Submit\n        </text>\n\n        {/* Footer line */}\n        <rect\n          x=\"40\"\n          y=\"102\"\n          width=\"80\"\n          height=\"3\"\n          rx=\"1\"\n          fill=\"#333\"\n          opacity={dimOpacity}\n        />\n      </svg>\n    </div>\n  );\n}\n\nconst snapshotDiffLines = [\n  \"  heading \\\"Sign Up\\\" [ref=e1]\",\n  \"  text \\\"Create your account\\\" [ref=e2]\",\n  \"- textbox \\\"Email\\\" [ref=e3]\",\n  \"+ textbox \\\"Email\\\" [ref=e3]: \\\"test@example.com\\\"\",\n  \"- button \\\"Submit\\\" [ref=e4]\",\n  \"+ button \\\"Submit\\\" [ref=e4] [disabled]\",\n  \"+ status \\\"Sending...\\\" [ref=e7]\",\n  \"  link \\\"Already have an account?\\\" [ref=e5]\",\n];\n\nexport function DiffDemo() {\n  return (\n    <div className=\"grid gap-8 my-8\">\n      {/* Panel 1: Snapshot diff */}\n      <div>\n        <div\n          className=\"text-xs font-medium uppercase tracking-wider mb-3\"\n          style={{ color: \"var(--muted-foreground)\" }}\n        >\n          Verify an action changed the page\n        </div>\n        <Terminal>\n          <div className=\"opacity-60 mb-2\">\n            <CommandLine>agent-browser snapshot -i</CommandLine>\n            <CommandLine>\n              agent-browser fill @e3 &quot;test@example.com&quot;\n            </CommandLine>\n            <CommandLine>agent-browser click @e4</CommandLine>\n          </div>\n          <div className=\"mb-3\">\n            <CommandLine>agent-browser diff snapshot</CommandLine>\n          </div>\n          <div\n            className=\"border-t pt-3\"\n            style={{ borderColor: \"var(--border)\" }}\n          >\n            {snapshotDiffLines.map((line, i) => (\n              <DiffLine key={i} line={line} />\n            ))}\n            <div className=\"mt-2 opacity-60\">\n              <span className=\"text-green-400\">3</span> additions,{\" \"}\n              <span className=\"text-red-400\">2</span> removals,{\" \"}\n              <span>3</span> unchanged\n            </div>\n          </div>\n        </Terminal>\n      </div>\n\n      {/* Panel 2: Screenshot diff */}\n      <div>\n        <div\n          className=\"text-xs font-medium uppercase tracking-wider mb-3\"\n          style={{ color: \"var(--muted-foreground)\" }}\n        >\n          Catch a visual regression\n        </div>\n        <Terminal>\n          <div className=\"mb-3\">\n            <CommandLine>\n              agent-browser diff screenshot --baseline before-deploy.png\n            </CommandLine>\n          </div>\n          <div\n            className=\"border-t pt-3\"\n            style={{ borderColor: \"var(--border)\" }}\n          >\n            <div className=\"text-red-400\">\n              &#x2717; 2.37% pixels differ\n            </div>\n            <div className=\"opacity-50\">\n              Diff image: ~/.agent-browser/tmp/diffs/diff-1708473621.png\n            </div>\n            <div className=\"opacity-50\">\n              <span className=\"text-red-400\">1,137</span> different /{\" \"}\n              48,000 total pixels\n            </div>\n          </div>\n        </Terminal>\n        <div className=\"flex gap-2 mt-3\">\n          <PageMockup label=\"Baseline\" buttonColor=\"#3b82f6\" />\n          <PageMockup label=\"Current\" buttonColor=\"#22c55e\" />\n          <PageMockup label=\"Diff\" buttonColor=\"#ef4444\" diffMode />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/docs-chat.tsx",
    "content": "\"use client\";\n\nimport {\n  useRef,\n  useEffect,\n  useState,\n  useCallback,\n  type PointerEvent as ReactPointerEvent,\n} from \"react\";\nimport { useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport } from \"ai\";\nimport { Streamdown } from \"streamdown\";\nimport Link from \"next/link\";\nimport { Sheet, SheetContent, SheetTitle } from \"@/components/ui/sheet\";\n\nconst STORAGE_KEY = \"docs-chat-messages\";\nconst transport = new DefaultChatTransport({ api: \"/api/docs-chat\" });\n\nconst DESKTOP_DEFAULT_WIDTH = 400;\nconst DESKTOP_MIN_WIDTH = 300;\nconst DESKTOP_MAX_WIDTH = 700;\n\nfunction setCookie(name: string, value: string) {\n  document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`;\n}\n\nconst TOOL_LABELS: Record<\n  string,\n  { label: string; pastLabel: string; argKey?: string }\n> = {\n  readFile: { label: \"Reading\", pastLabel: \"Read\", argKey: \"path\" },\n  bash: { label: \"Running\", pastLabel: \"Ran\", argKey: \"command\" },\n};\n\nfunction isToolPart(part: { type: string }): part is {\n  type: string;\n  toolCallId: string;\n  toolName?: string;\n  state: string;\n  input?: Record<string, unknown>;\n  output?: unknown;\n  errorText?: string;\n} {\n  return part.type.startsWith(\"tool-\") || part.type === \"dynamic-tool\";\n}\n\nfunction getToolName(part: { type: string; toolName?: string }): string {\n  if (part.type === \"dynamic-tool\") return part.toolName ?? \"tool\";\n  return part.type.replace(/^tool-/, \"\");\n}\n\nfunction ToolCallDisplay({\n  part,\n}: {\n  part: {\n    type: string;\n    toolCallId: string;\n    toolName?: string;\n    state: string;\n    input?: Record<string, unknown>;\n    output?: unknown;\n    errorText?: string;\n  };\n}) {\n  const toolName = getToolName(part);\n  const config = TOOL_LABELS[toolName] ?? {\n    label: toolName,\n    pastLabel: toolName,\n  };\n  const isDone = part.state === \"output-available\";\n  const isError = part.state === \"output-error\";\n  const isRunning = !isDone && !isError;\n  const displayLabel = isRunning ? config.label : config.pastLabel;\n\n  const args = (part.input ?? {}) as Record<string, unknown>;\n  const argValue = config.argKey ? args[config.argKey] : undefined;\n  const argPreview =\n    argValue != null\n      ? String(argValue)\n          .replace(/^\\/workspace\\//, \"/\")\n          .replace(/\\.md$/, \"\")\n          .replace(/\\/index$/, \"\") || \"/\"\n      : \"\";\n\n  // Link to the docs page if it's a readFile path\n  const docsLink =\n    toolName === \"readFile\" && argPreview.startsWith(\"/\") ? argPreview : null;\n\n  const argEl = argPreview ? (\n    docsLink ? (\n      <Link href={docsLink} className=\"truncate underline underline-offset-2\">\n        {argPreview}\n      </Link>\n    ) : (\n      <span className=\"truncate\">{argPreview}</span>\n    )\n  ) : null;\n\n  return (\n    <div className=\"text-xs py-0.5 min-w-0\">\n      {isRunning ? (\n        <span className=\"inline-flex items-center gap-1 font-mono text-muted-foreground animate-tool-shimmer min-w-0 max-w-full\">\n          <span className=\"shrink-0\">{displayLabel}</span>\n          {argEl}\n        </span>\n      ) : (\n        <span className=\"inline-flex items-center gap-1 font-mono text-muted-foreground/60 min-w-0 max-w-full\">\n          <span className=\"shrink-0\">{displayLabel}</span>\n          {argEl}\n          {isError && <span className=\"text-destructive\">failed</span>}\n        </span>\n      )}\n    </div>\n  );\n}\n\nconst SUGGESTIONS = [\n  \"What is agent-browser?\",\n  \"How do I install it?\",\n  \"What commands are available?\",\n  \"How do snapshots work?\",\n  \"How do I use CDP mode?\",\n];\n\nexport function DocsChat({\n  defaultOpen = false,\n  defaultWidth = DESKTOP_DEFAULT_WIDTH,\n}: {\n  defaultOpen?: boolean;\n  defaultWidth?: number;\n}) {\n  const [open, setOpen] = useState(defaultOpen);\n  const [input, setInput] = useState(\"\");\n  const [isDesktop, setIsDesktop] = useState(false);\n  const [hasMounted, setHasMounted] = useState(false);\n  const [desktopWidth, setDesktopWidth] = useState(\n    Math.min(DESKTOP_MAX_WIDTH, Math.max(DESKTOP_MIN_WIDTH, defaultWidth)),\n  );\n  const messagesScrollRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const restoredRef = useRef(false);\n  const isDraggingRef = useRef(false);\n\n  const { messages, sendMessage, status, setMessages, error } = useChat({\n    transport,\n  });\n\n  const isLoading = status === \"streaming\" || status === \"submitted\";\n  const showMessages = messages.length > 0 || !!error || isLoading;\n\n  // Detect desktop vs mobile. Close sidebar on mobile if it was open from cookie.\n  useEffect(() => {\n    const mq = window.matchMedia(\"(min-width: 640px)\");\n    setIsDesktop(mq.matches);\n    setHasMounted(true);\n    // If on mobile but sidebar was open from cookie, close it\n    if (!mq.matches && defaultOpen) {\n      setOpen(false);\n    }\n    const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);\n    mq.addEventListener(\"change\", handler);\n    return () => mq.removeEventListener(\"change\", handler);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Persist open state to cookie (only after mount to avoid overwriting on mobile)\n  useEffect(() => {\n    if (hasMounted) {\n      setCookie(\"docs-chat-open\", String(open));\n    }\n  }, [open, hasMounted]);\n\n  // Push page content on desktop when pane is open.\n  // Use padding on body so the page scrollbar stays at the viewport edge (behind the sidebar)\n  // instead of appearing right next to the sidebar's scrollbar.\n  useEffect(() => {\n    const body = document.body;\n    if (isDesktop && open) {\n      body.style.paddingRight = `${desktopWidth}px`;\n      if (!isDraggingRef.current) {\n        body.style.transition = \"padding-right 150ms ease\";\n      }\n    } else if (isDesktop) {\n      body.style.paddingRight = \"0px\";\n      body.style.transition = \"padding-right 150ms ease\";\n    }\n    return () => {\n      body.style.paddingRight = \"0px\";\n      body.style.transition = \"\";\n    };\n  }, [isDesktop, open, desktopWidth]);\n\n  // Resize handle drag\n  const handleResizePointerDown = useCallback(\n    (e: ReactPointerEvent<HTMLDivElement>) => {\n      e.preventDefault();\n      isDraggingRef.current = true;\n      document.documentElement.style.transition = \"none\";\n      const startX = e.clientX;\n      const startWidth = desktopWidth;\n\n      const onPointerMove = (ev: globalThis.PointerEvent) => {\n        const delta = startX - ev.clientX;\n        const newWidth = Math.min(\n          DESKTOP_MAX_WIDTH,\n          Math.max(DESKTOP_MIN_WIDTH, startWidth + delta),\n        );\n        setDesktopWidth(newWidth);\n      };\n\n      const onPointerUp = () => {\n        isDraggingRef.current = false;\n        document.documentElement.style.transition = \"\";\n        document.removeEventListener(\"pointermove\", onPointerMove);\n        document.removeEventListener(\"pointerup\", onPointerUp);\n      };\n\n      document.addEventListener(\"pointermove\", onPointerMove);\n      document.addEventListener(\"pointerup\", onPointerUp);\n    },\n    [desktopWidth],\n  );\n\n  // Persist width to cookie\n  useEffect(() => {\n    setCookie(\"docs-chat-width\", String(desktopWidth));\n  }, [desktopWidth]);\n\n  // Restore messages from sessionStorage on mount\n  useEffect(() => {\n    if (restoredRef.current) return;\n    restoredRef.current = true;\n    try {\n      const stored = sessionStorage.getItem(STORAGE_KEY);\n      if (stored) {\n        const parsed = JSON.parse(stored);\n        if (Array.isArray(parsed) && parsed.length > 0) {\n          setMessages(parsed);\n        }\n      }\n    } catch {\n      // ignore parse errors\n    }\n  }, [setMessages]);\n\n  // Save completed messages to sessionStorage\n  useEffect(() => {\n    if (!restoredRef.current) return;\n    if (isLoading) return;\n    if (messages.length === 0) {\n      sessionStorage.removeItem(STORAGE_KEY);\n      return;\n    }\n    try {\n      sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages));\n    } catch {\n      // ignore quota errors\n    }\n  }, [messages, isLoading]);\n\n  // Cmd+K to open sidebar and focus prompt, Escape to close\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"i\" && (e.metaKey || e.ctrlKey)) {\n        e.preventDefault();\n        setOpen((prev) => {\n          if (!prev) {\n            setTimeout(() => inputRef.current?.focus(), 200);\n          }\n          return !prev;\n        });\n      }\n      if (e.key === \"Escape\" && open && isDesktop) {\n        setOpen(false);\n      }\n    };\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [open, isDesktop]);\n\n  // Auto-focus input when opened\n  useEffect(() => {\n    if (open) {\n      const timer = setTimeout(() => inputRef.current?.focus(), 200);\n      return () => clearTimeout(timer);\n    }\n  }, [open]);\n\n  // Auto-open when error occurs\n  useEffect(() => {\n    if (error) setOpen(true);\n  }, [error]);\n\n  // Scroll to bottom when messages change or error occurs\n  useEffect(() => {\n    const el = messagesScrollRef.current;\n    if (!el) return;\n    requestAnimationFrame(() => {\n      el.scrollTop = el.scrollHeight;\n    });\n  }, [messages, error]);\n\n  const handleSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault();\n      if (!input.trim() || isLoading) return;\n      sendMessage({ text: input });\n      setInput(\"\");\n    },\n    [input, isLoading, sendMessage],\n  );\n\n  const handleClear = useCallback(() => {\n    setMessages([]);\n    sessionStorage.removeItem(STORAGE_KEY);\n  }, [setMessages]);\n\n  const hasVisibleContent = (\n    parts: (typeof messages)[number][\"parts\"],\n  ): boolean => {\n    return parts.some(\n      (p) => (p.type === \"text\" && p.text.length > 0) || isToolPart(p),\n    );\n  };\n\n  // Shared chat panel content used by both desktop and mobile\n  const chatPanel = (\n    <>\n      {/* Header */}\n      <div className=\"flex items-center justify-between px-4 py-3 border-b border-border/50 shrink-0\">\n        <span className=\"text-sm font-medium\">agent-browser Docs</span>\n        <div className=\"flex items-center gap-3\">\n          {showMessages && (\n            <button\n              onClick={handleClear}\n              className=\"text-xs text-muted-foreground hover:text-foreground transition-colors\"\n              aria-label=\"Clear conversation\"\n            >\n              Clear\n            </button>\n          )}\n          <button\n            onClick={() => setOpen(false)}\n            className=\"text-muted-foreground hover:text-foreground transition-colors\"\n            aria-label=\"Close panel\"\n          >\n            <svg\n              width=\"14\"\n              height=\"14\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n              <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n\n      {/* Content: suggestions or messages */}\n      {showMessages ? (\n        <div\n          ref={messagesScrollRef}\n          className=\"flex-1 min-h-0 p-4 space-y-4 overflow-y-auto\"\n        >\n          {messages.map((message) => {\n            if (!hasVisibleContent(message.parts)) return null;\n            return (\n              <div key={message.id}>\n                {message.role === \"user\" ? (\n                  <div className=\"text-sm text-muted-foreground whitespace-pre-wrap leading-relaxed\">\n                    {message.parts\n                      .filter(\n                        (p): p is Extract<typeof p, { type: \"text\" }> =>\n                          p.type === \"text\",\n                      )\n                      .map((p) => p.text)\n                      .join(\"\")}\n                  </div>\n                ) : (\n                  <div className=\"space-y-2\">\n                    {message.parts.map((part, i) => {\n                      if (part.type === \"text\" && part.text) {\n                        return (\n                          <div\n                            key={i}\n                            className=\"docs-chat-content text-sm text-foreground leading-relaxed prose prose-sm dark:prose-invert max-w-none\"\n                          >\n                            <Streamdown>{part.text}</Streamdown>\n                          </div>\n                        );\n                      }\n                      if (isToolPart(part)) {\n                        return (\n                          <ToolCallDisplay key={part.toolCallId} part={part} />\n                        );\n                      }\n                      return null;\n                    })}\n                  </div>\n                )}\n              </div>\n            );\n          })}\n          {error && (\n            <div className=\"text-sm text-destructive/80 bg-destructive/10 rounded-md px-3 py-2\">\n              {(() => {\n                try {\n                  const parsed = JSON.parse(error.message);\n                  return parsed.message || parsed.error || error.message;\n                } catch {\n                  return (\n                    error.message || \"Something went wrong. Please try again.\"\n                  );\n                }\n              })()}\n            </div>\n          )}\n        </div>\n      ) : (\n        <div className=\"flex-1 min-h-0 flex flex-col\">\n          <div className=\"flex flex-wrap gap-2 p-4\">\n            {SUGGESTIONS.map((s) => (\n              <button\n                key={s}\n                type=\"button\"\n                onClick={() => {\n                  sendMessage({ text: s });\n                }}\n                className=\"text-xs px-3 py-1.5 rounded-full border bg-secondary font-medium text-muted-foreground hover:text-foreground transition-colors\"\n              >\n                {s}\n              </button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {/* Input bar */}\n      <form\n        onSubmit={handleSubmit}\n        className=\"flex items-end gap-2 px-4 py-3 border-t border-border/50 shrink-0\"\n      >\n        <textarea\n          ref={inputRef}\n          value={input}\n          onChange={(e) => {\n            setInput(e.target.value);\n            e.target.style.height = \"auto\";\n            e.target.style.height = `${e.target.scrollHeight}px`;\n          }}\n          rows={1}\n          enterKeyHint=\"send\"\n          placeholder=\"Ask a question...\"\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && !e.shiftKey) {\n              e.preventDefault();\n              handleSubmit(e);\n            }\n          }}\n          className=\"flex-1 bg-transparent text-base sm:text-sm text-foreground outline-none disabled:opacity-50 resize-none max-h-32 leading-relaxed placeholder:text-muted-foreground\"\n        />\n        <button\n          type=\"submit\"\n          disabled={isLoading || !input.trim()}\n          className=\"bg-primary text-primary-foreground rounded-full p-1.5 hover:bg-primary/90 transition-colors disabled:opacity-30 shrink-0\"\n          aria-label=\"Send message\"\n        >\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n            <polyline points=\"5 12 12 5 19 12\" />\n          </svg>\n        </button>\n      </form>\n    </>\n  );\n\n  return (\n    <>\n      {/* Ask AI trigger button */}\n      {!open && (\n        <button\n          onClick={() => setOpen(true)}\n          className=\"fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground shadow-lg hover:opacity-90 transition-opacity text-sm font-medium\"\n          aria-label=\"Ask AI\"\n        >\n          Ask AI\n          <kbd className=\"hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono\">\n            <span>&#8984;</span>I\n          </kbd>\n        </button>\n      )}\n\n      {/* Desktop: resizable side pane -- always rendered, hidden on mobile via CSS */}\n      <aside\n        className={`hidden sm:flex fixed top-0 right-0 bottom-0 z-40 border-l border-border/50 bg-background transition-transform duration-150 ease-in-out ${open ? \"translate-x-0\" : \"translate-x-full\"}`}\n        style={{ width: desktopWidth }}\n        aria-hidden={!open}\n      >\n        {/* Resize handle */}\n        <div\n          onPointerDown={handleResizePointerDown}\n          className=\"absolute top-0 bottom-0 left-0 w-1.5 cursor-col-resize hover:bg-ring/30 active:bg-ring/50 transition-colors z-10\"\n        />\n        <div className=\"flex flex-col flex-1 min-w-0\">{chatPanel}</div>\n      </aside>\n\n      {/* Mobile: Sheet overlay/drawer -- only after mount to avoid flash on desktop */}\n      {hasMounted && !isDesktop && (\n        <Sheet open={open} onOpenChange={setOpen}>\n          <SheetContent\n            side=\"right\"\n            showCloseButton={false}\n            overlayClassName=\"bg-background!\"\n            className=\"inset-0! w-full! h-full! max-w-none! border-l-0! p-0 flex flex-col\"\n            style={{ backgroundColor: \"var(--background)\", opacity: 1 }}\n          >\n            <SheetTitle className=\"sr-only\">AI Chat</SheetTitle>\n            {chatPanel}\n          </SheetContent>\n        </Sheet>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/docs-mobile-nav.tsx",
    "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport {\n  Sheet,\n  SheetTrigger,\n  SheetContent,\n  SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { navigation, allDocsPages } from \"@/lib/docs-navigation\";\n\nexport function DocsMobileNav() {\n  const [open, setOpen] = useState(false);\n  const pathname = usePathname();\n\n  const currentPage = useMemo(() => {\n    const page = allDocsPages.find((p) => p.href === pathname);\n    return page ?? allDocsPages[0];\n  }, [pathname]);\n\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger className=\"lg:hidden sticky top-14 z-40 w-full px-6 py-3 bg-background/80 backdrop-blur-sm border-b border-border flex items-center justify-between focus:outline-none\">\n        <div className=\"text-sm font-medium\">{currentPage?.name}</div>\n        <div className=\"w-8 h-8 flex items-center justify-center\">\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className=\"text-muted-foreground\"\n          >\n            <line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\" />\n            <line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\" />\n            <line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\" />\n            <line x1=\"3\" y1=\"6\" x2=\"3.01\" y2=\"6\" />\n            <line x1=\"3\" y1=\"12\" x2=\"3.01\" y2=\"12\" />\n            <line x1=\"3\" y1=\"18\" x2=\"3.01\" y2=\"18\" />\n          </svg>\n        </div>\n      </SheetTrigger>\n      <SheetContent side=\"left\" showCloseButton={false} className=\"overflow-y-auto p-6\">\n        <SheetTitle className=\"mb-6\">Table of Contents</SheetTitle>\n        <nav className=\"space-y-6\">\n          {navigation.map((section, sectionIndex) => (\n            <div key={section.title ?? sectionIndex}>\n              {section.title && (\n                <h4 className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2\">\n                  {section.title}\n                </h4>\n              )}\n              <ul className=\"space-y-1\">\n                {section.items.map((item) => (\n                  <li key={item.href}>\n                    <Link\n                      href={item.href}\n                      onClick={() => setOpen(false)}\n                      className={`text-sm block py-2 transition-colors ${\n                        pathname === item.href\n                          ? \"text-primary font-medium\"\n                          : \"text-muted-foreground hover:text-foreground\"\n                      }`}\n                    >\n                      {item.name}\n                    </Link>\n                  </li>\n                ))}\n              </ul>\n            </div>\n          ))}\n        </nav>\n      </SheetContent>\n    </Sheet>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/docs-sidebar.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { cn } from \"@/lib/utils\";\nimport { navigation } from \"@/lib/docs-navigation\";\n\nexport function DocsSidebar() {\n  const pathname = usePathname();\n\n  return (\n    <nav className=\"space-y-6 pb-8\">\n      {navigation.map((section, sectionIndex) => (\n        <div key={section.title ?? sectionIndex}>\n          {section.title && (\n            <h4 className=\"text-xs font-normal text-muted-foreground/50 uppercase tracking-wider mb-2\">\n              {section.title}\n            </h4>\n          )}\n          <ul className=\"space-y-1\">\n            {section.items.map((item) => {\n              const isActive = pathname === item.href;\n              return (\n                <li key={item.href}>\n                  <Link\n                    href={item.href}\n                    className={cn(\n                      \"text-sm transition-colors block py-1\",\n                      isActive\n                        ? \"text-primary font-medium\"\n                        : \"text-muted-foreground hover:text-foreground\",\n                    )}\n                  >\n                    {item.name}\n                  </Link>\n                </li>\n              );\n            })}\n          </ul>\n        </div>\n      ))}\n    </nav>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/header.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { ThemeToggle } from \"./theme-toggle\";\nimport { Search } from \"./search\";\n\nexport function Header() {\n  return (\n    <header className=\"sticky top-0 z-50 bg-white/90 backdrop-blur-sm dark:bg-neutral-950/90\">\n      <div className=\"flex h-14 items-center justify-between px-4 gap-6\">\n        <div className=\"flex items-center gap-2\">\n          <Link href=\"https://vercel.com\" title=\"Made with love by Vercel\">\n            <svg\n              data-testid=\"geist-icon\"\n              height=\"18\"\n              strokeLinejoin=\"round\"\n              viewBox=\"0 0 16 16\"\n              width=\"18\"\n              style={{ color: \"currentcolor\" }}\n            >\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M8 1L16 15H0L8 1Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </Link>\n          <span className=\"text-neutral-300 dark:text-neutral-700\">\n            <svg\n              data-testid=\"geist-icon\"\n              height=\"16\"\n              strokeLinejoin=\"round\"\n              viewBox=\"0 0 16 16\"\n              width=\"16\"\n              style={{ color: \"currentcolor\" }}\n            >\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n          </span>\n          <Link href=\"/\">\n            <span\n              className=\"font-medium tracking-tight text-lg\"\n              style={{ fontFamily: \"var(--font-geist-pixel-square)\" }}\n            >\n              agent-browser\n            </span>\n          </Link>\n        </div>\n        <nav className=\"flex items-center gap-4\">\n          <Search />\n          <a\n            href=\"https://github.com/vercel-labs/agent-browser\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"flex items-center gap-1.5 text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100\"\n          >\n            <svg\n              viewBox=\"0 0 16 16\"\n              className=\"h-4 w-4\"\n              fill=\"currentColor\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n            </svg>\n            <span>23k</span>\n          </a>\n          <a\n            href=\"https://www.npmjs.com/package/agent-browser\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100\"\n          >\n            npm\n          </a>\n          <ThemeToggle />\n        </nav>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/search.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { Dialog, DialogContent, DialogTitle } from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\n\ntype SearchResult = {\n  title: string;\n  href: string;\n  section: string;\n  snippet: string;\n};\n\nexport function Search() {\n  const router = useRouter();\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [results, setResults] = useState<SearchResult[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [activeIndex, setActiveIndex] = useState(0);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n  const abortRef = useRef<AbortController | null>(null);\n\n  const navigate = useCallback(\n    (href: string) => {\n      setOpen(false);\n      setQuery(\"\");\n      setResults([]);\n      router.push(href);\n    },\n    [router],\n  );\n\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n        e.preventDefault();\n        setOpen((prev) => !prev);\n      }\n    }\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, []);\n\n  useEffect(() => {\n    if (open) {\n      setTimeout(() => inputRef.current?.focus(), 0);\n    } else {\n      setQuery(\"\");\n      setResults([]);\n    }\n  }, [open]);\n\n  useEffect(() => {\n    const q = query.trim();\n    if (!q) {\n      setResults([]);\n      setLoading(false);\n      return;\n    }\n\n    setLoading(true);\n    abortRef.current?.abort();\n    const controller = new AbortController();\n    abortRef.current = controller;\n\n    const timeout = setTimeout(async () => {\n      try {\n        const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {\n          signal: controller.signal,\n        });\n        if (res.ok) {\n          const data = await res.json();\n          setResults(data.results);\n        }\n      } catch {\n        // aborted or network error\n      } finally {\n        if (!controller.signal.aborted) {\n          setLoading(false);\n        }\n      }\n    }, 150);\n\n    return () => {\n      clearTimeout(timeout);\n      controller.abort();\n    };\n  }, [query]);\n\n  useEffect(() => {\n    setActiveIndex(0);\n  }, [results]);\n\n  function handleKeyDown(e: React.KeyboardEvent) {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setActiveIndex((i) => Math.min(i + 1, results.length - 1));\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setActiveIndex((i) => Math.max(i - 1, 0));\n    } else if (e.key === \"Enter\" && results[activeIndex]) {\n      e.preventDefault();\n      navigate(results[activeIndex].href);\n    }\n  }\n\n  useEffect(() => {\n    const active = listRef.current?.querySelector(\"[data-active='true']\");\n    active?.scrollIntoView({ block: \"nearest\" });\n  }, [activeIndex]);\n\n  const hasQuery = query.trim().length > 0;\n\n  return (\n    <>\n      <button\n        onClick={() => setOpen(true)}\n        className=\"hidden sm:flex items-center gap-2 rounded-md border border-border/50 bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/25 transition-colors\"\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"11\" cy=\"11\" r=\"8\" />\n          <path d=\"m21 21-4.3-4.3\" />\n        </svg>\n        Search docs\n        <kbd className=\"pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border/50 bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground\">\n          <span>&#8984;</span>K\n        </kbd>\n      </button>\n\n      <button\n        onClick={() => setOpen(true)}\n        className=\"sm:hidden flex items-center text-muted-foreground hover:text-foreground transition-colors\"\n        aria-label=\"Search docs\"\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"11\" cy=\"11\" r=\"8\" />\n          <path d=\"m21 21-4.3-4.3\" />\n        </svg>\n      </button>\n\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent showCloseButton={false} className=\"gap-0 p-0 sm:max-w-lg\">\n          <DialogTitle className=\"sr-only\">Search documentation</DialogTitle>\n          <div className=\"flex items-center gap-2 border-b border-border/50 px-3\">\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"16\"\n              height=\"16\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className=\"shrink-0 text-muted-foreground\"\n            >\n              <circle cx=\"11\" cy=\"11\" r=\"8\" />\n              <path d=\"m21 21-4.3-4.3\" />\n            </svg>\n            <input\n              ref={inputRef}\n              value={query}\n              onChange={(e) => setQuery(e.target.value)}\n              onKeyDown={handleKeyDown}\n              placeholder=\"Search docs...\"\n              className=\"flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground\"\n            />\n            {query && (\n              <button\n                onClick={() => setQuery(\"\")}\n                className=\"text-muted-foreground hover:text-foreground\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"14\"\n                  height=\"14\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <path d=\"M18 6 6 18\" />\n                  <path d=\"m6 6 12 12\" />\n                </svg>\n              </button>\n            )}\n          </div>\n\n          <div\n            ref={listRef}\n            className=\"max-h-[min(60vh,400px)] overflow-y-auto p-2\"\n          >\n            {loading && hasQuery ? (\n              <div className=\"flex items-center justify-center py-6\">\n                <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent\" />\n              </div>\n            ) : hasQuery && results.length === 0 ? (\n              <p className=\"py-6 text-center text-sm text-muted-foreground\">\n                No results found.\n              </p>\n            ) : !hasQuery ? (\n              <p className=\"py-6 text-center text-sm text-muted-foreground\">\n                Type to search documentation...\n              </p>\n            ) : (\n              results.map((item, i) => (\n                <button\n                  key={item.href}\n                  data-active={i === activeIndex}\n                  onClick={() => navigate(item.href)}\n                  onMouseEnter={() => setActiveIndex(i)}\n                  className={cn(\n                    \"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors\",\n                    i === activeIndex\n                      ? \"bg-muted text-foreground\"\n                      : \"text-foreground\",\n                  )}\n                >\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"text-sm font-medium\">{item.title}</span>\n                    {item.section && (\n                      <span className=\"shrink-0 text-xs text-muted-foreground\">\n                        {item.section}\n                      </span>\n                    )}\n                  </div>\n                  {item.snippet && (\n                    <span className=\"line-clamp-2 text-xs text-muted-foreground leading-relaxed\">\n                      {item.snippet}\n                    </span>\n                  )}\n                </button>\n              ))\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n  return (\n    <NextThemesProvider\n      attribute=\"class\"\n      defaultTheme=\"dark\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      {children}\n    </NextThemesProvider>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\n\nexport function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return <div className=\"w-8 h-8\" />;\n  }\n\n  return (\n    <button\n      onClick={() => setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n      className=\"w-8 h-8 flex items-center justify-center rounded-md text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-800\"\n      aria-label=\"Toggle theme\"\n    >\n      {theme === \"dark\" ? (\n        <svg\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"12\" cy=\"12\" r=\"4\" />\n          <path d=\"M12 2v2\" />\n          <path d=\"M12 20v2\" />\n          <path d=\"m4.93 4.93 1.41 1.41\" />\n          <path d=\"m17.66 17.66 1.41 1.41\" />\n          <path d=\"M2 12h2\" />\n          <path d=\"M20 12h2\" />\n          <path d=\"m6.34 17.66-1.41 1.41\" />\n          <path d=\"m19.07 4.93-1.41 1.41\" />\n        </svg>\n      ) : (\n        <svg\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <DialogPortal>\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-border/50 p-6 shadow-lg duration-200 outline-none sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n            <svg\n              width=\"16\"\n              height=\"16\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n              <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n            </svg>\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  );\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle };\n"
  },
  {
    "path": "docs/src/components/ui/sheet.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Dialog as SheetPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  showCloseButton = true,\n  overlayClassName,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n  showCloseButton?: boolean\n  overlayClassName?: string\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay className={overlayClassName} />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r border-border/50 sm:max-w-sm\",\n          side === \"top\" &&\n            \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n            <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n              <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n              <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n            </svg>\n            <span className=\"sr-only\">Close</span>\n          </SheetPrimitive.Close>\n        )}\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  )\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n}\n"
  },
  {
    "path": "docs/src/lib/docs-navigation.ts",
    "content": "export type NavItem = {\n  name: string;\n  href: string;\n};\n\nexport type NavSection = {\n  title: string | null;\n  items: NavItem[];\n};\n\nexport const navigation: NavSection[] = [\n  {\n    title: null,\n    items: [\n      { name: \"Introduction\", href: \"/\" },\n      { name: \"Installation\", href: \"/installation\" },\n      { name: \"Quick Start\", href: \"/quick-start\" },\n      { name: \"Skills\", href: \"/skills\" },\n    ],\n  },\n  {\n    title: \"Reference\",\n    items: [\n      { name: \"Commands\", href: \"/commands\" },\n      { name: \"Configuration\", href: \"/configuration\" },\n      { name: \"Selectors\", href: \"/selectors\" },\n      { name: \"Snapshots\", href: \"/snapshots\" },\n    ],\n  },\n  {\n    title: \"Features\",\n    items: [\n      { name: \"Sessions\", href: \"/sessions\" },\n      { name: \"Diffing\", href: \"/diffing\" },\n      { name: \"CDP Mode\", href: \"/cdp-mode\" },\n      { name: \"Streaming\", href: \"/streaming\" },\n      { name: \"Profiler\", href: \"/profiler\" },\n      { name: \"iOS Simulator\", href: \"/ios\" },\n      { name: \"Security\", href: \"/security\" },\n      { name: \"Next.js + Vercel\", href: \"/next\" },\n      { name: \"Native Mode\", href: \"/native-mode\" },\n    ],\n  },\n  {\n    title: \"Providers\",\n    items: [\n      { name: \"Browser Use\", href: \"/providers/browser-use\" },\n      { name: \"Browserbase\", href: \"/providers/browserbase\" },\n      { name: \"Browserless\", href: \"/providers/browserless\" },\n      { name: \"Kernel\", href: \"/providers/kernel\" },\n    ],\n  },\n  {\n    title: \"Engines\",\n    items: [\n      { name: \"Chrome\", href: \"/engines/chrome\" },\n      { name: \"Lightpanda\", href: \"/engines/lightpanda\" },\n    ],\n  },\n  {\n    title: null,\n    items: [{ name: \"Changelog\", href: \"/changelog\" }],\n  },\n];\n\nexport const allDocsPages: NavItem[] = navigation.flatMap(\n  (section) => section.items\n);\n"
  },
  {
    "path": "docs/src/lib/mdx-to-markdown.ts",
    "content": "/**\n * Converts raw MDX content to clean Markdown suitable for AI agents.\n *\n * Strips export/import statements and standalone JSX divs with className\n * attributes, passing everything else through as valid Markdown.\n */\nexport function mdxToCleanMarkdown(raw: string): string {\n  const lines = raw.split(\"\\n\");\n  const out: string[] = [];\n  let inJsxBlock = false;\n  let jsxDepth = 0;\n\n  for (const line of lines) {\n    const trimmed = line.trim();\n\n    if (trimmed.startsWith(\"export \") || trimmed.startsWith(\"import \")) {\n      continue;\n    }\n\n    if (\n      !inJsxBlock &&\n      trimmed.startsWith(\"<div \") &&\n      trimmed.includes(\"className=\")\n    ) {\n      inJsxBlock = true;\n      jsxDepth = 1;\n      continue;\n    }\n\n    if (inJsxBlock) {\n      const opens = (line.match(/<div[\\s>]/g) || []).length;\n      const closes = (line.match(/<\\/div>/g) || []).length;\n      jsxDepth += opens - closes;\n      if (jsxDepth <= 0) {\n        inJsxBlock = false;\n        jsxDepth = 0;\n      }\n      continue;\n    }\n\n    out.push(line);\n  }\n\n  let result = out.join(\"\\n\");\n  result = result.replace(/^\\n+/, \"\\n\").trim();\n  return result;\n}\n"
  },
  {
    "path": "docs/src/lib/page-metadata.ts",
    "content": "import type { Metadata } from \"next\";\nimport { PAGE_TITLES } from \"./page-titles\";\n\nconst DESCRIPTION =\n  \"Headless browser automation CLI for AI agents\";\n\nexport function pageMetadata(slug: string): Metadata {\n  const title = PAGE_TITLES[slug];\n  if (!title) return {};\n\n  const displayTitle = title.replace(/\\n/g, \" \");\n  const fullTitle = `${displayTitle} | agent-browser`;\n  const ogImageUrl = slug ? `/og/${slug}` : \"/og\";\n\n  return {\n    title: displayTitle,\n    openGraph: {\n      type: \"website\",\n      locale: \"en_US\",\n      siteName: \"agent-browser\",\n      title: fullTitle,\n      description: DESCRIPTION,\n      images: [\n        {\n          url: ogImageUrl,\n          width: 1200,\n          height: 630,\n          alt: `${displayTitle} - agent-browser`,\n        },\n      ],\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      title: fullTitle,\n      description: DESCRIPTION,\n      images: [ogImageUrl],\n    },\n  };\n}\n"
  },
  {
    "path": "docs/src/lib/page-titles.ts",
    "content": "export const PAGE_TITLES: Record<string, string> = {\n  \"\": \"Headless Browser\\nAutomation for AI\",\n  installation: \"Installation\",\n  \"quick-start\": \"Quick Start\",\n  skills: \"Skills\",\n  commands: \"Commands\",\n  configuration: \"Configuration\",\n  selectors: \"Selectors\",\n  snapshots: \"Snapshots\",\n  sessions: \"Sessions\",\n  diffing: \"Diffing\",\n  \"cdp-mode\": \"CDP Mode\",\n  streaming: \"Streaming\",\n  profiler: \"Profiler\",\n  ios: \"iOS Simulator\",\n  security: \"Security\",\n  \"engines/chrome\": \"Chrome\",\n  \"engines/lightpanda\": \"Lightpanda\",\n  next: \"Next.js + Vercel\",\n  \"native-mode\": \"Native Mode\",\n  \"providers/browser-use\": \"Browser Use\",\n  \"providers/browserbase\": \"Browserbase\",\n  \"providers/browserless\": \"Browserless\",\n  \"providers/kernel\": \"Kernel\",\n  changelog: \"Changelog\",\n};\n\nexport function getPageTitle(slug: string): string | null {\n  return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;\n}\n"
  },
  {
    "path": "docs/src/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "docs/src/lib/search-index.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { navigation } from \"./docs-navigation\";\nimport { mdxToCleanMarkdown } from \"./mdx-to-markdown\";\n\nexport type IndexEntry = {\n  title: string;\n  href: string;\n  section: string;\n  content: string;\n};\n\nlet cached: IndexEntry[] | null = null;\n\nfunction stripMarkdown(md: string): string {\n  return md\n    .replace(/```[\\s\\S]*?```/g, \"\")\n    .replace(/`[^`]+`/g, \"\")\n    .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n    .replace(/^#{1,6}\\s+/gm, \"\")\n    .replace(/\\*{1,3}([^*]+)\\*{1,3}/g, \"$1\")\n    .replace(/<[^>]+>/g, \"\")\n    .replace(/\\n{3,}/g, \"\\n\\n\")\n    .trim();\n}\n\nfunction mdxFileForSlug(slug: string): string {\n  const docsRoot = join(process.cwd(), \"src\", \"app\");\n  if (slug === \"/\") {\n    return join(docsRoot, \"page.mdx\");\n  }\n  const rest = slug.replace(/^\\//, \"\");\n  return join(docsRoot, ...rest.split(\"/\"), \"page.mdx\");\n}\n\nexport async function getSearchIndex(): Promise<IndexEntry[]> {\n  if (cached) return cached;\n\n  const entries: IndexEntry[] = [];\n\n  for (const section of navigation) {\n    for (const item of section.items) {\n      try {\n        const raw = await readFile(mdxFileForSlug(item.href), \"utf-8\");\n        const md = mdxToCleanMarkdown(raw);\n        const content = stripMarkdown(md);\n        entries.push({\n          title: item.name,\n          href: item.href,\n          section: section.title ?? \"\",\n          content,\n        });\n      } catch {\n        entries.push({\n          title: item.name,\n          href: item.href,\n          section: section.title ?? \"\",\n          content: \"\",\n        });\n      }\n    }\n  }\n\n  cached = entries;\n  return entries;\n}\n"
  },
  {
    "path": "docs/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\",\n    \"**/*.mts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/environments/.gitignore",
    "content": "node_modules/\n.next/\n.env\n.env.local\n.env*.local\nnext-env.d.ts\n"
  },
  {
    "path": "examples/environments/README.md",
    "content": "# agent-browser Environments\n\nA demo of agent-browser running in a Vercel Sandbox. Pick a URL, take a screenshot or accessibility snapshot, and watch each command execute in real time.\n\n## How It Works\n\nThe app runs agent-browser + Chrome inside an ephemeral Vercel Sandbox microVM. A Linux VM spins up on demand, executes agent-browser commands, and shuts down. No binary size limits, no Chromium bundling complexity.\n\nThe UI streams progress via Server-Sent Events so you can see each step as it runs (sandbox creation, browser startup, navigation, screenshot/snapshot, cleanup).\n\n## Getting Started\n\n```bash\ncd examples/environments\npnpm install\npnpm dev\n```\n\nFor local development, set `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID` in `.env.local` so the Sandbox SDK can authenticate.\n\n## Sandbox Snapshots\n\nWithout optimization, each Sandbox run installs system dependencies + agent-browser + Chromium from scratch (~30s). A **sandbox snapshot** is a saved VM image with everything pre-installed -- the sandbox boots from the image instead of installing, bringing startup down to sub-second. (This is unrelated to agent-browser's *accessibility snapshot* feature, which dumps a page's accessibility tree.)\n\nCreate a sandbox snapshot by running the helper script once:\n\n```bash\nnpx tsx scripts/create-snapshot.ts\n# Output: AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx\n```\n\nAdd the ID to your Vercel project environment variables or `.env.local`. Recommended for production.\n\n## Environment Variables\n\n| Variable | Description |\n|---|---|\n| `AGENT_BROWSER_SNAPSHOT_ID` | Sandbox snapshot ID for sub-second startup (see above) |\n| `VERCEL_TOKEN` | Vercel personal access token (for local dev; OIDC is automatic on Vercel) |\n| `VERCEL_TEAM_ID` | Vercel team ID (for local dev) |\n| `VERCEL_PROJECT_ID` | Vercel project ID (for local dev) |\n| `KV_REST_API_URL` | Upstash Redis URL for rate limiting (optional) |\n| `KV_REST_API_TOKEN` | Upstash Redis token for rate limiting (optional) |\n| `RATE_LIMIT_PER_MINUTE` | Max requests per minute per IP (default: 10) |\n| `RATE_LIMIT_PER_DAY` | Max requests per day per IP (default: 100) |\n\n## Project Structure\n\n```\nexamples/environments/\n  app/\n    page.tsx                  # Demo UI with streaming progress\n    actions/browse.ts         # Server action (env status check)\n    api/browse/route.ts       # Streaming SSE endpoint\n  lib/\n    agent-browser-sandbox.ts  # Vercel Sandbox client with progress callbacks\n    constants.ts              # Allowed URLs\n    rate-limit.ts             # Upstash rate limiting\n  scripts/\n    create-snapshot.ts        # Create sandbox snapshot\n```\n"
  },
  {
    "path": "examples/environments/app/actions/browse.ts",
    "content": "\"use server\";\n\nexport type EnvStatus = {\n  sandbox: {\n    hasSnapshot: boolean;\n  };\n};\n\nexport async function getEnvStatus(): Promise<EnvStatus> {\n  return {\n    sandbox: {\n      hasSnapshot: !!process.env.AGENT_BROWSER_SNAPSHOT_ID,\n    },\n  };\n}\n"
  },
  {
    "path": "examples/environments/app/api/browse/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport * as sandbox from \"@/lib/agent-browser-sandbox\";\nimport type { StepEvent } from \"@/lib/agent-browser-sandbox\";\nimport { ALLOWED_URLS } from \"@/lib/constants\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\n\nexport async function POST(req: NextRequest) {\n  const ip =\n    req.headers.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const minute = await minuteRateLimit.limit(ip);\n  if (!minute.success) {\n    return NextResponse.json(\n      { error: \"Too many requests. Please wait a moment before trying again.\" },\n      { status: 429 },\n    );\n  }\n\n  const daily = await dailyRateLimit.limit(ip);\n  if (!daily.success) {\n    return NextResponse.json(\n      { error: \"Daily limit reached. Please try again tomorrow.\" },\n      { status: 429 },\n    );\n  }\n\n  const body = await req.json();\n  const { url, action } = body;\n\n  if (!url) {\n    return NextResponse.json({ error: \"Provide a 'url'\" }, { status: 400 });\n  }\n\n  if (!(ALLOWED_URLS as readonly string[]).includes(url)) {\n    return NextResponse.json({ error: \"URL not allowed\" }, { status: 400 });\n  }\n\n  if (action !== \"screenshot\" && action !== \"snapshot\") {\n    return NextResponse.json(\n      { error: \"Provide 'action' as 'screenshot' or 'snapshot'\" },\n      { status: 400 },\n    );\n  }\n\n  const encoder = new TextEncoder();\n\n  const stream = new ReadableStream({\n    async start(controller) {\n      const send = (event: string, data: unknown) => {\n        controller.enqueue(\n          encoder.encode(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`),\n        );\n      };\n\n      const onStep = (step: StepEvent) => {\n        send(\"step\", step);\n      };\n\n      try {\n        if (action === \"screenshot\") {\n          const result = await sandbox.screenshotUrl(url, {\n            fullPage: body.fullPage,\n            onStep,\n          });\n          send(\"result\", { ok: true, ...result });\n        } else {\n          const result = await sandbox.snapshotUrl(url, {\n            interactive: true,\n            compact: true,\n            onStep,\n          });\n          send(\"result\", { ok: true, ...result });\n        }\n      } catch (err) {\n        const message = err instanceof Error ? err.message : String(err);\n        send(\"result\", { ok: false, error: message });\n      }\n\n      controller.close();\n    },\n  });\n\n  return new Response(stream, {\n    headers: {\n      \"Content-Type\": \"text/event-stream\",\n      \"Cache-Control\": \"no-cache\",\n      Connection: \"keep-alive\",\n    },\n  });\n}\n"
  },
  {
    "path": "examples/environments/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"shadcn/tailwind.css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n  --color-surface: #fafafa;\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n\n  body {\n    @apply bg-background text-foreground antialiased;\n  }\n\n  html {\n    @apply font-sans;\n  }\n}\n\n:root {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.58 0.22 27);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.809 0.105 251.813);\n  --chart-2: oklch(0.623 0.214 259.815);\n  --chart-3: oklch(0.546 0.245 262.881);\n  --chart-4: oklch(0.488 0.243 264.376);\n  --chart-5: oklch(0.424 0.199 265.638);\n  --radius: 0.625rem;\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n  --surface: oklch(0.985 0 0);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.87 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.371 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.809 0.105 251.813);\n  --chart-2: oklch(0.623 0.214 259.815);\n  --chart-3: oklch(0.546 0.245 262.881);\n  --chart-4: oklch(0.488 0.243 264.376);\n  --chart-5: oklch(0.424 0.199 265.638);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n  --surface: oklch(0.205 0 0);\n}\n\n@theme inline {\n  --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;\n  --font-mono: var(--font-geist-mono), ui-monospace, \"SFMono-Regular\",\n    \"Roboto Mono\", monospace;\n  --color-sidebar-ring: var(--sidebar-ring);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar: var(--sidebar);\n  --color-chart-5: var(--chart-5);\n  --color-chart-4: var(--chart-4);\n  --color-chart-3: var(--chart-3);\n  --color-chart-2: var(--chart-2);\n  --color-chart-1: var(--chart-1);\n  --color-ring: var(--ring);\n  --color-input: var(--input);\n  --color-border: var(--border);\n  --color-destructive: var(--destructive);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-accent: var(--accent);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-muted: var(--muted);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-secondary: var(--secondary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-primary: var(--primary);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-popover: var(--popover);\n  --color-card-foreground: var(--card-foreground);\n  --color-card: var(--card);\n  --color-foreground: var(--foreground);\n  --color-background: var(--background);\n  --color-surface: var(--surface);\n  --radius-sm: calc(var(--radius) * 0.6);\n  --radius-md: calc(var(--radius) * 0.8);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) * 1.4);\n  --radius-2xl: calc(var(--radius) * 1.8);\n  --radius-3xl: calc(var(--radius) * 2.2);\n  --radius-4xl: calc(var(--radius) * 2.6);\n}\n"
  },
  {
    "path": "examples/environments/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { GeistSans } from \"geist/font/sans\";\nimport { GeistMono } from \"geist/font/mono\";\nimport \"./globals.css\";\n\nexport const metadata: Metadata = {\n  title: \"agent-browser Environments\",\n  description: \"Run agent-browser in different compute environments\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\" className={`${GeistSans.variable} ${GeistMono.variable}`}>\n      <body className=\"min-h-screen font-sans antialiased\">{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/environments/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport { getEnvStatus } from \"./actions/browse\";\nimport type { EnvStatus } from \"./actions/browse\";\nimport {\n  ResizablePanelGroup,\n  ResizablePanel,\n  ResizableHandle,\n} from \"@/components/ui/resizable\";\nimport { ALLOWED_URLS } from \"@/lib/constants\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Alert, AlertTitle, AlertDescription } from \"@/components/ui/alert\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Loader2, Monitor, CircleX, Sun, Moon, Check } from \"lucide-react\";\n\nconst MOBILE_QUERY = \"(max-width: 767px)\";\nconst subscribe = (cb: () => void) => {\n  const mql = window.matchMedia(MOBILE_QUERY);\n  mql.addEventListener(\"change\", cb);\n  return () => mql.removeEventListener(\"change\", cb);\n};\nconst getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches;\nconst getServerSnapshot = () => false;\n\nfunction useIsMobile() {\n  return useSyncExternalStore(subscribe, getMobileSnapshot, getServerSnapshot);\n}\n\nfunction useTheme() {\n  const [theme, setThemeState] = useState<\"light\" | \"dark\">(\"light\");\n\n  useEffect(() => {\n    const stored = localStorage.getItem(\"theme\");\n    const initial =\n      stored === \"dark\" ||\n      (!stored && window.matchMedia(\"(prefers-color-scheme: dark)\").matches)\n        ? \"dark\"\n        : \"light\";\n    setThemeState(initial);\n    document.documentElement.classList.toggle(\"dark\", initial === \"dark\");\n  }, []);\n\n  const toggle = () => {\n    const next = theme === \"dark\" ? \"light\" : \"dark\";\n    setThemeState(next);\n    document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n    localStorage.setItem(\"theme\", next);\n  };\n\n  return { theme, toggle };\n}\n\ntype Action = \"screenshot\" | \"snapshot\";\n\ntype StepInfo = {\n  step: string;\n  status: \"running\" | \"done\" | \"error\";\n  elapsed?: number;\n};\n\ntype BrowseResult = {\n  ok: boolean;\n  screenshot?: string;\n  snapshot?: string;\n  title?: string;\n  error?: string;\n};\n\nfunction formatError(raw: string): string {\n  let cleaned = raw.replace(/<[^>]*>/g, \" \").replace(/\\s+/g, \" \").trim();\n  const match = cleaned.match(/(?:error|Error)[:\\s]*(.{1,200})/);\n  if (match) cleaned = match[1].trim();\n  if (cleaned.length > 300) cleaned = cleaned.slice(0, 300) + \"...\";\n  return cleaned || raw.slice(0, 300);\n}\n\nfunction SegmentedControl<T extends string>({\n  value,\n  onChange,\n  options,\n}: {\n  value: T;\n  onChange: (v: T) => void;\n  options: { value: T; label: string }[];\n}) {\n  return (\n    <div className=\"inline-flex rounded-lg border border-input bg-muted p-0.5 w-full\">\n      {options.map((opt) => (\n        <button\n          key={opt.value}\n          type=\"button\"\n          onClick={() => onChange(opt.value)}\n          className={`\n            flex-1 px-3 py-1.5 text-[13px] font-medium rounded-md transition-all cursor-pointer\n            ${\n              value === opt.value\n                ? \"bg-background text-foreground shadow-sm\"\n                : \"text-muted-foreground hover:text-foreground\"\n            }\n          `}\n        >\n          {opt.label}\n        </button>\n      ))}\n    </div>\n  );\n}\n\nfunction StepIndicator({ step }: { step: StepInfo }) {\n  return (\n    <div className=\"flex items-center gap-2.5 py-1\">\n      <div className=\"size-4 flex items-center justify-center shrink-0\">\n        {step.status === \"running\" ? (\n          <Loader2 className=\"size-3.5 animate-spin text-muted-foreground\" />\n        ) : step.status === \"done\" ? (\n          <Check className=\"size-3.5 text-emerald-500\" />\n        ) : (\n          <CircleX className=\"size-3.5 text-destructive\" />\n        )}\n      </div>\n      <span\n        className={`text-[13px] ${\n          step.status === \"running\"\n            ? \"text-foreground\"\n            : step.status === \"done\"\n              ? \"text-muted-foreground\"\n              : \"text-destructive\"\n        }`}\n      >\n        {step.step}\n      </span>\n      {step.elapsed != null && step.status !== \"running\" && (\n        <span className=\"text-[11px] text-muted-foreground/60 tabular-nums ml-auto\">\n          {(step.elapsed / 1000).toFixed(1)}s\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction ErrorDisplay({ error }: { error: string }) {\n  const isHtml = /<[a-z][\\s\\S]*>/i.test(error);\n  const message = isHtml ? formatError(error) : error;\n  const showRaw = isHtml && error.length > 100;\n\n  return (\n    <div className=\"w-full max-w-2xl space-y-0\">\n      <Alert variant=\"destructive\">\n        <CircleX className=\"size-4\" />\n        <AlertTitle>Request failed</AlertTitle>\n        <AlertDescription>{message}</AlertDescription>\n      </Alert>\n      {showRaw && (\n        <details className=\"border border-t-0 border-border rounded-b-lg overflow-hidden\">\n          <summary className=\"px-4 py-2 text-[11px] font-medium text-muted-foreground cursor-pointer hover:bg-muted transition-colors\">\n            Show raw response\n          </summary>\n          <pre className=\"px-4 py-3 text-[11px] leading-relaxed text-muted-foreground font-mono overflow-auto max-h-[200px] bg-muted/50\">\n            {error}\n          </pre>\n        </details>\n      )}\n    </div>\n  );\n}\n\nasync function streamBrowse(\n  url: string,\n  action: Action,\n  onStep: (step: StepInfo) => void,\n): Promise<BrowseResult> {\n  const res = await fetch(\"/api/browse\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ url, action }),\n  });\n\n  if (!res.ok) {\n    const body = await res.json().catch(() => null);\n    return { ok: false, error: body?.error || `HTTP ${res.status}` };\n  }\n\n  const reader = res.body?.getReader();\n  if (!reader) {\n    return { ok: false, error: \"No response stream\" };\n  }\n\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n  let result: BrowseResult = { ok: false, error: \"No result received\" };\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    buffer += decoder.decode(value, { stream: true });\n\n    const parts = buffer.split(\"\\n\\n\");\n    buffer = parts.pop() || \"\";\n\n    for (const part of parts) {\n      const eventMatch = part.match(/^event: (\\w+)\\ndata: ([\\s\\S]+)$/);\n      if (!eventMatch) continue;\n\n      const [, event, data] = eventMatch;\n      try {\n        const parsed = JSON.parse(data);\n        if (event === \"step\") {\n          onStep(parsed as StepInfo);\n        } else if (event === \"result\") {\n          result = parsed as BrowseResult;\n        }\n      } catch {\n        // skip malformed events\n      }\n    }\n  }\n\n  return result;\n}\n\nexport default function Home() {\n  const isMobile = useIsMobile();\n  const { theme, toggle: toggleTheme } = useTheme();\n  const [url, setUrl] = useState<string>(ALLOWED_URLS[0]);\n  const [loading, setLoading] = useState(false);\n  const [action, setAction] = useState<Action>(\"screenshot\");\n  const [result, setResult] = useState<BrowseResult | null>(null);\n  const [steps, setSteps] = useState<StepInfo[]>([]);\n  const [envStatus, setEnvStatus] = useState<EnvStatus | null>(null);\n  const stepsEndRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    getEnvStatus().then(setEnvStatus);\n  }, []);\n\n  function clearResults() {\n    setResult(null);\n    setSteps([]);\n  }\n\n  async function handleSubmit(e: React.FormEvent) {\n    e.preventDefault();\n    setLoading(true);\n    setResult(null);\n    setSteps([]);\n\n    try {\n      const browseResult = await streamBrowse(url, action, (step) => {\n        setSteps((prev) => {\n          const existing = prev.findIndex((s) => s.step === step.step);\n          if (existing >= 0) {\n            const updated = [...prev];\n            updated[existing] = step;\n            return updated;\n          }\n          return [...prev, step];\n        });\n      });\n      setResult(browseResult);\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      setResult({ ok: false, error: message });\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  useEffect(() => {\n    stepsEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  }, [steps]);\n\n  const controlsForm = (\n    <form onSubmit={handleSubmit} className=\"p-5 space-y-5\">\n      <div className=\"space-y-1.5\">\n        <Label\n          htmlFor=\"url-select\"\n          className=\"text-[11px] text-muted-foreground uppercase tracking-wider\"\n        >\n          URL\n        </Label>\n        <Select\n          value={url}\n          onValueChange={(v) => {\n            if (v) setUrl(v);\n            clearResults();\n          }}\n        >\n          <SelectTrigger id=\"url-select\">\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            {ALLOWED_URLS.map((u) => (\n              <SelectItem key={u} value={u}>\n                {u.replace(\"https://\", \"\")}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      </div>\n\n      <div className=\"space-y-1.5\">\n        <Label className=\"text-[11px] text-muted-foreground uppercase tracking-wider\">\n          Action\n        </Label>\n        <SegmentedControl<Action>\n          value={action}\n          onChange={(v) => {\n            setAction(v);\n            clearResults();\n          }}\n          options={[\n            { value: \"screenshot\", label: \"Screenshot\" },\n            { value: \"snapshot\", label: \"Snapshot\" },\n          ]}\n        />\n        <p className=\"text-[11px] text-muted-foreground\">\n          {action === \"screenshot\"\n            ? \"Captures a full-page PNG image\"\n            : \"Returns the accessibility tree\"}\n        </p>\n      </div>\n\n      {envStatus && !envStatus.sandbox.hasSnapshot && (\n        <Alert>\n          <AlertTitle className=\"text-[12px]\">\n            Sandbox snapshot not configured\n          </AlertTitle>\n          <AlertDescription className=\"text-[11px]\">\n            Without a sandbox snapshot, the VM installs agent-browser +\n            Chromium on every request (~30s). Create one with{\" \"}\n            <code className=\"text-[10px] bg-muted px-1 py-0.5 rounded\">\n              npx tsx scripts/create-snapshot.ts\n            </code>{\" \"}\n            and set{\" \"}\n            <code className=\"text-[10px] bg-muted px-1 py-0.5 rounded\">\n              AGENT_BROWSER_SNAPSHOT_ID\n            </code>{\" \"}\n            for sub-second startup.\n          </AlertDescription>\n        </Alert>\n      )}\n\n      <Button type=\"submit\" disabled={loading} className=\"w-full\" size=\"lg\">\n        {loading && <Loader2 className=\"size-4 animate-spin\" />}\n        {loading ? \"Running...\" : \"Run\"}\n      </Button>\n    </form>\n  );\n\n  const showSteps = loading || (steps.length > 0 && !result);\n  const hasResult = result && !loading;\n\n  const resultContent = showSteps ? (\n    <div className=\"p-6 lg:p-10\">\n      <div className=\"max-w-xl mx-auto\">\n        <div className=\"space-y-0.5\">\n          {steps.map((s, i) => (\n            <StepIndicator key={`${s.step}-${i}`} step={s} />\n          ))}\n          <div ref={stepsEndRef} />\n        </div>\n      </div>\n    </div>\n  ) : hasResult ? (\n    <div className=\"flex flex-col items-center p-6 lg:p-10\">\n      {result.ok && result.screenshot && (\n        <div className=\"w-full max-w-3xl\">\n          <div className=\"flex items-center justify-between mb-4\">\n            <h2 className=\"text-sm font-semibold truncate mr-3\">\n              {result.title}\n            </h2>\n            <Badge\n              variant=\"outline\"\n              className=\"font-mono text-[11px] shrink-0\"\n            >\n              screenshot\n            </Badge>\n          </div>\n          <div className=\"rounded-xl border border-border overflow-hidden shadow-sm\">\n            <img\n              src={`data:image/png;base64,${result.screenshot}`}\n              alt={result.title}\n              className=\"w-full block\"\n            />\n          </div>\n          <details className=\"mt-4\">\n            <summary className=\"text-[11px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors\">\n              Show steps ({steps.length})\n            </summary>\n            <div className=\"mt-2 space-y-0.5\">\n              {steps.map((s, i) => (\n                <StepIndicator key={`${s.step}-${i}`} step={s} />\n              ))}\n            </div>\n          </details>\n        </div>\n      )}\n\n      {result.ok && result.snapshot && (\n        <div className=\"w-full max-w-3xl\">\n          <div className=\"flex items-center justify-between mb-4\">\n            <h2 className=\"text-sm font-semibold truncate mr-3\">\n              {result.title}\n            </h2>\n            <Badge\n              variant=\"outline\"\n              className=\"font-mono text-[11px] shrink-0\"\n            >\n              snapshot\n            </Badge>\n          </div>\n          <pre className=\"bg-card rounded-xl border border-border p-5 overflow-auto text-[13px] leading-relaxed font-mono max-h-[calc(100vh-12rem)]\">\n            {result.snapshot}\n          </pre>\n          <details className=\"mt-4\">\n            <summary className=\"text-[11px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors\">\n              Show steps ({steps.length})\n            </summary>\n            <div className=\"mt-2 space-y-0.5\">\n              {steps.map((s, i) => (\n                <StepIndicator key={`${s.step}-${i}`} step={s} />\n              ))}\n            </div>\n          </details>\n        </div>\n      )}\n\n      {!result.ok && (\n        <div className=\"w-full max-w-2xl space-y-4\">\n          <ErrorDisplay error={result.error ?? \"Unknown error\"} />\n          {steps.length > 0 && (\n            <div className=\"space-y-0.5\">\n              {steps.map((s, i) => (\n                <StepIndicator key={`${s.step}-${i}`} step={s} />\n              ))}\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  ) : (\n    <div className=\"min-h-[300px] md:h-full flex flex-col items-center justify-center text-muted-foreground\">\n      <Monitor className=\"size-12 mb-4 opacity-30\" strokeWidth={1} />\n      <p className=\"text-sm font-medium mb-1\">No result yet</p>\n      <p className=\"text-[13px]\">Pick a URL and click Run</p>\n    </div>\n  );\n\n  return (\n    <div className=\"h-screen flex flex-col\">\n      <header className=\"border-b border-border shrink-0\">\n        <div className=\"px-4 md:px-6 h-12 flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            <span className=\"text-sm font-semibold tracking-tight\">\n              agent-browser\n            </span>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <button\n              type=\"button\"\n              onClick={toggleTheme}\n              className=\"size-8 inline-flex items-center justify-center rounded-md border border-input bg-background text-muted-foreground hover:text-foreground transition-colors cursor-pointer\"\n              aria-label=\"Toggle theme\"\n            >\n              {theme === \"dark\" ? (\n                <Sun className=\"size-4\" />\n              ) : (\n                <Moon className=\"size-4\" />\n              )}\n            </button>\n            <a\n              href=\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fagent-browser%2Ftree%2Fmain%2Fexamples%2Fenvironments&project-name=agent-browser-environments&repository-name=agent-browser-environments\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <img\n                src=\"https://vercel.com/button\"\n                alt=\"Deploy with Vercel\"\n                className=\"h-8\"\n              />\n            </a>\n          </div>\n        </div>\n      </header>\n\n      {isMobile ? (\n        <div className=\"flex-1 overflow-auto\">\n          <div className=\"border-b border-border\">{controlsForm}</div>\n          <div className=\"bg-surface\">{resultContent}</div>\n        </div>\n      ) : (\n        <ResizablePanelGroup orientation=\"horizontal\" className=\"flex-1\">\n          <ResizablePanel defaultSize=\"30%\" minSize=\"20%\" maxSize=\"50%\">\n            <aside className=\"h-full overflow-y-auto\">{controlsForm}</aside>\n          </ResizablePanel>\n\n          <ResizableHandle withHandle />\n\n          <ResizablePanel defaultSize=\"70%\">\n            <main className=\"h-full overflow-auto bg-surface\">\n              {resultContent}\n            </main>\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/environments/components/ui/alert.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst alertVariants = cva(\n  \"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  )\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AlertAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-action\"\n      className={cn(\"absolute top-2 right-2\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Alert, AlertTitle, AlertDescription, AlertAction }\n"
  },
  {
    "path": "examples/environments/components/ui/badge.tsx",
    "content": "import { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground [a]:hover:bg-primary/80\",\n        secondary:\n          \"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80\",\n        destructive:\n          \"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20\",\n        outline:\n          \"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground\",\n        ghost:\n          \"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nfunction Badge({\n  className,\n  variant = \"default\",\n  render,\n  ...props\n}: useRender.ComponentProps<\"span\"> & VariantProps<typeof badgeVariants>) {\n  return useRender({\n    defaultTagName: \"span\",\n    props: mergeProps<\"span\">(\n      {\n        className: cn(badgeVariants({ variant }), className),\n      },\n      props\n    ),\n    render,\n    state: {\n      slot: \"badge\",\n      variant,\n    },\n  })\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "examples/environments/components/ui/button.tsx",
    "content": "\"use client\"\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground [a]:hover:bg-primary/80\",\n        outline:\n          \"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground\",\n        ghost:\n          \"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50\",\n        destructive:\n          \"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default:\n          \"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        xs: \"h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n        lg: \"h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3\",\n        icon: \"size-8\",\n        \"icon-xs\":\n          \"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\":\n          \"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg\",\n        \"icon-lg\": \"size-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {\n  return (\n    <ButtonPrimitive\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "examples/environments/components/ui/input.tsx",
    "content": "import * as React from \"react\"\nimport { Input as InputPrimitive } from \"@base-ui/react/input\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <InputPrimitive\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Input }\n"
  },
  {
    "path": "examples/environments/components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Label({ className, ...props }: React.ComponentProps<\"label\">) {\n  return (\n    <label\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Label }\n"
  },
  {
    "path": "examples/environments/components/ui/resizable.tsx",
    "content": "\"use client\"\n\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ResizablePanelGroup({\n  className,\n  ...props\n}: ResizablePrimitive.GroupProps) {\n  return (\n    <ResizablePrimitive.Group\n      data-slot=\"resizable-panel-group\"\n      className={cn(\n        \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {\n  return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />\n}\n\nfunction ResizableHandle({\n  withHandle,\n  className,\n  ...props\n}: ResizablePrimitive.SeparatorProps & {\n  withHandle?: boolean\n}) {\n  return (\n    <ResizablePrimitive.Separator\n      data-slot=\"resizable-handle\"\n      className={cn(\n        \"relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n        className\n      )}\n      {...props}\n    >\n      {withHandle && (\n        <div className=\"z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border\" />\n      )}\n    </ResizablePrimitive.Separator>\n  )\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup }\n"
  },
  {
    "path": "examples/environments/components/ui/select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Select as SelectPrimitive } from \"@base-ui/react/select\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ChevronDownIcon, CheckIcon, ChevronUpIcon } from \"lucide-react\"\n\nconst Select = SelectPrimitive.Root\n\nfunction SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {\n  return (\n    <SelectPrimitive.Group\n      data-slot=\"select-group\"\n      className={cn(\"scroll-my-1 p-1\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {\n  return (\n    <SelectPrimitive.Value\n      data-slot=\"select-value\"\n      className={cn(\"flex flex-1 text-left\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: SelectPrimitive.Trigger.Props & {\n  size?: \"sm\" | \"default\"\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon\n        render={\n          <ChevronDownIcon className=\"pointer-events-none size-4 text-muted-foreground\" />\n        }\n      />\n    </SelectPrimitive.Trigger>\n  )\n}\n\nfunction SelectContent({\n  className,\n  children,\n  side = \"bottom\",\n  sideOffset = 4,\n  align = \"center\",\n  alignOffset = 0,\n  alignItemWithTrigger = true,\n  ...props\n}: SelectPrimitive.Popup.Props &\n  Pick<\n    SelectPrimitive.Positioner.Props,\n    \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\" | \"alignItemWithTrigger\"\n  >) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Positioner\n        side={side}\n        sideOffset={sideOffset}\n        align={align}\n        alignOffset={alignOffset}\n        alignItemWithTrigger={alignItemWithTrigger}\n        className=\"isolate z-50\"\n      >\n        <SelectPrimitive.Popup\n          data-slot=\"select-content\"\n          data-align-trigger={alignItemWithTrigger}\n          className={cn(\"relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\", className )}\n          {...props}\n        >\n          <SelectScrollUpButton />\n          <SelectPrimitive.List>{children}</SelectPrimitive.List>\n          <SelectScrollDownButton />\n        </SelectPrimitive.Popup>\n      </SelectPrimitive.Positioner>\n    </SelectPrimitive.Portal>\n  )\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: SelectPrimitive.GroupLabel.Props) {\n  return (\n    <SelectPrimitive.GroupLabel\n      data-slot=\"select-label\"\n      className={cn(\"px-1.5 py-1 text-xs text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: SelectPrimitive.Item.Props) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className\n      )}\n      {...props}\n    >\n      <SelectPrimitive.ItemText className=\"flex flex-1 shrink-0 gap-2 whitespace-nowrap\">\n        {children}\n      </SelectPrimitive.ItemText>\n      <SelectPrimitive.ItemIndicator\n        render={\n          <span className=\"pointer-events-none absolute right-2 flex size-4 items-center justify-center\" />\n        }\n      >\n        <CheckIcon className=\"pointer-events-none\" />\n      </SelectPrimitive.ItemIndicator>\n    </SelectPrimitive.Item>\n  )\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: SelectPrimitive.Separator.Props) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"pointer-events-none -mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {\n  return (\n    <SelectPrimitive.ScrollUpArrow\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      <ChevronUpIcon\n      />\n    </SelectPrimitive.ScrollUpArrow>\n  )\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {\n  return (\n    <SelectPrimitive.ScrollDownArrow\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      <ChevronDownIcon\n      />\n    </SelectPrimitive.ScrollDownArrow>\n  )\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n}\n"
  },
  {
    "path": "examples/environments/components/ui/separator.tsx",
    "content": "\"use client\"\n\nimport { Separator as SeparatorPrimitive } from \"@base-ui/react/separator\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: SeparatorPrimitive.Props) {\n  return (\n    <SeparatorPrimitive\n      data-slot=\"separator\"\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Separator }\n"
  },
  {
    "path": "examples/environments/components/ui/toggle-group.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { toggleVariants } from \"@/components/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number\n    orientation?: \"horizontal\" | \"vertical\"\n  }\n>({\n  size: \"default\",\n  variant: \"default\",\n  spacing: 0,\n  orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n  className,\n  variant,\n  size,\n  spacing = 0,\n  orientation = \"horizontal\",\n  children,\n  ...props\n}: ToggleGroupPrimitive.Props &\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number\n    orientation?: \"horizontal\" | \"vertical\"\n  }) {\n  return (\n    <ToggleGroupPrimitive\n      data-slot=\"toggle-group\"\n      data-variant={variant}\n      data-size={size}\n      data-spacing={spacing}\n      data-orientation={orientation}\n      style={{ \"--gap\": spacing } as React.CSSProperties}\n      className={cn(\n        \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch\",\n        className\n      )}\n      {...props}\n    >\n      <ToggleGroupContext.Provider\n        value={{ variant, size, spacing, orientation }}\n      >\n        {children}\n      </ToggleGroupContext.Provider>\n    </ToggleGroupPrimitive>\n  )\n}\n\nfunction ToggleGroupItem({\n  className,\n  children,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n  const context = React.useContext(ToggleGroupContext)\n\n  return (\n    <TogglePrimitive\n      data-slot=\"toggle-group-item\"\n      data-variant={context.variant || variant}\n      data-size={context.size || size}\n      data-spacing={context.spacing}\n      className={cn(\n        \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </TogglePrimitive>\n  )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n"
  },
  {
    "path": "examples/environments/components/ui/toggle.tsx",
    "content": "\"use client\"\n\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst toggleVariants = cva(\n  \"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline: \"border border-input bg-transparent hover:bg-muted\",\n      },\n      size: {\n        default: \"h-8 min-w-8 px-2\",\n        sm: \"h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]\",\n        lg: \"h-9 min-w-9 px-2.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Toggle({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Toggle, toggleVariants }\n"
  },
  {
    "path": "examples/environments/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"base-nova\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"rtl\": false,\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"menuColor\": \"default\",\n  \"menuAccent\": \"subtle\",\n  \"registries\": {}\n}\n"
  },
  {
    "path": "examples/environments/lib/agent-browser-sandbox.ts",
    "content": "/**\n * Run agent-browser inside a Vercel Sandbox.\n *\n * No external server needed -- a Linux microVM spins up on demand,\n * runs agent-browser + headless Chrome, and shuts down when done.\n *\n * For production, create a snapshot with agent-browser and Chromium\n * pre-installed so startup is sub-second instead of ~30s.\n */\n\nimport { Sandbox } from \"@vercel/sandbox\";\n\nexport type SandboxResult = {\n  exitCode: number;\n  stdout: string;\n  stderr: string;\n};\n\nexport type StepEvent = {\n  step: string;\n  status: \"running\" | \"done\" | \"error\";\n  elapsed?: number;\n};\n\nexport type OnStep = (event: StepEvent) => void;\n\nconst SNAPSHOT_ID = process.env.AGENT_BROWSER_SNAPSHOT_ID;\n\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\",\n  \"nspr\",\n  \"libxkbcommon\",\n  \"atk\",\n  \"at-spi2-atk\",\n  \"at-spi2-core\",\n  \"libXcomposite\",\n  \"libXdamage\",\n  \"libXrandr\",\n  \"libXfixes\",\n  \"libXcursor\",\n  \"libXi\",\n  \"libXtst\",\n  \"libXScrnSaver\",\n  \"libXext\",\n  \"mesa-libgbm\",\n  \"libdrm\",\n  \"mesa-libGL\",\n  \"mesa-libEGL\",\n  \"cups-libs\",\n  \"alsa-lib\",\n  \"pango\",\n  \"cairo\",\n  \"gtk3\",\n  \"dbus-libs\",\n];\n\n/**\n * Returns credentials to spread into Sandbox.create() calls.\n * When explicit env vars are set they take precedence; otherwise returns\n * an empty object so the SDK falls back to VERCEL_OIDC_TOKEN automatically.\n */\nexport function getSandboxCredentials():\n  | { token: string; teamId: string; projectId: string }\n  | Record<string, never> {\n  if (\n    process.env.VERCEL_TOKEN &&\n    process.env.VERCEL_TEAM_ID &&\n    process.env.VERCEL_PROJECT_ID\n  ) {\n    return {\n      token: process.env.VERCEL_TOKEN,\n      teamId: process.env.VERCEL_TEAM_ID,\n      projectId: process.env.VERCEL_PROJECT_ID,\n    };\n  }\n  return {};\n}\n\nasync function runStep<T>(\n  step: string,\n  fn: () => Promise<T>,\n  onStep?: OnStep,\n): Promise<T> {\n  const start = Date.now();\n  onStep?.({ step, status: \"running\" });\n  try {\n    const result = await fn();\n    onStep?.({ step, status: \"done\", elapsed: Date.now() - start });\n    return result;\n  } catch (err) {\n    onStep?.({ step, status: \"error\", elapsed: Date.now() - start });\n    throw err;\n  }\n}\n\n/**\n * Install system dependencies + agent-browser + Chromium into a fresh sandbox.\n * The sandbox base image is Amazon Linux (dnf).\n */\nasync function bootstrapSandbox(\n  sandbox: InstanceType<typeof Sandbox>,\n  onStep?: OnStep,\n): Promise<void> {\n  await runStep(\"Installing system dependencies\", async () => {\n    await sandbox.runCommand(\"sh\", [\n      \"-c\",\n      `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n    ]);\n  }, onStep);\n\n  await runStep(\"Installing agent-browser\", async () => {\n    await sandbox.runCommand(\"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n    await sandbox.runCommand(\"npx\", [\"agent-browser\", \"install\"]);\n  }, onStep);\n}\n\nasync function createSandbox(\n  onStep?: OnStep,\n): Promise<InstanceType<typeof Sandbox>> {\n  const credentials = getSandboxCredentials();\n\n  return runStep(\n    SNAPSHOT_ID ? \"Booting sandbox from snapshot\" : \"Creating sandbox\",\n    async () => {\n      if (SNAPSHOT_ID) {\n        return Sandbox.create({\n          ...credentials,\n          source: { type: \"snapshot\", snapshotId: SNAPSHOT_ID },\n          timeout: 120_000,\n        });\n      }\n\n      const sb = await Sandbox.create({\n        ...credentials,\n        runtime: \"node24\",\n        timeout: 120_000,\n      });\n      await bootstrapSandbox(sb, onStep);\n      return sb;\n    },\n    onStep,\n  );\n}\n\nasync function exec(\n  sandbox: InstanceType<typeof Sandbox>,\n  cmd: string,\n  args: string[],\n  onStep?: OnStep,\n  stepLabel?: string,\n): Promise<SandboxResult> {\n  const label = stepLabel || `${cmd} ${args.join(\" \")}`;\n\n  return runStep(label, async () => {\n    const result = await sandbox.runCommand(cmd, args);\n    const stdout = await result.stdout();\n    const stderr = await result.stderr();\n\n    if (result.exitCode !== 0) {\n      throw new Error(\n        `Command \"${cmd} ${args.join(\" \")}\" failed (exit ${result.exitCode}): ${stderr || stdout}`,\n      );\n    }\n\n    return { exitCode: result.exitCode, stdout, stderr };\n  }, onStep);\n}\n\n/**\n * Screenshot a URL using agent-browser inside a Vercel Sandbox.\n * Returns base64-encoded PNG.\n */\nexport async function screenshotUrl(\n  url: string,\n  opts: { fullPage?: boolean; onStep?: OnStep } = {},\n): Promise<{ screenshot: string; title: string }> {\n  const { onStep } = opts;\n  const sandbox = await createSandbox(onStep);\n\n  try {\n    await exec(sandbox, \"agent-browser\", [\"open\", \"about:blank\"], onStep, \"Starting browser\");\n    await exec(sandbox, \"agent-browser\", [\"open\", url], onStep, `Navigating to ${url}`);\n\n    const titleResult = await exec(\n      sandbox,\n      \"agent-browser\",\n      [\"get\", \"title\", \"--json\"],\n      onStep,\n      \"Getting page title\",\n    );\n    const title = tryParseJson(titleResult.stdout)?.data?.title || url;\n\n    const screenshotArgs = [\"screenshot\", \"--json\"];\n    if (opts.fullPage) screenshotArgs.push(\"--full\");\n    const ssResult = await exec(\n      sandbox,\n      \"agent-browser\",\n      screenshotArgs,\n      onStep,\n      \"Taking screenshot\",\n    );\n    const ssData = tryParseJson(ssResult.stdout)?.data;\n    const screenshotPath = ssData?.path;\n\n    if (!screenshotPath) {\n      throw new Error(\n        `Screenshot returned no file path. Raw output: ${ssResult.stdout.slice(0, 500)}`,\n      );\n    }\n\n    const b64Result = await exec(\n      sandbox,\n      \"base64\",\n      [\"-w\", \"0\", screenshotPath],\n      onStep,\n      \"Encoding screenshot\",\n    );\n    const screenshot = b64Result.stdout.trim();\n\n    if (!screenshot) {\n      throw new Error(\"Failed to read screenshot file from sandbox\");\n    }\n\n    await exec(sandbox, \"agent-browser\", [\"close\"], onStep, \"Closing browser\");\n\n    return { screenshot, title };\n  } finally {\n    await runStep(\"Stopping sandbox\", () => sandbox.stop(), onStep);\n  }\n}\n\n/**\n * Snapshot a URL (accessibility tree) using agent-browser inside a Vercel Sandbox.\n */\nexport async function snapshotUrl(\n  url: string,\n  opts: { interactive?: boolean; compact?: boolean; onStep?: OnStep } = {},\n): Promise<{ snapshot: string; title: string }> {\n  const { onStep } = opts;\n  const sandbox = await createSandbox(onStep);\n\n  try {\n    await exec(sandbox, \"agent-browser\", [\"open\", \"about:blank\"], onStep, \"Starting browser\");\n    await exec(sandbox, \"agent-browser\", [\"open\", url], onStep, `Navigating to ${url}`);\n\n    const titleResult = await exec(\n      sandbox,\n      \"agent-browser\",\n      [\"get\", \"title\", \"--json\"],\n      onStep,\n      \"Getting page title\",\n    );\n    const title = tryParseJson(titleResult.stdout)?.data?.title || url;\n\n    const snapshotArgs = [\"snapshot\"];\n    if (opts.interactive) snapshotArgs.push(\"-i\");\n    if (opts.compact) snapshotArgs.push(\"-c\");\n    const snapResult = await exec(\n      sandbox,\n      \"agent-browser\",\n      snapshotArgs,\n      onStep,\n      \"Taking accessibility snapshot\",\n    );\n\n    if (!snapResult.stdout.trim()) {\n      throw new Error(\"Snapshot returned empty data\");\n    }\n\n    await exec(sandbox, \"agent-browser\", [\"close\"], onStep, \"Closing browser\");\n\n    return { snapshot: snapResult.stdout, title };\n  } finally {\n    await runStep(\"Stopping sandbox\", () => sandbox.stop(), onStep);\n  }\n}\n\n/**\n * Run arbitrary agent-browser commands inside a Vercel Sandbox.\n * Each command is a string array like [\"open\", \"https://example.com\"].\n */\nexport async function runCommands(\n  commands: string[][],\n): Promise<SandboxResult[]> {\n  const sandbox = await createSandbox();\n\n  try {\n    const results: SandboxResult[] = [];\n    for (const args of commands) {\n      const result = await exec(sandbox, \"agent-browser\", args);\n      results.push(result);\n    }\n    return results;\n  } finally {\n    await sandbox.stop();\n  }\n}\n\n/**\n * Create a reusable snapshot with agent-browser + Chromium pre-installed.\n * Run this once, then set AGENT_BROWSER_SNAPSHOT_ID for fast startup.\n */\nexport async function createSnapshot(): Promise<string> {\n  const sandbox = await Sandbox.create({\n    ...getSandboxCredentials(),\n    runtime: \"node24\",\n    timeout: 300_000,\n  });\n\n  await bootstrapSandbox(sandbox);\n\n  const snapshot = await sandbox.snapshot();\n  return snapshot.snapshotId;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction tryParseJson(str: string): any {\n  try {\n    return JSON.parse(str);\n  } catch {\n    return null;\n  }\n}\n"
  },
  {
    "path": "examples/environments/lib/constants.ts",
    "content": "export const ALLOWED_URLS = [\n  \"https://example.com\",\n  \"https://ai-sdk.dev\",\n  \"https://useworkflow.dev\",\n  \"https://vercel.com\",\n] as const;\n\nexport type AllowedUrl = (typeof ALLOWED_URLS)[number];\n"
  },
  {
    "path": "examples/environments/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/environments/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "examples/environments/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/environments/package.json",
    "content": "{\n  \"name\": \"agent-browser-environments\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@base-ui/react\": \"^1.2.0\",\n    \"@tailwindcss/postcss\": \"^4.2.1\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.36.4\",\n    \"@vercel/sandbox\": \"^1.0.0\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"dotenv\": \"^17.3.1\",\n    \"geist\": \"^1.7.0\",\n    \"lucide-react\": \"^0.577.0\",\n    \"next\": \"^16.1.6\",\n    \"postcss\": \"^8.5.8\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\",\n    \"react-resizable-panels\": \"^4.7.2\",\n    \"shadcn\": \"^4.0.2\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"tailwindcss\": \"^4.2.1\",\n    \"tw-animate-css\": \"^1.4.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.0.0\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"typescript\": \"^5.6.0\"\n  }\n}\n"
  },
  {
    "path": "examples/environments/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/environments/scripts/create-snapshot.ts",
    "content": "/**\n * Create a Vercel Sandbox snapshot with agent-browser + Chromium pre-installed.\n *\n * Run once:   npx tsx scripts/create-snapshot.ts\n * Then set:   AGENT_BROWSER_SNAPSHOT_ID=<output id>\n *\n * Authentication (one of):\n *   - VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID\n *   - VERCEL_OIDC_TOKEN (automatically available on Vercel deployments)\n *\n * This makes sandbox creation sub-second instead of ~30s.\n */\n\nimport \"dotenv/config\";\nimport { createSnapshot, getSandboxCredentials } from \"../lib/agent-browser-sandbox\";\n\nconst hasExplicitCreds = !!(\n  process.env.VERCEL_TOKEN &&\n  process.env.VERCEL_TEAM_ID &&\n  process.env.VERCEL_PROJECT_ID\n);\nconst hasOidc = !!process.env.VERCEL_OIDC_TOKEN;\n\nif (!hasExplicitCreds && !hasOidc) {\n  console.error(\n    \"Missing sandbox credentials. Provide either:\\n\" +\n      \"  1. VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID\\n\" +\n      \"  2. VERCEL_OIDC_TOKEN\",\n  );\n  process.exit(1);\n}\n\nconst creds = getSandboxCredentials();\nconsole.log(\n  creds.token\n    ? `Authenticating with explicit credentials (team: ${creds.teamId})`\n    : \"Authenticating via VERCEL_OIDC_TOKEN\",\n);\n\nasync function main() {\n  console.log(\"Creating Vercel Sandbox with agent-browser + Chromium...\");\n  console.log(\"This takes ~30-60 seconds on first run.\\n\");\n\n  const snapshotId = await createSnapshot();\n\n  console.log(\"\\nSnapshot created successfully!\");\n  console.log(`\\n  AGENT_BROWSER_SNAPSHOT_ID=${snapshotId}\\n`);\n  console.log(\"Add this to your .env.local or Vercel environment variables.\");\n}\n\nmain().catch((err) => {\n  console.error(\"Failed to create snapshot:\", err.message || err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "examples/environments/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"server\"\n  ]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"agent-browser\",\n  \"version\": \"0.21.2\",\n  \"description\": \"Headless browser automation CLI for AI agents\",\n  \"type\": \"module\",\n  \"files\": [\n    \"bin\",\n    \"scripts\",\n    \"skills\"\n  ],\n  \"bin\": {\n    \"agent-browser\": \"./bin/agent-browser.js\"\n  },\n  \"scripts\": {\n    \"version:sync\": \"node scripts/sync-version.js\",\n    \"version\": \"npm run version:sync && git add cli/Cargo.toml\",\n    \"build:native\": \"npm run version:sync && cargo build --release --manifest-path cli/Cargo.toml && node scripts/copy-native.js\",\n    \"build:linux\": \"npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-linux\",\n    \"build:macos\": \"npm run version:sync && (cargo build --release --manifest-path cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path cli/Cargo.toml --target x86_64-apple-darwin & wait) && cp cli/target/aarch64-apple-darwin/release/agent-browser bin/agent-browser-darwin-arm64 && cp cli/target/x86_64-apple-darwin/release/agent-browser bin/agent-browser-darwin-x64\",\n    \"build:windows\": \"npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-windows\",\n    \"build:all-platforms\": \"npm run version:sync && (npm run build:linux & npm run build:windows & wait) && npm run build:macos\",\n    \"build:docker\": \"docker build -t agent-browser-builder -f docker/Dockerfile.build .\",\n    \"release\": \"npm run version:sync && npm run build:all-platforms && npm publish\",\n    \"postinstall\": \"node scripts/postinstall.js\",\n    \"changeset\": \"changeset\",\n    \"ci:version\": \"changeset version && pnpm run version:sync && pnpm install --no-frozen-lockfile\",\n    \"ci:publish\": \"pnpm run version:sync && changeset publish\"\n  },\n  \"keywords\": [\n    \"browser\",\n    \"automation\",\n    \"headless\",\n    \"chrome\",\n    \"cdp\",\n    \"cli\",\n    \"agent\"\n  ],\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/agent-browser.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/agent-browser/issues\"\n  },\n  \"homepage\": \"https://github.com/vercel-labs/agent-browser#readme\",\n  \"devDependencies\": {\n    \"@changesets/cli\": \"^2.29.8\"\n  }\n}\n"
  },
  {
    "path": "scripts/build-all-platforms.sh",
    "content": "#!/bin/bash\nset -e\n\n# Build agent-browser for all platforms using Docker\n# Usage: ./scripts/build-all-platforms.sh\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPROJECT_ROOT=\"$(dirname \"$SCRIPT_DIR\")\"\nOUTPUT_DIR=\"$PROJECT_ROOT/bin\"\n\n# Colors\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\necho -e \"${YELLOW}Building agent-browser for all platforms...${NC}\"\necho \"\"\n\n# Ensure output directory exists\nmkdir -p \"$OUTPUT_DIR\"\n\n# Build the Docker image if needed\necho -e \"${YELLOW}Building Docker cross-compilation image...${NC}\"\ndocker build -t agent-browser-builder -f \"$PROJECT_ROOT/docker/Dockerfile.build\" \"$PROJECT_ROOT\"\n\n# Function to build for a target\nbuild_target() {\n    local target=$1\n    local output_name=$2\n    \n    echo -e \"${YELLOW}Building for ${target}...${NC}\"\n    \n    docker run --rm \\\n        -v \"$PROJECT_ROOT/cli:/build\" \\\n        -v \"$OUTPUT_DIR:/output\" \\\n        agent-browser-builder \\\n        -c \"cargo zigbuild --release --target ${target} && cp /build/target/${target}/release/agent-browser* /output/${output_name} && chmod +x /output/${output_name} 2>/dev/null || true\"\n    \n    if [ -f \"$OUTPUT_DIR/$output_name\" ]; then\n        echo -e \"${GREEN}✓ Built ${output_name}${NC}\"\n    else\n        echo -e \"${RED}✗ Failed to build ${output_name}${NC}\"\n        return 1\n    fi\n}\n\n# Build for each platform\n# Linux x64\nbuild_target \"x86_64-unknown-linux-gnu\" \"agent-browser-linux-x64\"\n\n# Linux ARM64\nbuild_target \"aarch64-unknown-linux-gnu\" \"agent-browser-linux-arm64\"\n\n# Windows x64\nbuild_target \"x86_64-pc-windows-gnu\" \"agent-browser-win32-x64.exe\"\n\n# macOS x64 (via zig for cross-compilation)\nbuild_target \"x86_64-apple-darwin\" \"agent-browser-darwin-x64\"\n\n# macOS ARM64 (via zig for cross-compilation)\nbuild_target \"aarch64-apple-darwin\" \"agent-browser-darwin-arm64\"\n\n# Linux musl x64 (Alpine)\nbuild_target \"x86_64-unknown-linux-musl\" \"agent-browser-linux-musl-x64\"\n\n# Linux musl ARM64 (Alpine)\nbuild_target \"aarch64-unknown-linux-musl\" \"agent-browser-linux-musl-arm64\"\n\necho \"\"\necho -e \"${GREEN}Build complete!${NC}\"\necho \"\"\necho \"Binaries are in: $OUTPUT_DIR\"\nls -la \"$OUTPUT_DIR\"/agent-browser-*\n"
  },
  {
    "path": "scripts/check-version-sync.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Verifies that package.json and cli/Cargo.toml have the same version.\n * Used in CI to catch version drift.\n */\n\nimport { readFileSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst rootDir = join(__dirname, '..');\n\n// Read package.json version\nconst packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));\nconst packageVersion = packageJson.version;\n\n// Read Cargo.toml version\nconst cargoToml = readFileSync(join(rootDir, 'cli/Cargo.toml'), 'utf-8');\nconst cargoVersionMatch = cargoToml.match(/^version\\s*=\\s*\"([^\"]*)\"/m);\n\nif (!cargoVersionMatch) {\n  console.error('Could not find version in cli/Cargo.toml');\n  process.exit(1);\n}\n\nconst cargoVersion = cargoVersionMatch[1];\n\nif (packageVersion !== cargoVersion) {\n  console.error('Version mismatch detected!');\n  console.error(`  package.json:    ${packageVersion}`);\n  console.error(`  cli/Cargo.toml:  ${cargoVersion}`);\n  console.error('');\n  console.error(\"Run 'pnpm run version:sync' to fix this.\");\n  process.exit(1);\n}\n\nconsole.log(`Versions are in sync: ${packageVersion}`);\n"
  },
  {
    "path": "scripts/copy-native.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Copies the compiled Rust binary to bin/ with platform-specific naming\n */\n\nimport { copyFileSync, existsSync, mkdirSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\nimport { platform, arch } from 'os';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst projectRoot = join(__dirname, '..');\n\nconst sourceExt = platform() === 'win32' ? '.exe' : '';\nconst sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);\nconst binDir = join(projectRoot, 'bin');\n\n// Determine platform suffix\nconst platformKey = `${platform()}-${arch()}`;\nconst ext = platform() === 'win32' ? '.exe' : '';\nconst targetName = `agent-browser-${platformKey}${ext}`;\nconst targetPath = join(binDir, targetName);\n\nif (!existsSync(sourcePath)) {\n  console.error(`Error: Native binary not found at ${sourcePath}`);\n  console.error('Run \"cargo build --release --manifest-path cli/Cargo.toml\" first');\n  process.exit(1);\n}\n\nif (!existsSync(binDir)) {\n  mkdirSync(binDir, { recursive: true });\n}\n\ncopyFileSync(sourcePath, targetPath);\nconsole.log(`✓ Copied native binary to ${targetPath}`);\n"
  },
  {
    "path": "scripts/postinstall.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Postinstall script for agent-browser\n * \n * Downloads the platform-specific native binary if not present.\n * On global installs, patches npm's bin entry to use the native binary directly:\n * - Windows: Overwrites .cmd/.ps1 shims\n * - Mac/Linux: Replaces symlink to point to native binary\n */\n\nimport { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, symlinkSync, lstatSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\nimport { platform, arch } from 'os';\nimport { get } from 'https';\nimport { execSync } from 'child_process';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst projectRoot = join(__dirname, '..');\nconst binDir = join(projectRoot, 'bin');\n\n// Detect if the system uses musl libc (e.g. Alpine Linux)\nfunction isMusl() {\n  if (platform() !== 'linux') return false;\n  try {\n    const result = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });\n    return result.toLowerCase().includes('musl');\n  } catch {\n    return existsSync('/lib/ld-musl-x86_64.so.1') || existsSync('/lib/ld-musl-aarch64.so.1');\n  }\n}\n\n// Platform detection\nconst osKey = platform() === 'linux' && isMusl() ? 'linux-musl' : platform();\nconst platformKey = `${osKey}-${arch()}`;\nconst ext = platform() === 'win32' ? '.exe' : '';\nconst binaryName = `agent-browser-${platformKey}${ext}`;\nconst binaryPath = join(binDir, binaryName);\n\n// Package info\nconst packageJson = JSON.parse(\n  (await import('fs')).readFileSync(join(projectRoot, 'package.json'), 'utf8')\n);\nconst version = packageJson.version;\n\n// GitHub release URL\nconst GITHUB_REPO = 'vercel-labs/agent-browser';\nconst DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;\n\nasync function downloadFile(url, dest) {\n  return new Promise((resolve, reject) => {\n    const file = createWriteStream(dest);\n    \n    const request = (url) => {\n      get(url, (response) => {\n        // Handle redirects\n        if (response.statusCode === 301 || response.statusCode === 302) {\n          request(response.headers.location);\n          return;\n        }\n        \n        if (response.statusCode !== 200) {\n          reject(new Error(`Failed to download: HTTP ${response.statusCode}`));\n          return;\n        }\n        \n        response.pipe(file);\n        file.on('finish', () => {\n          file.close();\n          resolve();\n        });\n      }).on('error', (err) => {\n        unlinkSync(dest);\n        reject(err);\n      });\n    };\n    \n    request(url);\n  });\n}\n\nasync function main() {\n  // Check if binary already exists\n  if (existsSync(binaryPath)) {\n    // Ensure binary is executable (npm doesn't preserve execute bit)\n    if (platform() !== 'win32') {\n      chmodSync(binaryPath, 0o755);\n    }\n    console.log(`✓ Native binary ready: ${binaryName}`);\n    \n    // On global installs, fix npm's bin entry to use native binary directly\n    await fixGlobalInstallBin();\n    \n    showInstallReminder();\n    return;\n  }\n\n  // Ensure bin directory exists\n  if (!existsSync(binDir)) {\n    mkdirSync(binDir, { recursive: true });\n  }\n\n  console.log(`Downloading native binary for ${platformKey}...`);\n  console.log(`URL: ${DOWNLOAD_URL}`);\n\n  try {\n    await downloadFile(DOWNLOAD_URL, binaryPath);\n    \n    // Make executable on Unix\n    if (platform() !== 'win32') {\n      chmodSync(binaryPath, 0o755);\n    }\n    \n    console.log(`✓ Downloaded native binary: ${binaryName}`);\n  } catch (err) {\n    console.log(`Could not download native binary: ${err.message}`);\n    console.log('');\n    console.log('To build the native binary locally:');\n    console.log('  1. Install Rust: https://rustup.rs');\n    console.log('  2. Run: npm run build:native');\n  }\n\n  // On global installs, fix npm's bin entry to use native binary directly\n  // This avoids the /bin/sh error on Windows and provides zero-overhead execution\n  await fixGlobalInstallBin();\n\n  showInstallReminder();\n}\n\nfunction findSystemChrome() {\n  const os = platform();\n  if (os === 'darwin') {\n    const candidates = [\n      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n      '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',\n      '/Applications/Chromium.app/Contents/MacOS/Chromium',\n    ];\n    return candidates.find(p => existsSync(p)) || null;\n  }\n  if (os === 'linux') {\n    const names = ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium'];\n    for (const name of names) {\n      try {\n        const result = execSync(`which ${name} 2>/dev/null`, { encoding: 'utf8' }).trim();\n        if (result) return result;\n      } catch {}\n    }\n    return null;\n  }\n  if (os === 'win32') {\n    const candidates = [\n      `${process.env.LOCALAPPDATA}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n      'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n      'C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',\n    ];\n    return candidates.find(p => p && existsSync(p)) || null;\n  }\n  return null;\n}\n\nfunction showInstallReminder() {\n  const systemChrome = findSystemChrome();\n  if (systemChrome) {\n    console.log('');\n    console.log(`  ✓ System Chrome found: ${systemChrome}`);\n    console.log('    agent-browser will use it automatically.');\n    console.log('');\n    return;\n  }\n\n  console.log('');\n  console.log('  ⚠ No Chrome installation detected.');\n  console.log('  If you plan to use a local browser, run:');\n  console.log('');\n  console.log('    agent-browser install');\n  if (platform() === 'linux') {\n    console.log('');\n    console.log('  On Linux, include system dependencies with:');\n    console.log('');\n    console.log('    agent-browser install --with-deps');\n  }\n  console.log('');\n  console.log('  You can skip this if you use --cdp, --provider, --engine, or --executable-path.');\n  console.log('');\n}\n\n/**\n * Fix npm's bin entry on global installs to use the native binary directly.\n * This provides zero-overhead CLI execution for global installs.\n */\nasync function fixGlobalInstallBin() {\n  if (platform() === 'win32') {\n    await fixWindowsShims();\n  } else {\n    await fixUnixSymlink();\n  }\n}\n\n/**\n * Fix npm symlink on Mac/Linux global installs.\n * Replace the symlink to the JS wrapper with a symlink to the native binary.\n */\nasync function fixUnixSymlink() {\n  // Get npm's global bin directory (npm prefix -g + /bin)\n  let npmBinDir;\n  try {\n    const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();\n    npmBinDir = join(prefix, 'bin');\n  } catch {\n    return; // npm not available\n  }\n\n  const symlinkPath = join(npmBinDir, 'agent-browser');\n\n  // Check if symlink exists (indicates global install)\n  try {\n    const stat = lstatSync(symlinkPath);\n    if (!stat.isSymbolicLink()) {\n      return; // Not a symlink, don't touch it\n    }\n  } catch {\n    return; // Symlink doesn't exist, not a global install\n  }\n\n  // Replace symlink to point directly to native binary\n  try {\n    unlinkSync(symlinkPath);\n    symlinkSync(binaryPath, symlinkPath);\n    console.log('✓ Optimized: symlink points to native binary (zero overhead)');\n  } catch (err) {\n    // Permission error or other issue - not critical, JS wrapper still works\n    console.log(`⚠ Could not optimize symlink: ${err.message}`);\n    console.log('  CLI will work via Node.js wrapper (slightly slower startup)');\n  }\n}\n\n/**\n * Fix npm-generated shims on Windows global installs.\n * npm generates shims that try to run /bin/sh, which doesn't exist on Windows.\n * We overwrite them to invoke the native .exe directly.\n */\nasync function fixWindowsShims() {\n  let npmBinDir;\n  try {\n    npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();\n  } catch {\n    return;\n  }\n\n  const cmdShim = join(npmBinDir, 'agent-browser.cmd');\n  const ps1Shim = join(npmBinDir, 'agent-browser.ps1');\n\n  // Shims may not exist yet during postinstall (npm creates them after\n  // lifecycle scripts). If missing, fall back: the JS wrapper at\n  // bin/agent-browser.js handles Windows correctly via child_process.spawn.\n  if (!existsSync(cmdShim)) {\n    return;\n  }\n\n  // Detect architecture so ARM64 Windows is handled correctly\n  const cpuArch = arch() === 'arm64' ? 'arm64' : 'x64';\n  const relativeBinaryPath = `node_modules\\\\agent-browser\\\\bin\\\\agent-browser-win32-${cpuArch}.exe`;\n  const absoluteBinaryPath = join(npmBinDir, relativeBinaryPath);\n\n  // Only rewrite shims if the native binary actually exists\n  if (!existsSync(absoluteBinaryPath)) {\n    return;\n  }\n\n  try {\n    const cmdContent = `@ECHO off\\r\\n\"%~dp0${relativeBinaryPath}\" %*\\r\\n`;\n    writeFileSync(cmdShim, cmdContent);\n\n    const ps1Content = `#!/usr/bin/env pwsh\\r\\n$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent\\r\\n& \"$basedir\\\\${relativeBinaryPath}\" $args\\r\\nexit $LASTEXITCODE\\r\\n`;\n    writeFileSync(ps1Shim, ps1Content);\n\n    console.log('✓ Optimized: shims point to native binary (zero overhead)');\n  } catch (err) {\n    console.log(`⚠ Could not optimize shims: ${err.message}`);\n    console.log('  CLI will work via Node.js wrapper (slightly slower startup)');\n  }\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "scripts/sync-version.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Syncs the version from package.json to all other config files.\n * Run this script before building or releasing.\n */\n\nimport { execSync } from \"child_process\";\nimport { readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst rootDir = join(__dirname, \"..\");\nconst cliDir = join(rootDir, \"cli\");\n\n// Read version from package.json (single source of truth)\nconst packageJson = JSON.parse(\n  readFileSync(join(rootDir, \"package.json\"), \"utf-8\")\n);\nconst version = packageJson.version;\n\nconsole.log(`Syncing version ${version} to all config files...`);\n\n// Update Cargo.toml\nconst cargoTomlPath = join(cliDir, \"Cargo.toml\");\nlet cargoToml = readFileSync(cargoTomlPath, \"utf-8\");\nconst cargoVersionRegex = /^version\\s*=\\s*\"[^\"]*\"/m;\nconst newCargoVersion = `version = \"${version}\"`;\n\nlet cargoTomlUpdated = false;\nif (cargoVersionRegex.test(cargoToml)) {\n  const oldMatch = cargoToml.match(cargoVersionRegex)?.[0];\n  if (oldMatch !== newCargoVersion) {\n    cargoToml = cargoToml.replace(cargoVersionRegex, newCargoVersion);\n    writeFileSync(cargoTomlPath, cargoToml);\n    console.log(`  Updated cli/Cargo.toml: ${oldMatch} -> ${newCargoVersion}`);\n    cargoTomlUpdated = true;\n  } else {\n    console.log(`  cli/Cargo.toml already up to date`);\n  }\n} else {\n  console.error(\"  Could not find version field in cli/Cargo.toml\");\n  process.exit(1);\n}\n\n// Update Cargo.lock to match Cargo.toml\nif (cargoTomlUpdated) {\n  try {\n    execSync(\"cargo update -p agent-browser --offline\", {\n      cwd: cliDir,\n      stdio: \"pipe\",\n    });\n    console.log(`  Updated cli/Cargo.lock`);\n  } catch {\n    // --offline may fail if package not in cache, try without it\n    try {\n      execSync(\"cargo update -p agent-browser\", {\n        cwd: cliDir,\n        stdio: \"pipe\",\n      });\n      console.log(`  Updated cli/Cargo.lock`);\n    } catch (e) {\n      console.error(`  Warning: Could not update Cargo.lock: ${e.message}`);\n    }\n  }\n}\n\nconsole.log(\"Version sync complete.\");\n"
  },
  {
    "path": "skills/agent-browser/SKILL.md",
    "content": "---\nname: agent-browser\ndescription: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to \"open a website\", \"fill out a form\", \"click a button\", \"take a screenshot\", \"scrape data from a page\", \"test this web app\", \"login to a site\", \"automate browser actions\", or any task requiring programmatic web interaction.\nallowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*)\n---\n\n# Browser Automation with agent-browser\n\nThe CLI uses Chrome/Chromium via CDP directly. Install via `npm i -g agent-browser`, `brew install agent-browser`, or `cargo install agent-browser`. Run `agent-browser install` to download Chrome. Run `agent-browser upgrade` to update to the latest version.\n\n## Core Workflow\n\nEvery browser automation follows this pattern:\n\n1. **Navigate**: `agent-browser open <url>`\n2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)\n3. **Interact**: Use refs to click, fill, select\n4. **Re-snapshot**: After navigation or DOM changes, get fresh refs\n\n```bash\nagent-browser open https://example.com/form\nagent-browser snapshot -i\n# Output: @e1 [input type=\"email\"], @e2 [input type=\"password\"], @e3 [button] \"Submit\"\n\nagent-browser fill @e1 \"user@example.com\"\nagent-browser fill @e2 \"password123\"\nagent-browser click @e3\nagent-browser wait --load networkidle\nagent-browser snapshot -i  # Check result\n```\n\n## Command Chaining\n\nCommands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls.\n\n```bash\n# Chain open + wait + snapshot in one call\nagent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i\n\n# Chain multiple interactions\nagent-browser fill @e1 \"user@example.com\" && agent-browser fill @e2 \"password123\" && agent-browser click @e3\n\n# Navigate and capture\nagent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png\n```\n\n**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs).\n\n## Handling Authentication\n\nWhen automating a site that requires login, choose the approach that fits:\n\n**Option 1: Import auth from the user's browser (fastest for one-off tasks)**\n\n```bash\n# Connect to the user's running Chrome (they're already logged in)\nagent-browser --auto-connect state save ./auth.json\n# Use that auth state\nagent-browser --state ./auth.json open https://app.example.com/dashboard\n```\n\nState files contain session tokens in plaintext -- add to `.gitignore` and delete when no longer needed. Set `AGENT_BROWSER_ENCRYPTION_KEY` for encryption at rest.\n\n**Option 2: Persistent profile (simplest for recurring tasks)**\n\n```bash\n# First run: login manually or via automation\nagent-browser --profile ~/.myapp open https://app.example.com/login\n# ... fill credentials, submit ...\n\n# All future runs: already authenticated\nagent-browser --profile ~/.myapp open https://app.example.com/dashboard\n```\n\n**Option 3: Session name (auto-save/restore cookies + localStorage)**\n\n```bash\nagent-browser --session-name myapp open https://app.example.com/login\n# ... login flow ...\nagent-browser close  # State auto-saved\n\n# Next time: state auto-restored\nagent-browser --session-name myapp open https://app.example.com/dashboard\n```\n\n**Option 4: Auth vault (credentials stored encrypted, login by name)**\n\n```bash\necho \"$PASSWORD\" | agent-browser auth save myapp --url https://app.example.com/login --username user --password-stdin\nagent-browser auth login myapp\n```\n\n**Option 5: State file (manual save/load)**\n\n```bash\n# After logging in:\nagent-browser state save ./auth.json\n# In a future session:\nagent-browser state load ./auth.json\nagent-browser open https://app.example.com/dashboard\n```\n\nSee [references/authentication.md](references/authentication.md) for OAuth, 2FA, cookie-based auth, and token refresh patterns.\n\n## Essential Commands\n\n```bash\n# Navigation\nagent-browser open <url>              # Navigate (aliases: goto, navigate)\nagent-browser close                   # Close browser\n\n# Snapshot\nagent-browser snapshot -i             # Interactive elements with refs (recommended)\nagent-browser snapshot -i -C          # Include cursor-interactive elements (divs with onclick, cursor:pointer)\nagent-browser snapshot -s \"#selector\" # Scope to CSS selector\n\n# Interaction (use @refs from snapshot)\nagent-browser click @e1               # Click element\nagent-browser click @e1 --new-tab     # Click and open in new tab\nagent-browser fill @e2 \"text\"         # Clear and type text\nagent-browser type @e2 \"text\"         # Type without clearing\nagent-browser select @e1 \"option\"     # Select dropdown option\nagent-browser check @e1               # Check checkbox\nagent-browser press Enter             # Press key\nagent-browser keyboard type \"text\"    # Type at current focus (no selector)\nagent-browser keyboard inserttext \"text\"  # Insert without key events\nagent-browser scroll down 500         # Scroll page\nagent-browser scroll down 500 --selector \"div.content\"  # Scroll within a specific container\n\n# Get information\nagent-browser get text @e1            # Get element text\nagent-browser get url                 # Get current URL\nagent-browser get title               # Get page title\nagent-browser get cdp-url             # Get CDP WebSocket URL\n\n# Wait\nagent-browser wait @e1                # Wait for element\nagent-browser wait --load networkidle # Wait for network idle\nagent-browser wait --url \"**/page\"    # Wait for URL pattern\nagent-browser wait 2000               # Wait milliseconds\nagent-browser wait --text \"Welcome\"    # Wait for text to appear (substring match)\nagent-browser wait --fn \"!document.body.innerText.includes('Loading...')\"  # Wait for text to disappear\nagent-browser wait \"#spinner\" --state hidden  # Wait for element to disappear\n\n# Downloads\nagent-browser download @e1 ./file.pdf          # Click element to trigger download\nagent-browser wait --download ./output.zip     # Wait for any download to complete\nagent-browser --download-path ./downloads open <url>  # Set default download directory\n\n# Network\nagent-browser network requests                 # Inspect tracked requests\nagent-browser network route \"**/api/*\" --abort  # Block matching requests\nagent-browser network har start                # Start HAR recording\nagent-browser network har stop ./capture.har   # Stop and save HAR file\n\n# Viewport & Device Emulation\nagent-browser set viewport 1920 1080          # Set viewport size (default: 1280x720)\nagent-browser set viewport 1920 1080 2        # 2x retina (same CSS size, higher res screenshots)\nagent-browser set device \"iPhone 14\"          # Emulate device (viewport + user agent)\n\n# Capture\nagent-browser screenshot              # Screenshot to temp dir\nagent-browser screenshot --full       # Full page screenshot\nagent-browser screenshot --annotate   # Annotated screenshot with numbered element labels\nagent-browser screenshot --screenshot-dir ./shots  # Save to custom directory\nagent-browser screenshot --screenshot-format jpeg --screenshot-quality 80\nagent-browser pdf output.pdf          # Save as PDF\n\n# Clipboard\nagent-browser clipboard read                      # Read text from clipboard\nagent-browser clipboard write \"Hello, World!\"     # Write text to clipboard\nagent-browser clipboard copy                      # Copy current selection\nagent-browser clipboard paste                     # Paste from clipboard\n\n# Diff (compare page states)\nagent-browser diff snapshot                          # Compare current vs last snapshot\nagent-browser diff snapshot --baseline before.txt    # Compare current vs saved file\nagent-browser diff screenshot --baseline before.png  # Visual pixel diff\nagent-browser diff url <url1> <url2>                 # Compare two pages\nagent-browser diff url <url1> <url2> --wait-until networkidle  # Custom wait strategy\nagent-browser diff url <url1> <url2> --selector \"#main\"  # Scope to element\n```\n\n## Batch Execution\n\nExecute multiple commands in a single invocation by piping a JSON array of string arrays to `batch`. This avoids per-command process startup overhead when running multi-step workflows.\n\n```bash\necho '[\n  [\"open\", \"https://example.com\"],\n  [\"snapshot\", \"-i\"],\n  [\"click\", \"@e1\"],\n  [\"screenshot\", \"result.png\"]\n]' | agent-browser batch --json\n\n# Stop on first error\nagent-browser batch --bail < commands.json\n```\n\nUse `batch` when you have a known sequence of commands that don't depend on intermediate output. Use separate commands or `&&` chaining when you need to parse output between steps (e.g., snapshot to discover refs, then interact).\n\n## Common Patterns\n\n### Form Submission\n\n```bash\nagent-browser open https://example.com/signup\nagent-browser snapshot -i\nagent-browser fill @e1 \"Jane Doe\"\nagent-browser fill @e2 \"jane@example.com\"\nagent-browser select @e3 \"California\"\nagent-browser check @e4\nagent-browser click @e5\nagent-browser wait --load networkidle\n```\n\n### Authentication with Auth Vault (Recommended)\n\n```bash\n# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY)\n# Recommended: pipe password via stdin to avoid shell history exposure\necho \"pass\" | agent-browser auth save github --url https://github.com/login --username user --password-stdin\n\n# Login using saved profile (LLM never sees password)\nagent-browser auth login github\n\n# List/show/delete profiles\nagent-browser auth list\nagent-browser auth show github\nagent-browser auth delete github\n```\n\n### Authentication with State Persistence\n\n```bash\n# Login once and save state\nagent-browser open https://app.example.com/login\nagent-browser snapshot -i\nagent-browser fill @e1 \"$USERNAME\"\nagent-browser fill @e2 \"$PASSWORD\"\nagent-browser click @e3\nagent-browser wait --url \"**/dashboard\"\nagent-browser state save auth.json\n\n# Reuse in future sessions\nagent-browser state load auth.json\nagent-browser open https://app.example.com/dashboard\n```\n\n### Session Persistence\n\n```bash\n# Auto-save/restore cookies and localStorage across browser restarts\nagent-browser --session-name myapp open https://app.example.com/login\n# ... login flow ...\nagent-browser close  # State auto-saved to ~/.agent-browser/sessions/\n\n# Next time, state is auto-loaded\nagent-browser --session-name myapp open https://app.example.com/dashboard\n\n# Encrypt state at rest\nexport AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)\nagent-browser --session-name secure open https://app.example.com\n\n# Manage saved states\nagent-browser state list\nagent-browser state show myapp-default.json\nagent-browser state clear myapp\nagent-browser state clean --older-than 7\n```\n\n### Working with Iframes\n\nIframe content is automatically inlined in snapshots. Refs inside iframes carry frame context, so you can interact with them directly.\n\n```bash\nagent-browser open https://example.com/checkout\nagent-browser snapshot -i\n# @e1 [heading] \"Checkout\"\n# @e2 [Iframe] \"payment-frame\"\n#   @e3 [input] \"Card number\"\n#   @e4 [input] \"Expiry\"\n#   @e5 [button] \"Pay\"\n\n# Interact directly — no frame switch needed\nagent-browser fill @e3 \"4111111111111111\"\nagent-browser fill @e4 \"12/28\"\nagent-browser click @e5\n\n# To scope a snapshot to one iframe:\nagent-browser frame @e2\nagent-browser snapshot -i         # Only iframe content\nagent-browser frame main          # Return to main frame\n```\n\n### Data Extraction\n\n```bash\nagent-browser open https://example.com/products\nagent-browser snapshot -i\nagent-browser get text @e5           # Get specific element text\nagent-browser get text body > page.txt  # Get all page text\n\n# JSON output for parsing\nagent-browser snapshot -i --json\nagent-browser get text @e1 --json\n```\n\n### Parallel Sessions\n\n```bash\nagent-browser --session site1 open https://site-a.com\nagent-browser --session site2 open https://site-b.com\n\nagent-browser --session site1 snapshot -i\nagent-browser --session site2 snapshot -i\n\nagent-browser session list\n```\n\n### Connect to Existing Chrome\n\n```bash\n# Auto-discover running Chrome with remote debugging enabled\nagent-browser --auto-connect open https://example.com\nagent-browser --auto-connect snapshot\n\n# Or with explicit CDP port\nagent-browser --cdp 9222 snapshot\n```\n\nAuto-connect discovers Chrome via `DevToolsActivePort`, common debugging ports (9222, 9229), and falls back to a direct WebSocket connection if HTTP-based CDP discovery fails.\n\n### Color Scheme (Dark Mode)\n\n```bash\n# Persistent dark mode via flag (applies to all pages and new tabs)\nagent-browser --color-scheme dark open https://example.com\n\n# Or via environment variable\nAGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com\n\n# Or set during session (persists for subsequent commands)\nagent-browser set media dark\n```\n\n### Viewport & Responsive Testing\n\n```bash\n# Set a custom viewport size (default is 1280x720)\nagent-browser set viewport 1920 1080\nagent-browser screenshot desktop.png\n\n# Test mobile-width layout\nagent-browser set viewport 375 812\nagent-browser screenshot mobile.png\n\n# Retina/HiDPI: same CSS layout at 2x pixel density\n# Screenshots stay at logical viewport size, but content renders at higher DPI\nagent-browser set viewport 1920 1080 2\nagent-browser screenshot retina.png\n\n# Device emulation (sets viewport + user agent in one step)\nagent-browser set device \"iPhone 14\"\nagent-browser screenshot device.png\n```\n\nThe `scale` parameter (3rd argument) sets `window.devicePixelRatio` without changing CSS layout. Use it when testing retina rendering or capturing higher-resolution screenshots.\n\n### Visual Browser (Debugging)\n\n```bash\nagent-browser --headed open https://example.com\nagent-browser highlight @e1          # Highlight element\nagent-browser inspect                # Open Chrome DevTools for the active page\nagent-browser record start demo.webm # Record session\nagent-browser profiler start         # Start Chrome DevTools profiling\nagent-browser profiler stop trace.json # Stop and save profile (path optional)\n```\n\nUse `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode.\n\n### Local Files (PDFs, HTML)\n\n```bash\n# Open local files with file:// URLs\nagent-browser --allow-file-access open file:///path/to/document.pdf\nagent-browser --allow-file-access open file:///path/to/page.html\nagent-browser screenshot output.png\n```\n\n### iOS Simulator (Mobile Safari)\n\n```bash\n# List available iOS simulators\nagent-browser device list\n\n# Launch Safari on a specific device\nagent-browser -p ios --device \"iPhone 16 Pro\" open https://example.com\n\n# Same workflow as desktop - snapshot, interact, re-snapshot\nagent-browser -p ios snapshot -i\nagent-browser -p ios tap @e1          # Tap (alias for click)\nagent-browser -p ios fill @e2 \"text\"\nagent-browser -p ios swipe up         # Mobile-specific gesture\n\n# Take screenshot\nagent-browser -p ios screenshot mobile.png\n\n# Close session (shuts down simulator)\nagent-browser -p ios close\n```\n\n**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)\n\n**Real devices:** Works with physical iOS devices if pre-configured. Use `--device \"<UDID>\"` where UDID is from `xcrun xctrace list devices`.\n\n## Security\n\nAll security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output.\n\n### Content Boundaries (Recommended for AI Agents)\n\nEnable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content:\n\n```bash\nexport AGENT_BROWSER_CONTENT_BOUNDARIES=1\nagent-browser snapshot\n# Output:\n# --- AGENT_BROWSER_PAGE_CONTENT nonce=<hex> origin=https://example.com ---\n# [accessibility tree]\n# --- END_AGENT_BROWSER_PAGE_CONTENT nonce=<hex> ---\n```\n\n### Domain Allowlist\n\nRestrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on:\n\n```bash\nexport AGENT_BROWSER_ALLOWED_DOMAINS=\"example.com,*.example.com\"\nagent-browser open https://example.com        # OK\nagent-browser open https://malicious.com       # Blocked\n```\n\n### Action Policy\n\nUse a policy file to gate destructive actions:\n\n```bash\nexport AGENT_BROWSER_ACTION_POLICY=./policy.json\n```\n\nExample `policy.json`:\n\n```json\n{ \"default\": \"deny\", \"allow\": [\"navigate\", \"snapshot\", \"click\", \"scroll\", \"wait\", \"get\"] }\n```\n\nAuth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies.\n\n### Output Limits\n\nPrevent context flooding from large pages:\n\n```bash\nexport AGENT_BROWSER_MAX_OUTPUT=50000\n```\n\n## Diffing (Verifying Changes)\n\nUse `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session.\n\n```bash\n# Typical workflow: snapshot -> action -> diff\nagent-browser snapshot -i          # Take baseline snapshot\nagent-browser click @e2            # Perform action\nagent-browser diff snapshot        # See what changed (auto-compares to last snapshot)\n```\n\nFor visual regression testing or monitoring:\n\n```bash\n# Save a baseline screenshot, then compare later\nagent-browser screenshot baseline.png\n# ... time passes or changes are made ...\nagent-browser diff screenshot --baseline baseline.png\n\n# Compare staging vs production\nagent-browser diff url https://staging.example.com https://prod.example.com --screenshot\n```\n\n`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage.\n\n## Timeouts and Slow Pages\n\nThe default timeout is 25 seconds. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout:\n\n```bash\n# Wait for network activity to settle (best for slow pages)\nagent-browser wait --load networkidle\n\n# Wait for a specific element to appear\nagent-browser wait \"#content\"\nagent-browser wait @e1\n\n# Wait for a specific URL pattern (useful after redirects)\nagent-browser wait --url \"**/dashboard\"\n\n# Wait for a JavaScript condition\nagent-browser wait --fn \"document.readyState === 'complete'\"\n\n# Wait a fixed duration (milliseconds) as a last resort\nagent-browser wait 5000\n```\n\nWhen dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait <selector>` or `wait @ref`.\n\n## Session Management and Cleanup\n\nWhen running multiple agents or automations concurrently, always use named sessions to avoid conflicts:\n\n```bash\n# Each agent gets its own isolated session\nagent-browser --session agent1 open site-a.com\nagent-browser --session agent2 open site-b.com\n\n# Check active sessions\nagent-browser session list\n```\n\nAlways close your browser session when done to avoid leaked processes:\n\n```bash\nagent-browser close                    # Close default session\nagent-browser --session agent1 close   # Close specific session\n```\n\nIf a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work.\n\nTo auto-shutdown the daemon after a period of inactivity (useful for ephemeral/CI environments):\n\n```bash\nAGENT_BROWSER_IDLE_TIMEOUT_MS=60000 agent-browser open example.com\n```\n\n## Ref Lifecycle (Important)\n\nRefs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:\n\n- Clicking links or buttons that navigate\n- Form submissions\n- Dynamic content loading (dropdowns, modals)\n\n```bash\nagent-browser click @e5              # Navigates to new page\nagent-browser snapshot -i            # MUST re-snapshot\nagent-browser click @e1              # Use new refs\n```\n\n## Annotated Screenshots (Vision Mode)\n\nUse `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot.\n\n```bash\nagent-browser screenshot --annotate\n# Output includes the image path and a legend:\n#   [1] @e1 button \"Submit\"\n#   [2] @e2 link \"Home\"\n#   [3] @e3 textbox \"Email\"\nagent-browser click @e2              # Click using ref from annotated screenshot\n```\n\nUse annotated screenshots when:\n\n- The page has unlabeled icon buttons or visual-only elements\n- You need to verify visual layout or styling\n- Canvas or chart elements are present (invisible to text snapshots)\n- You need spatial reasoning about element positions\n\n## Semantic Locators (Alternative to Refs)\n\nWhen refs are unavailable or unreliable, use semantic locators:\n\n```bash\nagent-browser find text \"Sign In\" click\nagent-browser find label \"Email\" fill \"user@test.com\"\nagent-browser find role button click --name \"Submit\"\nagent-browser find placeholder \"Search\" type \"query\"\nagent-browser find testid \"submit-btn\" click\n```\n\n## JavaScript Evaluation (eval)\n\nUse `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues.\n\n```bash\n# Simple expressions work with regular quoting\nagent-browser eval 'document.title'\nagent-browser eval 'document.querySelectorAll(\"img\").length'\n\n# Complex JS: use --stdin with heredoc (RECOMMENDED)\nagent-browser eval --stdin <<'EVALEOF'\nJSON.stringify(\n  Array.from(document.querySelectorAll(\"img\"))\n    .filter(i => !i.alt)\n    .map(i => ({ src: i.src.split(\"/\").pop(), width: i.width }))\n)\nEVALEOF\n\n# Alternative: base64 encoding (avoids all shell escaping issues)\nagent-browser eval -b \"$(echo -n 'Array.from(document.querySelectorAll(\"a\")).map(a => a.href)' | base64)\"\n```\n\n**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.\n\n**Rules of thumb:**\n\n- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine\n- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`\n- Programmatic/generated scripts -> use `eval -b` with base64\n\n## Configuration File\n\nCreate `agent-browser.json` in the project root for persistent settings:\n\n```json\n{\n  \"headed\": true,\n  \"proxy\": \"http://localhost:8080\",\n  \"profile\": \"./browser-data\"\n}\n```\n\nPriority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config <path>` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `\"executablePath\"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced.\n\n## Deep-Dive Documentation\n\n| Reference                                                            | When to Use                                               |\n| -------------------------------------------------------------------- | --------------------------------------------------------- |\n| [references/commands.md](references/commands.md)                     | Full command reference with all options                   |\n| [references/snapshot-refs.md](references/snapshot-refs.md)           | Ref lifecycle, invalidation rules, troubleshooting        |\n| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |\n| [references/authentication.md](references/authentication.md)         | Login flows, OAuth, 2FA handling, state reuse             |\n| [references/video-recording.md](references/video-recording.md)       | Recording workflows for debugging and documentation       |\n| [references/profiling.md](references/profiling.md)                   | Chrome DevTools profiling for performance analysis        |\n| [references/proxy-support.md](references/proxy-support.md)           | Proxy configuration, geo-testing, rotating proxies        |\n\n## Browser Engine Selection\n\nUse `--engine` to choose a local browser engine. The default is `chrome`.\n\n```bash\n# Use Lightpanda (fast headless browser, requires separate install)\nagent-browser --engine lightpanda open example.com\n\n# Via environment variable\nexport AGENT_BROWSER_ENGINE=lightpanda\nagent-browser open example.com\n\n# With custom binary path\nagent-browser --engine lightpanda --executable-path /path/to/lightpanda open example.com\n```\n\nSupported engines:\n- `chrome` (default) -- Chrome/Chromium via CDP\n- `lightpanda` -- Lightpanda headless browser via CDP (10x faster, 10x less memory than Chrome)\n\nLightpanda does not support `--extension`, `--profile`, `--state`, or `--allow-file-access`. Install Lightpanda from https://lightpanda.io/docs/open-source/installation.\n\n## Ready-to-Use Templates\n\n| Template                                                                 | Description                         |\n| ------------------------------------------------------------------------ | ----------------------------------- |\n| [templates/form-automation.sh](templates/form-automation.sh)             | Form filling with validation        |\n| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state             |\n| [templates/capture-workflow.sh](templates/capture-workflow.sh)           | Content extraction with screenshots |\n\n```bash\n./templates/form-automation.sh https://example.com/form\n./templates/authenticated-session.sh https://app.example.com/login\n./templates/capture-workflow.sh https://example.com ./output\n```\n"
  },
  {
    "path": "skills/agent-browser/references/authentication.md",
    "content": "# Authentication Patterns\n\nLogin flows, session persistence, OAuth, 2FA, and authenticated browsing.\n\n**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start.\n\n## Contents\n\n- [Import Auth from Your Browser](#import-auth-from-your-browser)\n- [Persistent Profiles](#persistent-profiles)\n- [Session Persistence](#session-persistence)\n- [Basic Login Flow](#basic-login-flow)\n- [Saving Authentication State](#saving-authentication-state)\n- [Restoring Authentication](#restoring-authentication)\n- [OAuth / SSO Flows](#oauth--sso-flows)\n- [Two-Factor Authentication](#two-factor-authentication)\n- [HTTP Basic Auth](#http-basic-auth)\n- [Cookie-Based Auth](#cookie-based-auth)\n- [Token Refresh Handling](#token-refresh-handling)\n- [Security Best Practices](#security-best-practices)\n\n## Import Auth from Your Browser\n\nThe fastest way to authenticate is to reuse cookies from a Chrome session you are already logged into.\n\n**Step 1: Start Chrome with remote debugging**\n\n```bash\n# macOS\n\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" --remote-debugging-port=9222\n\n# Linux\ngoogle-chrome --remote-debugging-port=9222\n\n# Windows\n\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222\n```\n\nLog in to your target site(s) in this Chrome window as you normally would.\n\n> **Security note:** `--remote-debugging-port` exposes full browser control on localhost. Any local process can connect and read cookies, execute JS, etc. Only use on trusted machines and close Chrome when done.\n\n**Step 2: Grab the auth state**\n\n```bash\n# Auto-discover the running Chrome and save its cookies + localStorage\nagent-browser --auto-connect state save ./my-auth.json\n```\n\n**Step 3: Reuse in automation**\n\n```bash\n# Load auth at launch\nagent-browser --state ./my-auth.json open https://app.example.com/dashboard\n\n# Or load into an existing session\nagent-browser state load ./my-auth.json\nagent-browser open https://app.example.com/dashboard\n```\n\nThis works for any site, including those with complex OAuth flows, SSO, or 2FA -- as long as Chrome already has valid session cookies.\n\n> **Security note:** State files contain session tokens in plaintext. Add them to `.gitignore`, delete when no longer needed, and set `AGENT_BROWSER_ENCRYPTION_KEY` for encryption at rest. See [Security Best Practices](#security-best-practices).\n\n**Tip:** Combine with `--session-name` so the imported auth auto-persists across restarts:\n\n```bash\nagent-browser --session-name myapp state load ./my-auth.json\n# From now on, state is auto-saved/restored for \"myapp\"\n```\n\n## Persistent Profiles\n\nUse `--profile` to point agent-browser at a Chrome user data directory. This persists everything (cookies, IndexedDB, service workers, cache) across browser restarts without explicit save/load:\n\n```bash\n# First run: login once\nagent-browser --profile ~/.myapp-profile open https://app.example.com/login\n# ... complete login flow ...\n\n# All subsequent runs: already authenticated\nagent-browser --profile ~/.myapp-profile open https://app.example.com/dashboard\n```\n\nUse different paths for different projects or test users:\n\n```bash\nagent-browser --profile ~/.profiles/admin open https://app.example.com\nagent-browser --profile ~/.profiles/viewer open https://app.example.com\n```\n\nOr set via environment variable:\n\n```bash\nexport AGENT_BROWSER_PROFILE=~/.myapp-profile\nagent-browser open https://app.example.com/dashboard\n```\n\n## Session Persistence\n\nUse `--session-name` to auto-save and restore cookies + localStorage by name, without managing files:\n\n```bash\n# Auto-saves state on close, auto-restores on next launch\nagent-browser --session-name twitter open https://twitter.com\n# ... login flow ...\nagent-browser close  # state saved to ~/.agent-browser/sessions/\n\n# Next time: state is automatically restored\nagent-browser --session-name twitter open https://twitter.com\n```\n\nEncrypt state at rest:\n\n```bash\nexport AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)\nagent-browser --session-name secure open https://app.example.com\n```\n\n## Basic Login Flow\n\n```bash\n# Navigate to login page\nagent-browser open https://app.example.com/login\nagent-browser wait --load networkidle\n\n# Get form elements\nagent-browser snapshot -i\n# Output: @e1 [input type=\"email\"], @e2 [input type=\"password\"], @e3 [button] \"Sign In\"\n\n# Fill credentials\nagent-browser fill @e1 \"user@example.com\"\nagent-browser fill @e2 \"password123\"\n\n# Submit\nagent-browser click @e3\nagent-browser wait --load networkidle\n\n# Verify login succeeded\nagent-browser get url  # Should be dashboard, not login\n```\n\n## Saving Authentication State\n\nAfter logging in, save state for reuse:\n\n```bash\n# Login first (see above)\nagent-browser open https://app.example.com/login\nagent-browser snapshot -i\nagent-browser fill @e1 \"user@example.com\"\nagent-browser fill @e2 \"password123\"\nagent-browser click @e3\nagent-browser wait --url \"**/dashboard\"\n\n# Save authenticated state\nagent-browser state save ./auth-state.json\n```\n\n## Restoring Authentication\n\nSkip login by loading saved state:\n\n```bash\n# Load saved auth state\nagent-browser state load ./auth-state.json\n\n# Navigate directly to protected page\nagent-browser open https://app.example.com/dashboard\n\n# Verify authenticated\nagent-browser snapshot -i\n```\n\n## OAuth / SSO Flows\n\nFor OAuth redirects:\n\n```bash\n# Start OAuth flow\nagent-browser open https://app.example.com/auth/google\n\n# Handle redirects automatically\nagent-browser wait --url \"**/accounts.google.com**\"\nagent-browser snapshot -i\n\n# Fill Google credentials\nagent-browser fill @e1 \"user@gmail.com\"\nagent-browser click @e2  # Next button\nagent-browser wait 2000\nagent-browser snapshot -i\nagent-browser fill @e3 \"password\"\nagent-browser click @e4  # Sign in\n\n# Wait for redirect back\nagent-browser wait --url \"**/app.example.com**\"\nagent-browser state save ./oauth-state.json\n```\n\n## Two-Factor Authentication\n\nHandle 2FA with manual intervention:\n\n```bash\n# Login with credentials\nagent-browser open https://app.example.com/login --headed  # Show browser\nagent-browser snapshot -i\nagent-browser fill @e1 \"user@example.com\"\nagent-browser fill @e2 \"password123\"\nagent-browser click @e3\n\n# Wait for user to complete 2FA manually\necho \"Complete 2FA in the browser window...\"\nagent-browser wait --url \"**/dashboard\" --timeout 120000\n\n# Save state after 2FA\nagent-browser state save ./2fa-state.json\n```\n\n## HTTP Basic Auth\n\nFor sites using HTTP Basic Authentication:\n\n```bash\n# Set credentials before navigation\nagent-browser set credentials username password\n\n# Navigate to protected resource\nagent-browser open https://protected.example.com/api\n```\n\n## Cookie-Based Auth\n\nManually set authentication cookies:\n\n```bash\n# Set auth cookie\nagent-browser cookies set session_token \"abc123xyz\"\n\n# Navigate to protected page\nagent-browser open https://app.example.com/dashboard\n```\n\n## Token Refresh Handling\n\nFor sessions with expiring tokens:\n\n```bash\n#!/bin/bash\n# Wrapper that handles token refresh\n\nSTATE_FILE=\"./auth-state.json\"\n\n# Try loading existing state\nif [[ -f \"$STATE_FILE\" ]]; then\n    agent-browser state load \"$STATE_FILE\"\n    agent-browser open https://app.example.com/dashboard\n\n    # Check if session is still valid\n    URL=$(agent-browser get url)\n    if [[ \"$URL\" == *\"/login\"* ]]; then\n        echo \"Session expired, re-authenticating...\"\n        # Perform fresh login\n        agent-browser snapshot -i\n        agent-browser fill @e1 \"$USERNAME\"\n        agent-browser fill @e2 \"$PASSWORD\"\n        agent-browser click @e3\n        agent-browser wait --url \"**/dashboard\"\n        agent-browser state save \"$STATE_FILE\"\n    fi\nelse\n    # First-time login\n    agent-browser open https://app.example.com/login\n    # ... login flow ...\nfi\n```\n\n## Security Best Practices\n\n1. **Never commit state files** - They contain session tokens\n   ```bash\n   echo \"*.auth-state.json\" >> .gitignore\n   ```\n\n2. **Use environment variables for credentials**\n   ```bash\n   agent-browser fill @e1 \"$APP_USERNAME\"\n   agent-browser fill @e2 \"$APP_PASSWORD\"\n   ```\n\n3. **Clean up after automation**\n   ```bash\n   agent-browser cookies clear\n   rm -f ./auth-state.json\n   ```\n\n4. **Use short-lived sessions for CI/CD**\n   ```bash\n   # Don't persist state in CI\n   agent-browser open https://app.example.com/login\n   # ... login and perform actions ...\n   agent-browser close  # Session ends, nothing persisted\n   ```\n"
  },
  {
    "path": "skills/agent-browser/references/commands.md",
    "content": "# Command Reference\n\nComplete reference for all agent-browser commands. For quick start and common patterns, see SKILL.md.\n\n## Navigation\n\n```bash\nagent-browser open <url>      # Navigate to URL (aliases: goto, navigate)\n                              # Supports: https://, http://, file://, about:, data://\n                              # Auto-prepends https:// if no protocol given\nagent-browser back            # Go back\nagent-browser forward         # Go forward\nagent-browser reload          # Reload page\nagent-browser close           # Close browser (aliases: quit, exit)\nagent-browser connect 9222    # Connect to browser via CDP port\n```\n\n## Snapshot (page analysis)\n\n```bash\nagent-browser snapshot            # Full accessibility tree\nagent-browser snapshot -i         # Interactive elements only (recommended)\nagent-browser snapshot -c         # Compact output\nagent-browser snapshot -d 3       # Limit depth to 3\nagent-browser snapshot -s \"#main\" # Scope to CSS selector\n```\n\n## Interactions (use @refs from snapshot)\n\n```bash\nagent-browser click @e1           # Click\nagent-browser click @e1 --new-tab # Click and open in new tab\nagent-browser dblclick @e1        # Double-click\nagent-browser focus @e1           # Focus element\nagent-browser fill @e2 \"text\"     # Clear and type\nagent-browser type @e2 \"text\"     # Type without clearing\nagent-browser press Enter         # Press key (alias: key)\nagent-browser press Control+a     # Key combination\nagent-browser keydown Shift       # Hold key down\nagent-browser keyup Shift         # Release key\nagent-browser hover @e1           # Hover\nagent-browser check @e1           # Check checkbox\nagent-browser uncheck @e1         # Uncheck checkbox\nagent-browser select @e1 \"value\"  # Select dropdown option\nagent-browser select @e1 \"a\" \"b\"  # Select multiple options\nagent-browser scroll down 500     # Scroll page (default: down 300px)\nagent-browser scrollintoview @e1  # Scroll element into view (alias: scrollinto)\nagent-browser drag @e1 @e2        # Drag and drop\nagent-browser upload @e1 file.pdf # Upload files\n```\n\n## Get Information\n\n```bash\nagent-browser get text @e1        # Get element text\nagent-browser get html @e1        # Get innerHTML\nagent-browser get value @e1       # Get input value\nagent-browser get attr @e1 href   # Get attribute\nagent-browser get title           # Get page title\nagent-browser get url             # Get current URL\nagent-browser get cdp-url         # Get CDP WebSocket URL\nagent-browser get count \".item\"   # Count matching elements\nagent-browser get box @e1         # Get bounding box\nagent-browser get styles @e1      # Get computed styles (font, color, bg, etc.)\n```\n\n## Check State\n\n```bash\nagent-browser is visible @e1      # Check if visible\nagent-browser is enabled @e1      # Check if enabled\nagent-browser is checked @e1      # Check if checked\n```\n\n## Screenshots and PDF\n\n```bash\nagent-browser screenshot          # Save to temporary directory\nagent-browser screenshot path.png # Save to specific path\nagent-browser screenshot --full   # Full page\nagent-browser pdf output.pdf      # Save as PDF\n```\n\n## Video Recording\n\n```bash\nagent-browser record start ./demo.webm    # Start recording\nagent-browser click @e1                   # Perform actions\nagent-browser record stop                 # Stop and save video\nagent-browser record restart ./take2.webm # Stop current + start new\n```\n\n## Wait\n\n```bash\nagent-browser wait @e1                     # Wait for element\nagent-browser wait 2000                    # Wait milliseconds\nagent-browser wait --text \"Success\"        # Wait for text (or -t)\nagent-browser wait --url \"**/dashboard\"    # Wait for URL pattern (or -u)\nagent-browser wait --load networkidle      # Wait for network idle (or -l)\nagent-browser wait --fn \"window.ready\"     # Wait for JS condition (or -f)\n```\n\n## Mouse Control\n\n```bash\nagent-browser mouse move 100 200      # Move mouse\nagent-browser mouse down left         # Press button\nagent-browser mouse up left           # Release button\nagent-browser mouse wheel 100         # Scroll wheel\n```\n\n## Semantic Locators (alternative to refs)\n\n```bash\nagent-browser find role button click --name \"Submit\"\nagent-browser find text \"Sign In\" click\nagent-browser find text \"Sign In\" click --exact      # Exact match only\nagent-browser find label \"Email\" fill \"user@test.com\"\nagent-browser find placeholder \"Search\" type \"query\"\nagent-browser find alt \"Logo\" click\nagent-browser find title \"Close\" click\nagent-browser find testid \"submit-btn\" click\nagent-browser find first \".item\" click\nagent-browser find last \".item\" click\nagent-browser find nth 2 \"a\" hover\n```\n\n## Browser Settings\n\n```bash\nagent-browser set viewport 1920 1080          # Set viewport size\nagent-browser set viewport 1920 1080 2        # 2x retina (same CSS size, higher res screenshots)\nagent-browser set device \"iPhone 14\"          # Emulate device\nagent-browser set geo 37.7749 -122.4194       # Set geolocation (alias: geolocation)\nagent-browser set offline on                  # Toggle offline mode\nagent-browser set headers '{\"X-Key\":\"v\"}'     # Extra HTTP headers\nagent-browser set credentials user pass       # HTTP basic auth (alias: auth)\nagent-browser set media dark                  # Emulate color scheme\nagent-browser set media light reduced-motion  # Light mode + reduced motion\n```\n\n## Cookies and Storage\n\n```bash\nagent-browser cookies                     # Get all cookies\nagent-browser cookies set name value      # Set cookie\nagent-browser cookies clear               # Clear cookies\nagent-browser storage local               # Get all localStorage\nagent-browser storage local key           # Get specific key\nagent-browser storage local set k v       # Set value\nagent-browser storage local clear         # Clear all\n```\n\n## Network\n\n```bash\nagent-browser network route <url>              # Intercept requests\nagent-browser network route <url> --abort      # Block requests\nagent-browser network route <url> --body '{}'  # Mock response\nagent-browser network unroute [url]            # Remove routes\nagent-browser network requests                 # View tracked requests\nagent-browser network requests --filter api    # Filter requests\n```\n\n## Tabs and Windows\n\n```bash\nagent-browser tab                 # List tabs\nagent-browser tab new [url]       # New tab\nagent-browser tab 2               # Switch to tab by index\nagent-browser tab close           # Close current tab\nagent-browser tab close 2         # Close tab by index\nagent-browser window new          # New window\n```\n\n## Frames\n\n```bash\nagent-browser frame \"#iframe\"     # Switch to iframe by CSS selector\nagent-browser frame @e3           # Switch to iframe by element ref\nagent-browser frame main          # Back to main frame\n```\n\n### Iframe support\n\nIframes are detected automatically during snapshots. When the main-frame snapshot runs, `Iframe` nodes are resolved and their content is inlined beneath the iframe element in the output (one level of nesting; iframes within iframes are not expanded).\n\n```bash\nagent-browser snapshot -i\n# @e3 [Iframe] \"payment-frame\"\n#   @e4 [input] \"Card number\"\n#   @e5 [button] \"Pay\"\n\n# Interact directly — refs inside iframes already work\nagent-browser fill @e4 \"4111111111111111\"\nagent-browser click @e5\n\n# Or switch frame context for scoped snapshots\nagent-browser frame @e3               # Switch using element ref\nagent-browser snapshot -i             # Snapshot scoped to that iframe\nagent-browser frame main              # Return to main frame\n```\n\nThe `frame` command accepts:\n- **Element refs** — `frame @e3` resolves the ref to an iframe element\n- **CSS selectors** — `frame \"#payment-iframe\"` finds the iframe by selector\n- **Frame name/URL** — matches against the browser's frame tree\n\n## Dialogs\n\n```bash\nagent-browser dialog accept [text]  # Accept dialog\nagent-browser dialog dismiss        # Dismiss dialog\n```\n\n## JavaScript\n\n```bash\nagent-browser eval \"document.title\"          # Simple expressions only\nagent-browser eval -b \"<base64>\"             # Any JavaScript (base64 encoded)\nagent-browser eval --stdin                   # Read script from stdin\n```\n\nUse `-b`/`--base64` or `--stdin` for reliable execution. Shell escaping with nested quotes and special characters is error-prone.\n\n```bash\n# Base64 encode your script, then:\nagent-browser eval -b \"ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ==\"\n\n# Or use stdin with heredoc for multiline scripts:\ncat <<'EOF' | agent-browser eval --stdin\nconst links = document.querySelectorAll('a');\nArray.from(links).map(a => a.href);\nEOF\n```\n\n## State Management\n\n```bash\nagent-browser state save auth.json    # Save cookies, storage, auth state\nagent-browser state load auth.json    # Restore saved state\n```\n\n## Global Options\n\n```bash\nagent-browser --session <name> ...    # Isolated browser session\nagent-browser --json ...              # JSON output for parsing\nagent-browser --headed ...            # Show browser window (not headless)\nagent-browser --full ...              # Full page screenshot (-f)\nagent-browser --cdp <port> ...        # Connect via Chrome DevTools Protocol\nagent-browser -p <provider> ...       # Cloud browser provider (--provider)\nagent-browser --proxy <url> ...       # Use proxy server\nagent-browser --proxy-bypass <hosts>  # Hosts to bypass proxy\nagent-browser --headers <json> ...    # HTTP headers scoped to URL's origin\nagent-browser --executable-path <p>   # Custom browser executable\nagent-browser --extension <path> ...  # Load browser extension (repeatable)\nagent-browser --ignore-https-errors   # Ignore SSL certificate errors\nagent-browser --help                  # Show help (-h)\nagent-browser --version               # Show version (-V)\nagent-browser <command> --help        # Show detailed help for a command\n```\n\n## Debugging\n\n```bash\nagent-browser --headed open example.com   # Show browser window\nagent-browser --cdp 9222 snapshot         # Connect via CDP port\nagent-browser connect 9222                # Alternative: connect command\nagent-browser console                     # View console messages\nagent-browser console --clear             # Clear console\nagent-browser errors                      # View page errors\nagent-browser errors --clear              # Clear errors\nagent-browser highlight @e1               # Highlight element\nagent-browser inspect                     # Open Chrome DevTools for this session\nagent-browser trace start                 # Start recording trace\nagent-browser trace stop trace.zip        # Stop and save trace\nagent-browser profiler start              # Start Chrome DevTools profiling\nagent-browser profiler stop trace.json    # Stop and save profile\n```\n\n## Environment Variables\n\n```bash\nAGENT_BROWSER_SESSION=\"mysession\"            # Default session name\nAGENT_BROWSER_EXECUTABLE_PATH=\"/path/chrome\" # Custom browser path\nAGENT_BROWSER_EXTENSIONS=\"/ext1,/ext2\"       # Comma-separated extension paths\nAGENT_BROWSER_PROVIDER=\"browserbase\"         # Cloud browser provider\nAGENT_BROWSER_STREAM_PORT=\"9223\"             # WebSocket streaming port\nAGENT_BROWSER_HOME=\"/path/to/agent-browser\"  # Custom install location\n```\n"
  },
  {
    "path": "skills/agent-browser/references/profiling.md",
    "content": "# Profiling\n\nCapture Chrome DevTools performance profiles during browser automation for performance analysis.\n\n**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.\n\n## Contents\n\n- [Basic Profiling](#basic-profiling)\n- [Profiler Commands](#profiler-commands)\n- [Categories](#categories)\n- [Use Cases](#use-cases)\n- [Output Format](#output-format)\n- [Viewing Profiles](#viewing-profiles)\n- [Limitations](#limitations)\n\n## Basic Profiling\n\n```bash\n# Start profiling\nagent-browser profiler start\n\n# Perform actions\nagent-browser navigate https://example.com\nagent-browser click \"#button\"\nagent-browser wait 1000\n\n# Stop and save\nagent-browser profiler stop ./trace.json\n```\n\n## Profiler Commands\n\n```bash\n# Start profiling with default categories\nagent-browser profiler start\n\n# Start with custom trace categories\nagent-browser profiler start --categories \"devtools.timeline,v8.execute,blink.user_timing\"\n\n# Stop profiling and save to file\nagent-browser profiler stop ./trace.json\n```\n\n## Categories\n\nThe `--categories` flag accepts a comma-separated list of Chrome trace categories. Default categories include:\n\n- `devtools.timeline` -- standard DevTools performance traces\n- `v8.execute` -- time spent running JavaScript\n- `blink` -- renderer events\n- `blink.user_timing` -- `performance.mark()` / `performance.measure()` calls\n- `latencyInfo` -- input-to-latency tracking\n- `renderer.scheduler` -- task scheduling and execution\n- `toplevel` -- broad-spectrum basic events\n\nSeveral `disabled-by-default-*` categories are also included for detailed timeline, call stack, and V8 CPU profiling data.\n\n## Use Cases\n\n### Diagnosing Slow Page Loads\n\n```bash\nagent-browser profiler start\nagent-browser navigate https://app.example.com\nagent-browser wait --load networkidle\nagent-browser profiler stop ./page-load-profile.json\n```\n\n### Profiling User Interactions\n\n```bash\nagent-browser navigate https://app.example.com\nagent-browser profiler start\nagent-browser click \"#submit\"\nagent-browser wait 2000\nagent-browser profiler stop ./interaction-profile.json\n```\n\n### CI Performance Regression Checks\n\n```bash\n#!/bin/bash\nagent-browser profiler start\nagent-browser navigate https://app.example.com\nagent-browser wait --load networkidle\nagent-browser profiler stop \"./profiles/build-${BUILD_ID}.json\"\n```\n\n## Output Format\n\nThe output is a JSON file in Chrome Trace Event format:\n\n```json\n{\n  \"traceEvents\": [\n    { \"cat\": \"devtools.timeline\", \"name\": \"RunTask\", \"ph\": \"X\", \"ts\": 12345, \"dur\": 100, ... },\n    ...\n  ],\n  \"metadata\": {\n    \"clock-domain\": \"LINUX_CLOCK_MONOTONIC\"\n  }\n}\n```\n\nThe `metadata.clock-domain` field is set based on the host platform (Linux or macOS). On Windows it is omitted.\n\n## Viewing Profiles\n\nLoad the output JSON file in any of these tools:\n\n- **Chrome DevTools**: Performance panel > Load profile (Ctrl+Shift+I > Performance)\n- **Perfetto UI**: https://ui.perfetto.dev/ -- drag and drop the JSON file\n- **Trace Viewer**: `chrome://tracing` in any Chromium browser\n\n## Limitations\n\n- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.\n- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.\n- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.\n"
  },
  {
    "path": "skills/agent-browser/references/proxy-support.md",
    "content": "# Proxy Support\n\nProxy configuration for geo-testing, rate limiting avoidance, and corporate environments.\n\n**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start.\n\n## Contents\n\n- [Basic Proxy Configuration](#basic-proxy-configuration)\n- [Authenticated Proxy](#authenticated-proxy)\n- [SOCKS Proxy](#socks-proxy)\n- [Proxy Bypass](#proxy-bypass)\n- [Common Use Cases](#common-use-cases)\n- [Verifying Proxy Connection](#verifying-proxy-connection)\n- [Troubleshooting](#troubleshooting)\n- [Best Practices](#best-practices)\n\n## Basic Proxy Configuration\n\nUse the `--proxy` flag or set proxy via environment variable:\n\n```bash\n# Via CLI flag\nagent-browser --proxy \"http://proxy.example.com:8080\" open https://example.com\n\n# Via environment variable\nexport HTTP_PROXY=\"http://proxy.example.com:8080\"\nagent-browser open https://example.com\n\n# HTTPS proxy\nexport HTTPS_PROXY=\"https://proxy.example.com:8080\"\nagent-browser open https://example.com\n\n# Both\nexport HTTP_PROXY=\"http://proxy.example.com:8080\"\nexport HTTPS_PROXY=\"http://proxy.example.com:8080\"\nagent-browser open https://example.com\n```\n\n## Authenticated Proxy\n\nFor proxies requiring authentication:\n\n```bash\n# Include credentials in URL\nexport HTTP_PROXY=\"http://username:password@proxy.example.com:8080\"\nagent-browser open https://example.com\n```\n\n## SOCKS Proxy\n\n```bash\n# SOCKS5 proxy\nexport ALL_PROXY=\"socks5://proxy.example.com:1080\"\nagent-browser open https://example.com\n\n# SOCKS5 with auth\nexport ALL_PROXY=\"socks5://user:pass@proxy.example.com:1080\"\nagent-browser open https://example.com\n```\n\n## Proxy Bypass\n\nSkip proxy for specific domains using `--proxy-bypass` or `NO_PROXY`:\n\n```bash\n# Via CLI flag\nagent-browser --proxy \"http://proxy.example.com:8080\" --proxy-bypass \"localhost,*.internal.com\" open https://example.com\n\n# Via environment variable\nexport NO_PROXY=\"localhost,127.0.0.1,.internal.company.com\"\nagent-browser open https://internal.company.com  # Direct connection\nagent-browser open https://external.com          # Via proxy\n```\n\n## Common Use Cases\n\n### Geo-Location Testing\n\n```bash\n#!/bin/bash\n# Test site from different regions using geo-located proxies\n\nPROXIES=(\n    \"http://us-proxy.example.com:8080\"\n    \"http://eu-proxy.example.com:8080\"\n    \"http://asia-proxy.example.com:8080\"\n)\n\nfor proxy in \"${PROXIES[@]}\"; do\n    export HTTP_PROXY=\"$proxy\"\n    export HTTPS_PROXY=\"$proxy\"\n\n    region=$(echo \"$proxy\" | grep -oP '^\\w+-\\w+')\n    echo \"Testing from: $region\"\n\n    agent-browser --session \"$region\" open https://example.com\n    agent-browser --session \"$region\" screenshot \"./screenshots/$region.png\"\n    agent-browser --session \"$region\" close\ndone\n```\n\n### Rotating Proxies for Scraping\n\n```bash\n#!/bin/bash\n# Rotate through proxy list to avoid rate limiting\n\nPROXY_LIST=(\n    \"http://proxy1.example.com:8080\"\n    \"http://proxy2.example.com:8080\"\n    \"http://proxy3.example.com:8080\"\n)\n\nURLS=(\n    \"https://site.com/page1\"\n    \"https://site.com/page2\"\n    \"https://site.com/page3\"\n)\n\nfor i in \"${!URLS[@]}\"; do\n    proxy_index=$((i % ${#PROXY_LIST[@]}))\n    export HTTP_PROXY=\"${PROXY_LIST[$proxy_index]}\"\n    export HTTPS_PROXY=\"${PROXY_LIST[$proxy_index]}\"\n\n    agent-browser open \"${URLS[$i]}\"\n    agent-browser get text body > \"output-$i.txt\"\n    agent-browser close\n\n    sleep 1  # Polite delay\ndone\n```\n\n### Corporate Network Access\n\n```bash\n#!/bin/bash\n# Access internal sites via corporate proxy\n\nexport HTTP_PROXY=\"http://corpproxy.company.com:8080\"\nexport HTTPS_PROXY=\"http://corpproxy.company.com:8080\"\nexport NO_PROXY=\"localhost,127.0.0.1,.company.com\"\n\n# External sites go through proxy\nagent-browser open https://external-vendor.com\n\n# Internal sites bypass proxy\nagent-browser open https://intranet.company.com\n```\n\n## Verifying Proxy Connection\n\n```bash\n# Check your apparent IP\nagent-browser open https://httpbin.org/ip\nagent-browser get text body\n# Should show proxy's IP, not your real IP\n```\n\n## Troubleshooting\n\n### Proxy Connection Failed\n\n```bash\n# Test proxy connectivity first\ncurl -x http://proxy.example.com:8080 https://httpbin.org/ip\n\n# Check if proxy requires auth\nexport HTTP_PROXY=\"http://user:pass@proxy.example.com:8080\"\n```\n\n### SSL/TLS Errors Through Proxy\n\nSome proxies perform SSL inspection. If you encounter certificate errors:\n\n```bash\n# For testing only - not recommended for production\nagent-browser open https://example.com --ignore-https-errors\n```\n\n### Slow Performance\n\n```bash\n# Use proxy only when necessary\nexport NO_PROXY=\"*.cdn.com,*.static.com\"  # Direct CDN access\n```\n\n## Best Practices\n\n1. **Use environment variables** - Don't hardcode proxy credentials\n2. **Set NO_PROXY appropriately** - Avoid routing local traffic through proxy\n3. **Test proxy before automation** - Verify connectivity with simple requests\n4. **Handle proxy failures gracefully** - Implement retry logic for unstable proxies\n5. **Rotate proxies for large scraping jobs** - Distribute load and avoid bans\n"
  },
  {
    "path": "skills/agent-browser/references/session-management.md",
    "content": "# Session Management\n\nMultiple isolated browser sessions with state persistence and concurrent browsing.\n\n**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start.\n\n## Contents\n\n- [Named Sessions](#named-sessions)\n- [Session Isolation Properties](#session-isolation-properties)\n- [Session State Persistence](#session-state-persistence)\n- [Common Patterns](#common-patterns)\n- [Default Session](#default-session)\n- [Session Cleanup](#session-cleanup)\n- [Best Practices](#best-practices)\n\n## Named Sessions\n\nUse `--session` flag to isolate browser contexts:\n\n```bash\n# Session 1: Authentication flow\nagent-browser --session auth open https://app.example.com/login\n\n# Session 2: Public browsing (separate cookies, storage)\nagent-browser --session public open https://example.com\n\n# Commands are isolated by session\nagent-browser --session auth fill @e1 \"user@example.com\"\nagent-browser --session public get text body\n```\n\n## Session Isolation Properties\n\nEach session has independent:\n- Cookies\n- LocalStorage / SessionStorage\n- IndexedDB\n- Cache\n- Browsing history\n- Open tabs\n\n## Session State Persistence\n\n### Save Session State\n\n```bash\n# Save cookies, storage, and auth state\nagent-browser state save /path/to/auth-state.json\n```\n\n### Load Session State\n\n```bash\n# Restore saved state\nagent-browser state load /path/to/auth-state.json\n\n# Continue with authenticated session\nagent-browser open https://app.example.com/dashboard\n```\n\n### State File Contents\n\n```json\n{\n  \"cookies\": [...],\n  \"localStorage\": {...},\n  \"sessionStorage\": {...},\n  \"origins\": [...]\n}\n```\n\n## Common Patterns\n\n### Authenticated Session Reuse\n\n```bash\n#!/bin/bash\n# Save login state once, reuse many times\n\nSTATE_FILE=\"/tmp/auth-state.json\"\n\n# Check if we have saved state\nif [[ -f \"$STATE_FILE\" ]]; then\n    agent-browser state load \"$STATE_FILE\"\n    agent-browser open https://app.example.com/dashboard\nelse\n    # Perform login\n    agent-browser open https://app.example.com/login\n    agent-browser snapshot -i\n    agent-browser fill @e1 \"$USERNAME\"\n    agent-browser fill @e2 \"$PASSWORD\"\n    agent-browser click @e3\n    agent-browser wait --load networkidle\n\n    # Save for future use\n    agent-browser state save \"$STATE_FILE\"\nfi\n```\n\n### Concurrent Scraping\n\n```bash\n#!/bin/bash\n# Scrape multiple sites concurrently\n\n# Start all sessions\nagent-browser --session site1 open https://site1.com &\nagent-browser --session site2 open https://site2.com &\nagent-browser --session site3 open https://site3.com &\nwait\n\n# Extract from each\nagent-browser --session site1 get text body > site1.txt\nagent-browser --session site2 get text body > site2.txt\nagent-browser --session site3 get text body > site3.txt\n\n# Cleanup\nagent-browser --session site1 close\nagent-browser --session site2 close\nagent-browser --session site3 close\n```\n\n### A/B Testing Sessions\n\n```bash\n# Test different user experiences\nagent-browser --session variant-a open \"https://app.com?variant=a\"\nagent-browser --session variant-b open \"https://app.com?variant=b\"\n\n# Compare\nagent-browser --session variant-a screenshot /tmp/variant-a.png\nagent-browser --session variant-b screenshot /tmp/variant-b.png\n```\n\n## Default Session\n\nWhen `--session` is omitted, commands use the default session:\n\n```bash\n# These use the same default session\nagent-browser open https://example.com\nagent-browser snapshot -i\nagent-browser close  # Closes default session\n```\n\n## Session Cleanup\n\n```bash\n# Close specific session\nagent-browser --session auth close\n\n# List active sessions\nagent-browser session list\n```\n\n## Best Practices\n\n### 1. Name Sessions Semantically\n\n```bash\n# GOOD: Clear purpose\nagent-browser --session github-auth open https://github.com\nagent-browser --session docs-scrape open https://docs.example.com\n\n# AVOID: Generic names\nagent-browser --session s1 open https://github.com\n```\n\n### 2. Always Clean Up\n\n```bash\n# Close sessions when done\nagent-browser --session auth close\nagent-browser --session scrape close\n```\n\n### 3. Handle State Files Securely\n\n```bash\n# Don't commit state files (contain auth tokens!)\necho \"*.auth-state.json\" >> .gitignore\n\n# Delete after use\nrm /tmp/auth-state.json\n```\n\n### 4. Timeout Long Sessions\n\n```bash\n# Set timeout for automated scripts\ntimeout 60 agent-browser --session long-task get text body\n```\n"
  },
  {
    "path": "skills/agent-browser/references/snapshot-refs.md",
    "content": "# Snapshot and Refs\n\nCompact element references that reduce context usage dramatically for AI agents.\n\n**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.\n\n## Contents\n\n- [How Refs Work](#how-refs-work)\n- [Snapshot Command](#the-snapshot-command)\n- [Using Refs](#using-refs)\n- [Ref Lifecycle](#ref-lifecycle)\n- [Best Practices](#best-practices)\n- [Ref Notation Details](#ref-notation-details)\n- [Troubleshooting](#troubleshooting)\n\n## How Refs Work\n\nTraditional approach:\n```\nFull DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens)\n```\n\nagent-browser approach:\n```\nCompact snapshot → @refs assigned → Direct interaction (~200-400 tokens)\n```\n\n## The Snapshot Command\n\n```bash\n# Basic snapshot (shows page structure)\nagent-browser snapshot\n\n# Interactive snapshot (-i flag) - RECOMMENDED\nagent-browser snapshot -i\n```\n\n### Snapshot Output Format\n\n```\nPage: Example Site - Home\nURL: https://example.com\n\n@e1 [header]\n  @e2 [nav]\n    @e3 [a] \"Home\"\n    @e4 [a] \"Products\"\n    @e5 [a] \"About\"\n  @e6 [button] \"Sign In\"\n\n@e7 [main]\n  @e8 [h1] \"Welcome\"\n  @e9 [form]\n    @e10 [input type=\"email\"] placeholder=\"Email\"\n    @e11 [input type=\"password\"] placeholder=\"Password\"\n    @e12 [button type=\"submit\"] \"Log In\"\n\n@e13 [footer]\n  @e14 [a] \"Privacy Policy\"\n```\n\n## Using Refs\n\nOnce you have refs, interact directly:\n\n```bash\n# Click the \"Sign In\" button\nagent-browser click @e6\n\n# Fill email input\nagent-browser fill @e10 \"user@example.com\"\n\n# Fill password\nagent-browser fill @e11 \"password123\"\n\n# Submit the form\nagent-browser click @e12\n```\n\n## Ref Lifecycle\n\n**IMPORTANT**: Refs are invalidated when the page changes!\n\n```bash\n# Get initial snapshot\nagent-browser snapshot -i\n# @e1 [button] \"Next\"\n\n# Click triggers page change\nagent-browser click @e1\n\n# MUST re-snapshot to get new refs!\nagent-browser snapshot -i\n# @e1 [h1] \"Page 2\"  ← Different element now!\n```\n\n## Best Practices\n\n### 1. Always Snapshot Before Interacting\n\n```bash\n# CORRECT\nagent-browser open https://example.com\nagent-browser snapshot -i          # Get refs first\nagent-browser click @e1            # Use ref\n\n# WRONG\nagent-browser open https://example.com\nagent-browser click @e1            # Ref doesn't exist yet!\n```\n\n### 2. Re-Snapshot After Navigation\n\n```bash\nagent-browser click @e5            # Navigates to new page\nagent-browser snapshot -i          # Get new refs\nagent-browser click @e1            # Use new refs\n```\n\n### 3. Re-Snapshot After Dynamic Changes\n\n```bash\nagent-browser click @e1            # Opens dropdown\nagent-browser snapshot -i          # See dropdown items\nagent-browser click @e7            # Select item\n```\n\n### 4. Snapshot Specific Regions\n\nFor complex pages, snapshot specific areas:\n\n```bash\n# Snapshot just the form\nagent-browser snapshot @e9\n```\n\n## Ref Notation Details\n\n```\n@e1 [tag type=\"value\"] \"text content\" placeholder=\"hint\"\n│    │   │             │               │\n│    │   │             │               └─ Additional attributes\n│    │   │             └─ Visible text\n│    │   └─ Key attributes shown\n│    └─ HTML tag name\n└─ Unique ref ID\n```\n\n### Common Patterns\n\n```\n@e1 [button] \"Submit\"                    # Button with text\n@e2 [input type=\"email\"]                 # Email input\n@e3 [input type=\"password\"]              # Password input\n@e4 [a href=\"/page\"] \"Link Text\"         # Anchor link\n@e5 [select]                             # Dropdown\n@e6 [textarea] placeholder=\"Message\"     # Text area\n@e7 [div class=\"modal\"]                  # Container (when relevant)\n@e8 [img alt=\"Logo\"]                     # Image\n@e9 [checkbox] checked                   # Checked checkbox\n@e10 [radio] selected                    # Selected radio\n```\n\n## Iframes\n\nSnapshots automatically detect and inline iframe content. When the main-frame snapshot runs, each `Iframe` node is resolved and its child accessibility tree is included directly beneath it in the output. Refs assigned to elements inside iframes carry frame context, so interactions like `click`, `fill`, and `type` work without manually switching frames.\n\n```bash\nagent-browser snapshot -i\n# @e1 [heading] \"Checkout\"\n# @e2 [Iframe] \"payment-frame\"\n#   @e3 [input] \"Card number\"\n#   @e4 [input] \"Expiry\"\n#   @e5 [button] \"Pay\"\n# @e6 [button] \"Cancel\"\n\n# Interact with iframe elements directly using their refs\nagent-browser fill @e3 \"4111111111111111\"\nagent-browser fill @e4 \"12/28\"\nagent-browser click @e5\n```\n\n**Key details:**\n- Only one level of iframe nesting is expanded (iframes within iframes are not recursed)\n- Cross-origin iframes that block accessibility tree access are silently skipped\n- Empty iframes or iframes with no interactive content are omitted from the output\n- To scope a snapshot to a single iframe, use `frame @ref` then `snapshot -i`\n\n## Troubleshooting\n\n### \"Ref not found\" Error\n\n```bash\n# Ref may have changed - re-snapshot\nagent-browser snapshot -i\n```\n\n### Element Not Visible in Snapshot\n\n```bash\n# Scroll down to reveal element\nagent-browser scroll down 1000\nagent-browser snapshot -i\n\n# Or wait for dynamic content\nagent-browser wait 1000\nagent-browser snapshot -i\n```\n\n### Too Many Elements\n\n```bash\n# Snapshot specific container\nagent-browser snapshot @e5\n\n# Or use get text for content-only extraction\nagent-browser get text @e5\n```\n"
  },
  {
    "path": "skills/agent-browser/references/video-recording.md",
    "content": "# Video Recording\n\nCapture browser automation as video for debugging, documentation, or verification.\n\n**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.\n\n## Contents\n\n- [Basic Recording](#basic-recording)\n- [Recording Commands](#recording-commands)\n- [Use Cases](#use-cases)\n- [Best Practices](#best-practices)\n- [Output Format](#output-format)\n- [Limitations](#limitations)\n\n## Basic Recording\n\n```bash\n# Start recording\nagent-browser record start ./demo.webm\n\n# Perform actions\nagent-browser open https://example.com\nagent-browser snapshot -i\nagent-browser click @e1\nagent-browser fill @e2 \"test input\"\n\n# Stop and save\nagent-browser record stop\n```\n\n## Recording Commands\n\n```bash\n# Start recording to file\nagent-browser record start ./output.webm\n\n# Stop current recording\nagent-browser record stop\n\n# Restart with new file (stops current + starts new)\nagent-browser record restart ./take2.webm\n```\n\n## Use Cases\n\n### Debugging Failed Automation\n\n```bash\n#!/bin/bash\n# Record automation for debugging\n\nagent-browser record start ./debug-$(date +%Y%m%d-%H%M%S).webm\n\n# Run your automation\nagent-browser open https://app.example.com\nagent-browser snapshot -i\nagent-browser click @e1 || {\n    echo \"Click failed - check recording\"\n    agent-browser record stop\n    exit 1\n}\n\nagent-browser record stop\n```\n\n### Documentation Generation\n\n```bash\n#!/bin/bash\n# Record workflow for documentation\n\nagent-browser record start ./docs/how-to-login.webm\n\nagent-browser open https://app.example.com/login\nagent-browser wait 1000  # Pause for visibility\n\nagent-browser snapshot -i\nagent-browser fill @e1 \"demo@example.com\"\nagent-browser wait 500\n\nagent-browser fill @e2 \"password\"\nagent-browser wait 500\n\nagent-browser click @e3\nagent-browser wait --load networkidle\nagent-browser wait 1000  # Show result\n\nagent-browser record stop\n```\n\n### CI/CD Test Evidence\n\n```bash\n#!/bin/bash\n# Record E2E test runs for CI artifacts\n\nTEST_NAME=\"${1:-e2e-test}\"\nRECORDING_DIR=\"./test-recordings\"\nmkdir -p \"$RECORDING_DIR\"\n\nagent-browser record start \"$RECORDING_DIR/$TEST_NAME-$(date +%s).webm\"\n\n# Run test\nif run_e2e_test; then\n    echo \"Test passed\"\nelse\n    echo \"Test failed - recording saved\"\nfi\n\nagent-browser record stop\n```\n\n## Best Practices\n\n### 1. Add Pauses for Clarity\n\n```bash\n# Slow down for human viewing\nagent-browser click @e1\nagent-browser wait 500  # Let viewer see result\n```\n\n### 2. Use Descriptive Filenames\n\n```bash\n# Include context in filename\nagent-browser record start ./recordings/login-flow-2024-01-15.webm\nagent-browser record start ./recordings/checkout-test-run-42.webm\n```\n\n### 3. Handle Recording in Error Cases\n\n```bash\n#!/bin/bash\nset -e\n\ncleanup() {\n    agent-browser record stop 2>/dev/null || true\n    agent-browser close 2>/dev/null || true\n}\ntrap cleanup EXIT\n\nagent-browser record start ./automation.webm\n# ... automation steps ...\n```\n\n### 4. Combine with Screenshots\n\n```bash\n# Record video AND capture key frames\nagent-browser record start ./flow.webm\n\nagent-browser open https://example.com\nagent-browser screenshot ./screenshots/step1-homepage.png\n\nagent-browser click @e1\nagent-browser screenshot ./screenshots/step2-after-click.png\n\nagent-browser record stop\n```\n\n## Output Format\n\n- Default format: WebM (VP8/VP9 codec)\n- Compatible with all modern browsers and video players\n- Compressed but high quality\n\n## Limitations\n\n- Recording adds slight overhead to automation\n- Large recordings can consume significant disk space\n- Some headless environments may have codec limitations\n"
  },
  {
    "path": "skills/agent-browser/templates/authenticated-session.sh",
    "content": "#!/bin/bash\n# Template: Authenticated Session Workflow\n# Purpose: Login once, save state, reuse for subsequent runs\n# Usage: ./authenticated-session.sh <login-url> [state-file]\n#\n# RECOMMENDED: Use the auth vault instead of this template:\n#   echo \"<pass>\" | agent-browser auth save myapp --url <login-url> --username <user> --password-stdin\n#   agent-browser auth login myapp\n# The auth vault stores credentials securely and the LLM never sees passwords.\n#\n# Environment variables:\n#   APP_USERNAME - Login username/email\n#   APP_PASSWORD - Login password\n#\n# Two modes:\n#   1. Discovery mode (default): Shows form structure so you can identify refs\n#   2. Login mode: Performs actual login after you update the refs\n#\n# Setup steps:\n#   1. Run once to see form structure (discovery mode)\n#   2. Update refs in LOGIN FLOW section below\n#   3. Set APP_USERNAME and APP_PASSWORD\n#   4. Delete the DISCOVERY section\n\nset -euo pipefail\n\nLOGIN_URL=\"${1:?Usage: $0 <login-url> [state-file]}\"\nSTATE_FILE=\"${2:-./auth-state.json}\"\n\necho \"Authentication workflow: $LOGIN_URL\"\n\n# ================================================================\n# SAVED STATE: Skip login if valid saved state exists\n# ================================================================\nif [[ -f \"$STATE_FILE\" ]]; then\n    echo \"Loading saved state from $STATE_FILE...\"\n    if agent-browser --state \"$STATE_FILE\" open \"$LOGIN_URL\" 2>/dev/null; then\n        agent-browser wait --load networkidle\n\n        CURRENT_URL=$(agent-browser get url)\n        if [[ \"$CURRENT_URL\" != *\"login\"* ]] && [[ \"$CURRENT_URL\" != *\"signin\"* ]]; then\n            echo \"Session restored successfully\"\n            agent-browser snapshot -i\n            exit 0\n        fi\n        echo \"Session expired, performing fresh login...\"\n        agent-browser close 2>/dev/null || true\n    else\n        echo \"Failed to load state, re-authenticating...\"\n    fi\n    rm -f \"$STATE_FILE\"\nfi\n\n# ================================================================\n# DISCOVERY MODE: Shows form structure (delete after setup)\n# ================================================================\necho \"Opening login page...\"\nagent-browser open \"$LOGIN_URL\"\nagent-browser wait --load networkidle\n\necho \"\"\necho \"Login form structure:\"\necho \"---\"\nagent-browser snapshot -i\necho \"---\"\necho \"\"\necho \"Next steps:\"\necho \"  1. Note the refs: username=@e?, password=@e?, submit=@e?\"\necho \"  2. Update the LOGIN FLOW section below with your refs\"\necho \"  3. Set: export APP_USERNAME='...' APP_PASSWORD='...'\"\necho \"  4. Delete this DISCOVERY MODE section\"\necho \"\"\nagent-browser close\nexit 0\n\n# ================================================================\n# LOGIN FLOW: Uncomment and customize after discovery\n# ================================================================\n# : \"${APP_USERNAME:?Set APP_USERNAME environment variable}\"\n# : \"${APP_PASSWORD:?Set APP_PASSWORD environment variable}\"\n#\n# agent-browser open \"$LOGIN_URL\"\n# agent-browser wait --load networkidle\n# agent-browser snapshot -i\n#\n# # Fill credentials (update refs to match your form)\n# agent-browser fill @e1 \"$APP_USERNAME\"\n# agent-browser fill @e2 \"$APP_PASSWORD\"\n# agent-browser click @e3\n# agent-browser wait --load networkidle\n#\n# # Verify login succeeded\n# FINAL_URL=$(agent-browser get url)\n# if [[ \"$FINAL_URL\" == *\"login\"* ]] || [[ \"$FINAL_URL\" == *\"signin\"* ]]; then\n#     echo \"Login failed - still on login page\"\n#     agent-browser screenshot /tmp/login-failed.png\n#     agent-browser close\n#     exit 1\n# fi\n#\n# # Save state for future runs\n# echo \"Saving state to $STATE_FILE\"\n# agent-browser state save \"$STATE_FILE\"\n# echo \"Login successful\"\n# agent-browser snapshot -i\n"
  },
  {
    "path": "skills/agent-browser/templates/capture-workflow.sh",
    "content": "#!/bin/bash\n# Template: Content Capture Workflow\n# Purpose: Extract content from web pages (text, screenshots, PDF)\n# Usage: ./capture-workflow.sh <url> [output-dir]\n#\n# Outputs:\n#   - page-full.png: Full page screenshot\n#   - page-structure.txt: Page element structure with refs\n#   - page-text.txt: All text content\n#   - page.pdf: PDF version\n#\n# Optional: Load auth state for protected pages\n\nset -euo pipefail\n\nTARGET_URL=\"${1:?Usage: $0 <url> [output-dir]}\"\nOUTPUT_DIR=\"${2:-.}\"\n\necho \"Capturing: $TARGET_URL\"\nmkdir -p \"$OUTPUT_DIR\"\n\n# Optional: Load authentication state\n# if [[ -f \"./auth-state.json\" ]]; then\n#     echo \"Loading authentication state...\"\n#     agent-browser state load \"./auth-state.json\"\n# fi\n\n# Navigate to target\nagent-browser open \"$TARGET_URL\"\nagent-browser wait --load networkidle\n\n# Get metadata\nTITLE=$(agent-browser get title)\nURL=$(agent-browser get url)\necho \"Title: $TITLE\"\necho \"URL: $URL\"\n\n# Capture full page screenshot\nagent-browser screenshot --full \"$OUTPUT_DIR/page-full.png\"\necho \"Saved: $OUTPUT_DIR/page-full.png\"\n\n# Get page structure with refs\nagent-browser snapshot -i > \"$OUTPUT_DIR/page-structure.txt\"\necho \"Saved: $OUTPUT_DIR/page-structure.txt\"\n\n# Extract all text content\nagent-browser get text body > \"$OUTPUT_DIR/page-text.txt\"\necho \"Saved: $OUTPUT_DIR/page-text.txt\"\n\n# Save as PDF\nagent-browser pdf \"$OUTPUT_DIR/page.pdf\"\necho \"Saved: $OUTPUT_DIR/page.pdf\"\n\n# Optional: Extract specific elements using refs from structure\n# agent-browser get text @e5 > \"$OUTPUT_DIR/main-content.txt\"\n\n# Optional: Handle infinite scroll pages\n# for i in {1..5}; do\n#     agent-browser scroll down 1000\n#     agent-browser wait 1000\n# done\n# agent-browser screenshot --full \"$OUTPUT_DIR/page-scrolled.png\"\n\n# Cleanup\nagent-browser close\n\necho \"\"\necho \"Capture complete:\"\nls -la \"$OUTPUT_DIR\"\n"
  },
  {
    "path": "skills/agent-browser/templates/form-automation.sh",
    "content": "#!/bin/bash\n# Template: Form Automation Workflow\n# Purpose: Fill and submit web forms with validation\n# Usage: ./form-automation.sh <form-url>\n#\n# This template demonstrates the snapshot-interact-verify pattern:\n# 1. Navigate to form\n# 2. Snapshot to get element refs\n# 3. Fill fields using refs\n# 4. Submit and verify result\n#\n# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output\n\nset -euo pipefail\n\nFORM_URL=\"${1:?Usage: $0 <form-url>}\"\n\necho \"Form automation: $FORM_URL\"\n\n# Step 1: Navigate to form\nagent-browser open \"$FORM_URL\"\nagent-browser wait --load networkidle\n\n# Step 2: Snapshot to discover form elements\necho \"\"\necho \"Form structure:\"\nagent-browser snapshot -i\n\n# Step 3: Fill form fields (customize these refs based on snapshot output)\n#\n# Common field types:\n#   agent-browser fill @e1 \"John Doe\"           # Text input\n#   agent-browser fill @e2 \"user@example.com\"   # Email input\n#   agent-browser fill @e3 \"SecureP@ss123\"      # Password input\n#   agent-browser select @e4 \"Option Value\"     # Dropdown\n#   agent-browser check @e5                     # Checkbox\n#   agent-browser click @e6                     # Radio button\n#   agent-browser fill @e7 \"Multi-line text\"   # Textarea\n#   agent-browser upload @e8 /path/to/file.pdf # File upload\n#\n# Uncomment and modify:\n# agent-browser fill @e1 \"Test User\"\n# agent-browser fill @e2 \"test@example.com\"\n# agent-browser click @e3  # Submit button\n\n# Step 4: Wait for submission\n# agent-browser wait --load networkidle\n# agent-browser wait --url \"**/success\"  # Or wait for redirect\n\n# Step 5: Verify result\necho \"\"\necho \"Result:\"\nagent-browser get url\nagent-browser snapshot -i\n\n# Optional: Capture evidence\nagent-browser screenshot /tmp/form-result.png\necho \"Screenshot saved: /tmp/form-result.png\"\n\n# Cleanup\nagent-browser close\necho \"Done\"\n"
  },
  {
    "path": "skills/dogfood/SKILL.md",
    "content": "---\nname: dogfood\ndescription: Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to \"dogfood\", \"QA\", \"exploratory test\", \"find issues\", \"bug hunt\", \"test this app/site/platform\", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.\nallowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)\n---\n\n# Dogfood\n\nSystematically explore a web application, find issues, and produce a report with full reproduction evidence for every finding.\n\n## Setup\n\nOnly the **Target URL** is required. Everything else has sensible defaults -- use them unless the user explicitly provides an override.\n\n| Parameter | Default | Example override |\n|-----------|---------|-----------------|\n| **Target URL** | _(required)_ | `vercel.com`, `http://localhost:3000` |\n| **Session name** | Slugified domain (e.g., `vercel.com` -> `vercel-com`) | `--session my-session` |\n| **Output directory** | `./dogfood-output/` | `Output directory: /tmp/qa` |\n| **Scope** | Full app | `Focus on the billing page` |\n| **Authentication** | None | `Sign in to user@example.com` |\n\nIf the user says something like \"dogfood vercel.com\", start immediately with defaults. Do not ask clarifying questions unless authentication is mentioned but credentials are missing.\n\nAlways use `agent-browser` directly -- never `npx agent-browser`. The direct binary uses the fast Rust client. `npx` routes through Node.js and is significantly slower.\n\n## Workflow\n\n```\n1. Initialize    Set up session, output dirs, report file\n2. Authenticate  Sign in if needed, save state\n3. Orient        Navigate to starting point, take initial snapshot\n4. Explore       Systematically visit pages and test features\n5. Document      Screenshot + record each issue as found\n6. Wrap up       Update summary counts, close session\n```\n\n### 1. Initialize\n\n```bash\nmkdir -p {OUTPUT_DIR}/screenshots {OUTPUT_DIR}/videos\n```\n\nCopy the report template into the output directory and fill in the header fields:\n\n```bash\ncp {SKILL_DIR}/templates/dogfood-report-template.md {OUTPUT_DIR}/report.md\n```\n\nStart a named session:\n\n```bash\nagent-browser --session {SESSION} open {TARGET_URL}\nagent-browser --session {SESSION} wait --load networkidle\n```\n\n### 2. Authenticate\n\nIf the app requires login:\n\n```bash\nagent-browser --session {SESSION} snapshot -i\n# Identify login form refs, fill credentials\nagent-browser --session {SESSION} fill @e1 \"{EMAIL}\"\nagent-browser --session {SESSION} fill @e2 \"{PASSWORD}\"\nagent-browser --session {SESSION} click @e3\nagent-browser --session {SESSION} wait --load networkidle\n```\n\nFor OTP/email codes: ask the user, wait for their response, then enter the code.\n\nAfter successful login, save state for potential reuse:\n\n```bash\nagent-browser --session {SESSION} state save {OUTPUT_DIR}/auth-state.json\n```\n\n### 3. Orient\n\nTake an initial annotated screenshot and snapshot to understand the app structure:\n\n```bash\nagent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/initial.png\nagent-browser --session {SESSION} snapshot -i\n```\n\nIdentify the main navigation elements and map out the sections to visit.\n\n### 4. Explore\n\nRead [references/issue-taxonomy.md](references/issue-taxonomy.md) for the full list of what to look for and the exploration checklist.\n\n**Strategy -- work through the app systematically:**\n\n- Start from the main navigation. Visit each top-level section.\n- Within each section, test interactive elements: click buttons, fill forms, open dropdowns/modals.\n- Check edge cases: empty states, error handling, boundary inputs.\n- Try realistic end-to-end workflows (create, edit, delete flows).\n- Check the browser console for errors periodically.\n\n**At each page:**\n\n```bash\nagent-browser --session {SESSION} snapshot -i\nagent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/{page-name}.png\nagent-browser --session {SESSION} errors\nagent-browser --session {SESSION} console\n```\n\nUse your judgment on how deep to go. Spend more time on core features and less on peripheral pages. If you find a cluster of issues in one area, investigate deeper.\n\n### 5. Document Issues (Repro-First)\n\nSteps 4 and 5 happen together -- explore and document in a single pass. When you find an issue, stop exploring and document it immediately before moving on. Do not explore the whole app first and document later.\n\nEvery issue must be reproducible. When you find something wrong, do not just note it -- prove it with evidence. The goal is that someone reading the report can see exactly what happened and replay it.\n\n**Choose the right level of evidence for the issue:**\n\n#### Interactive / behavioral issues (functional, ux, console errors on action)\n\nThese require user interaction to reproduce -- use full repro with video and step-by-step screenshots:\n\n1. **Start a repro video** _before_ reproducing:\n\n```bash\nagent-browser --session {SESSION} record start {OUTPUT_DIR}/videos/issue-{NNN}-repro.webm\n```\n\n2. **Walk through the steps at human pace.** Pause 1-2 seconds between actions so the video is watchable. Take a screenshot at each step:\n\n```bash\nagent-browser --session {SESSION} screenshot {OUTPUT_DIR}/screenshots/issue-{NNN}-step-1.png\nsleep 1\n# Perform action (click, fill, etc.)\nsleep 1\nagent-browser --session {SESSION} screenshot {OUTPUT_DIR}/screenshots/issue-{NNN}-step-2.png\nsleep 1\n# ...continue until the issue manifests\n```\n\n3. **Capture the broken state.** Pause so the viewer can see it, then take an annotated screenshot:\n\n```bash\nsleep 2\nagent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/issue-{NNN}-result.png\n```\n\n4. **Stop the video:**\n\n```bash\nagent-browser --session {SESSION} record stop\n```\n\n5. Write numbered repro steps in the report, each referencing its screenshot.\n\n#### Static / visible-on-load issues (typos, placeholder text, clipped text, misalignment, console errors on load)\n\nThese are visible without interaction -- a single annotated screenshot is sufficient. No video, no multi-step repro:\n\n```bash\nagent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/issue-{NNN}.png\n```\n\nWrite a brief description and reference the screenshot in the report. Set **Repro Video** to `N/A`.\n\n---\n\n**For all issues:**\n\n1. **Append to the report immediately.** Do not batch issues for later. Write each one as you find it so nothing is lost if the session is interrupted.\n\n2. **Increment the issue counter** (ISSUE-001, ISSUE-002, ...).\n\n### 6. Wrap Up\n\nAim to find **5-10 well-documented issues**, then wrap up. Depth of evidence matters more than total count -- 5 issues with full repro beats 20 with vague descriptions.\n\nAfter exploring:\n\n1. Re-read the report and update the summary severity counts so they match the actual issues. Every `### ISSUE-` block must be reflected in the totals.\n2. Close the session:\n\n```bash\nagent-browser --session {SESSION} close\n```\n\n3. Tell the user the report is ready and summarize findings: total issues, breakdown by severity, and the most critical items.\n\n## Guidance\n\n- **Repro is everything.** Every issue needs proof -- but match the evidence to the issue. Interactive bugs need video and step-by-step screenshots. Static bugs (typos, placeholder text, visual glitches visible on load) only need a single annotated screenshot.\n- **Verify reproducibility before collecting evidence.** Before recording video or taking screenshots, verify the issue is reproducible with at least one retry. If it can't be reproduced consistently, it's not a valid issue.\n- **Don't record video for static issues.** A typo or clipped text doesn't benefit from a video. Save video for issues that involve user interaction, timing, or state changes.\n- **For interactive issues, screenshot each step.** Capture the before, the action, and the after -- so someone can see the full sequence.\n- **Write repro steps that map to screenshots.** Each numbered step in the report should reference its corresponding screenshot. A reader should be able to follow the steps visually without touching a browser.\n- **Use the right snapshot command.**\n  - `snapshot -i` — for finding clickable/fillable elements (buttons, inputs, links)\n  - `snapshot` (no flag) — for reading page content (text, headings, data lists)\n- **Be thorough but use judgment.** You are not following a test script -- you are exploring like a real user would. If something feels off, investigate.\n- **Write findings incrementally.** Append each issue to the report as you discover it. If the session is interrupted, findings are preserved. Never batch all issues for the end.\n- **Never delete output files.** Do not `rm` screenshots, videos, or the report mid-session. Do not close the session and restart. Work forward, not backward.\n- **Never read the target app's source code.** You are testing as a user, not auditing code. Do not read HTML, JS, or config files of the app under test. All findings must come from what you observe in the browser.\n- **Check the console.** Many issues are invisible in the UI but show up as JS errors or failed requests.\n- **Test like a user, not a robot.** Try common workflows end-to-end. Click things a real user would click. Enter realistic data.\n- **Type like a human.** When filling form fields during video recording, use `type` instead of `fill` -- it types character-by-character. Use `fill` only outside of video recording when speed matters.\n- **Pace repro videos for humans.** Add `sleep 1` between actions and `sleep 2` before the final result screenshot. Videos should be watchable at 1x speed -- a human reviewing the report needs to see what happened, not a blur of instant state changes.\n- **Be efficient with commands.** Batch multiple `agent-browser` commands in a single shell call when they are independent (e.g., `agent-browser ... screenshot ... && agent-browser ... console`). Use `agent-browser --session {SESSION} scroll down 300` for scrolling -- do not use `key` or `evaluate` to scroll.\n\n## References\n\n| Reference | When to Read |\n|-----------|--------------|\n| [references/issue-taxonomy.md](references/issue-taxonomy.md) | Start of session -- calibrate what to look for, severity levels, exploration checklist |\n\n## Templates\n\n| Template | Purpose |\n|----------|---------|\n| [templates/dogfood-report-template.md](templates/dogfood-report-template.md) | Copy into output directory as the report file |\n"
  },
  {
    "path": "skills/dogfood/references/issue-taxonomy.md",
    "content": "# Issue Taxonomy\n\nReference for categorizing issues found during dogfooding. Read this at the start of a dogfood session to calibrate what to look for.\n\n## Contents\n\n- [Severity Levels](#severity-levels)\n- [Categories](#categories)\n- [Exploration Checklist](#exploration-checklist)\n\n## Severity Levels\n\n| Severity | Definition |\n|----------|------------|\n| **critical** | Blocks a core workflow, causes data loss, or crashes the app |\n| **high** | Major feature broken or unusable, no workaround |\n| **medium** | Feature works but with noticeable problems, workaround exists |\n| **low** | Minor cosmetic or polish issue |\n\n## Categories\n\n### Visual / UI\n\n- Layout broken or misaligned elements\n- Overlapping or clipped text\n- Inconsistent spacing, padding, or margins\n- Missing or broken icons/images\n- Dark mode / light mode rendering issues\n- Responsive layout problems (viewport sizes)\n- Z-index stacking issues (elements hidden behind others)\n- Font rendering issues (wrong font, size, weight)\n- Color contrast problems\n- Animation glitches or jank\n\n### Functional\n\n- Broken links (404, wrong destination)\n- Buttons or controls that do nothing on click\n- Form validation that rejects valid input or accepts invalid input\n- Incorrect redirects\n- Features that fail silently\n- State not persisted when expected (lost on refresh, navigation)\n- Race conditions (double-submit, stale data)\n- Broken search or filtering\n- Pagination issues\n- File upload/download failures\n\n### UX\n\n- Confusing or unclear navigation\n- Missing loading indicators or feedback after actions\n- Slow or unresponsive interactions (>300ms perceived delay)\n- Unclear error messages\n- Missing confirmation for destructive actions\n- Dead ends (no way to go back or proceed)\n- Inconsistent patterns across similar features\n- Missing keyboard shortcuts or focus management\n- Unintuitive defaults\n- Missing empty states or unhelpful empty states\n\n### Content\n\n- Typos or grammatical errors\n- Outdated or incorrect text\n- Placeholder or lorem ipsum content left in\n- Truncated text without tooltip or expansion\n- Missing or wrong labels\n- Inconsistent terminology\n\n### Performance\n\n- Slow page loads (>3s)\n- Janky scrolling or animations\n- Large layout shifts (content jumping)\n- Excessive network requests (check via console/network)\n- Memory leaks (page slows over time)\n- Unoptimized images (large file sizes)\n\n### Console / Errors\n\n- JavaScript exceptions in console\n- Failed network requests (4xx, 5xx)\n- Deprecation warnings\n- CORS errors\n- Mixed content warnings\n- Unhandled promise rejections\n\n### Accessibility\n\n- Missing alt text on images\n- Unlabeled form inputs\n- Poor keyboard navigation (can't tab to elements)\n- Focus traps\n- Insufficient color contrast\n- Missing ARIA attributes on dynamic content\n- Screen reader incompatible patterns\n\n## Exploration Checklist\n\nUse this as a guide for what to test on each page/feature:\n\n1. **Visual scan** -- Take an annotated screenshot. Look for layout, alignment, and rendering issues.\n2. **Interactive elements** -- Click every button, link, and control. Do they work? Is there feedback?\n3. **Forms** -- Fill and submit. Test empty submission, invalid input, and edge cases.\n4. **Navigation** -- Follow all navigation paths. Check breadcrumbs, back button, deep links.\n5. **States** -- Check empty states, loading states, error states, and full/overflow states.\n6. **Console** -- Check for JS errors, failed requests, and warnings.\n7. **Responsiveness** -- If relevant, test at different viewport sizes.\n8. **Auth boundaries** -- Test what happens when not logged in, with different roles if applicable.\n"
  },
  {
    "path": "skills/dogfood/templates/dogfood-report-template.md",
    "content": "# Dogfood Report: {APP_NAME}\n\n| Field | Value |\n|-------|-------|\n| **Date** | {DATE} |\n| **App URL** | {URL} |\n| **Session** | {SESSION_NAME} |\n| **Scope** | {SCOPE} |\n\n## Summary\n\n| Severity | Count |\n|----------|-------|\n| Critical | 0 |\n| High | 0 |\n| Medium | 0 |\n| Low | 0 |\n| **Total** | **0** |\n\n## Issues\n\n<!-- Copy this block for each issue found. Interactive issues need video + step-by-step screenshots. Static issues (typos, visual glitches) only need a single screenshot -- set Repro Video to N/A. -->\n\n### ISSUE-001: {Short title}\n\n| Field | Value |\n|-------|-------|\n| **Severity** | critical / high / medium / low |\n| **Category** | visual / functional / ux / content / performance / console / accessibility |\n| **URL** | {page URL where issue was found} |\n| **Repro Video** | {path to video, or N/A for static issues} |\n\n**Description**\n\n{What is wrong, what was expected, and what actually happened.}\n\n**Repro Steps**\n\n<!-- Each step has a screenshot. A reader should be able to follow along visually. -->\n\n1. Navigate to {URL}\n   ![Step 1](screenshots/issue-001-step-1.png)\n\n2. {Action -- e.g., click \"Settings\" in the sidebar}\n   ![Step 2](screenshots/issue-001-step-2.png)\n\n3. {Action -- e.g., type \"test\" in the search field and press Enter}\n   ![Step 3](screenshots/issue-001-step-3.png)\n\n4. **Observe:** {what goes wrong -- e.g., the page shows a blank white screen instead of search results}\n   ![Result](screenshots/issue-001-result.png)\n\n---\n"
  },
  {
    "path": "skills/electron/SKILL.md",
    "content": "---\nname: electron\ndescription: Automate Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) using agent-browser via Chrome DevTools Protocol. Use when the user needs to interact with an Electron app, automate a desktop app, connect to a running app, control a native app, or test an Electron application. Triggers include \"automate Slack app\", \"control VS Code\", \"interact with Discord app\", \"test this Electron app\", \"connect to desktop app\", or any task requiring automation of a native Electron application.\nallowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)\n---\n\n# Electron App Automation\n\nAutomate any Electron desktop app using agent-browser. Electron apps are built on Chromium and expose a Chrome DevTools Protocol (CDP) port that agent-browser can connect to, enabling the same snapshot-interact workflow used for web pages.\n\n## Core Workflow\n\n1. **Launch** the Electron app with remote debugging enabled\n2. **Connect** agent-browser to the CDP port\n3. **Snapshot** to discover interactive elements\n4. **Interact** using element refs\n5. **Re-snapshot** after navigation or state changes\n\n```bash\n# Launch an Electron app with remote debugging\nopen -a \"Slack\" --args --remote-debugging-port=9222\n\n# Connect agent-browser to the app\nagent-browser connect 9222\n\n# Standard workflow from here\nagent-browser snapshot -i\nagent-browser click @e5\nagent-browser screenshot slack-desktop.png\n```\n\n## Launching Electron Apps with CDP\n\nEvery Electron app supports the `--remote-debugging-port` flag since it's built into Chromium.\n\n### macOS\n\n```bash\n# Slack\nopen -a \"Slack\" --args --remote-debugging-port=9222\n\n# VS Code\nopen -a \"Visual Studio Code\" --args --remote-debugging-port=9223\n\n# Discord\nopen -a \"Discord\" --args --remote-debugging-port=9224\n\n# Figma\nopen -a \"Figma\" --args --remote-debugging-port=9225\n\n# Notion\nopen -a \"Notion\" --args --remote-debugging-port=9226\n\n# Spotify\nopen -a \"Spotify\" --args --remote-debugging-port=9227\n```\n\n### Linux\n\n```bash\nslack --remote-debugging-port=9222\ncode --remote-debugging-port=9223\ndiscord --remote-debugging-port=9224\n```\n\n### Windows\n\n```bash\n\"C:\\Users\\%USERNAME%\\AppData\\Local\\slack\\slack.exe\" --remote-debugging-port=9222\n\"C:\\Users\\%USERNAME%\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe\" --remote-debugging-port=9223\n```\n\n**Important:** If the app is already running, quit it first, then relaunch with the flag. The `--remote-debugging-port` flag must be present at launch time.\n\n## Connecting\n\n```bash\n# Connect to a specific port\nagent-browser connect 9222\n\n# Or use --cdp on each command\nagent-browser --cdp 9222 snapshot -i\n\n# Auto-discover a running Chromium-based app\nagent-browser --auto-connect snapshot -i\n```\n\nAfter `connect`, all subsequent commands target the connected app without needing `--cdp`.\n\n## Tab Management\n\nElectron apps often have multiple windows or webviews. Use tab commands to list and switch between them:\n\n```bash\n# List all available targets (windows, webviews, etc.)\nagent-browser tab\n\n# Switch to a specific tab by index\nagent-browser tab 2\n\n# Switch by URL pattern\nagent-browser tab --url \"*settings*\"\n```\n\n## Webview Support\n\nElectron `<webview>` elements are automatically discovered and can be controlled like regular pages. Webviews appear as separate targets in the tab list with `type: \"webview\"`:\n\n```bash\n# Connect to running Electron app\nagent-browser connect 9222\n\n# List targets -- webviews appear alongside pages\nagent-browser tab\n# Example output:\n#   0: [page]    Slack - Main Window     https://app.slack.com/\n#   1: [webview] Embedded Content        https://example.com/widget\n\n# Switch to a webview\nagent-browser tab 1\n\n# Interact with the webview normally\nagent-browser snapshot -i\nagent-browser click @e3\nagent-browser screenshot webview.png\n```\n\n**Note:** Webview support works via raw CDP connection.\n\n## Common Patterns\n\n### Inspect and Navigate an App\n\n```bash\nopen -a \"Slack\" --args --remote-debugging-port=9222\nsleep 3  # Wait for app to start\nagent-browser connect 9222\nagent-browser snapshot -i\n# Read the snapshot output to identify UI elements\nagent-browser click @e10  # Navigate to a section\nagent-browser snapshot -i  # Re-snapshot after navigation\n```\n\n### Take Screenshots of Desktop Apps\n\n```bash\nagent-browser connect 9222\nagent-browser screenshot app-state.png\nagent-browser screenshot --full full-app.png\nagent-browser screenshot --annotate annotated-app.png\n```\n\n### Extract Data from a Desktop App\n\n```bash\nagent-browser connect 9222\nagent-browser snapshot -i\nagent-browser get text @e5\nagent-browser snapshot --json > app-state.json\n```\n\n### Fill Forms in Desktop Apps\n\n```bash\nagent-browser connect 9222\nagent-browser snapshot -i\nagent-browser fill @e3 \"search query\"\nagent-browser press Enter\nagent-browser wait 1000\nagent-browser snapshot -i\n```\n\n### Run Multiple Apps Simultaneously\n\nUse named sessions to control multiple Electron apps at the same time:\n\n```bash\n# Connect to Slack\nagent-browser --session slack connect 9222\n\n# Connect to VS Code\nagent-browser --session vscode connect 9223\n\n# Interact with each independently\nagent-browser --session slack snapshot -i\nagent-browser --session vscode snapshot -i\n```\n\n## Color Scheme\n\nThe default color scheme when connecting via CDP may be `light`. To preserve dark mode:\n\n```bash\nagent-browser connect 9222\nagent-browser --color-scheme dark snapshot -i\n```\n\nOr set it globally:\n\n```bash\nAGENT_BROWSER_COLOR_SCHEME=dark agent-browser connect 9222\n```\n\n## Troubleshooting\n\n### \"Connection refused\" or \"Cannot connect\"\n\n- Make sure the app was launched with `--remote-debugging-port=NNNN`\n- If the app was already running, quit and relaunch with the flag\n- Check that the port isn't in use by another process: `lsof -i :9222`\n\n### App launches but connect fails\n\n- Wait a few seconds after launch before connecting (`sleep 3`)\n- Some apps take time to initialize their webview\n\n### Elements not appearing in snapshot\n\n- The app may use multiple webviews. Use `agent-browser tab` to list targets and switch to the right one\n- Use `agent-browser snapshot -i -C` to include cursor-interactive elements (divs with onclick handlers)\n\n### Cannot type in input fields\n\n- Try `agent-browser keyboard type \"text\"` to type at the current focus without a selector\n- Some Electron apps use custom input components; use `agent-browser keyboard inserttext \"text\"` to bypass key events\n\n## Supported Apps\n\nAny app built on Electron works, including:\n\n- **Communication:** Slack, Discord, Microsoft Teams, Signal, Telegram Desktop\n- **Development:** VS Code, GitHub Desktop, Postman, Insomnia\n- **Design:** Figma, Notion, Obsidian\n- **Media:** Spotify, Tidal\n- **Productivity:** Todoist, Linear, 1Password\n\nIf an app is built with Electron, it supports `--remote-debugging-port` and can be automated with agent-browser.\n"
  },
  {
    "path": "skills/slack/SKILL.md",
    "content": "---\nname: slack\ndescription: Interact with Slack workspaces using browser automation. Use when the user needs to check unread channels, navigate Slack, send messages, extract data, find information, search conversations, or automate any Slack task. Triggers include \"check my Slack\", \"what channels have unreads\", \"send a message to\", \"search Slack for\", \"extract from Slack\", \"find who said\", or any task requiring programmatic Slack interaction.\nallowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)\n---\n\n# Slack Automation\n\nInteract with Slack workspaces to check messages, extract data, and automate common tasks.\n\n## Quick Start\n\nConnect to an existing Slack browser session or open Slack:\n\n```bash\n# Connect to existing session on port 9222 (typical for already-open Slack)\nagent-browser connect 9222\n\n# Or open Slack if not already running\nagent-browser open https://app.slack.com\n```\n\nThen take a snapshot to see what's available:\n\n```bash\nagent-browser snapshot -i\n```\n\n## Core Workflow\n\n1. **Connect/Navigate**: Open or connect to Slack\n2. **Snapshot**: Get interactive elements with refs (`@e1`, `@e2`, etc.)\n3. **Navigate**: Click tabs, expand sections, or navigate to specific channels\n4. **Extract/Interact**: Read data or perform actions\n5. **Screenshot**: Capture evidence of findings\n\n```bash\n# Example: Check unread channels\nagent-browser connect 9222\nagent-browser snapshot -i\n# Look for \"More unreads\" button\nagent-browser click @e21  # Ref for \"More unreads\" button\nagent-browser screenshot slack-unreads.png\n```\n\n## Common Tasks\n\n### Checking Unread Messages\n\n```bash\n# Connect to Slack\nagent-browser connect 9222\n\n# Take snapshot to locate unreads button\nagent-browser snapshot -i\n\n# Look for:\n# - \"More unreads\" button (usually near top of sidebar)\n# - \"Unreads\" toggle in Activity tab (shows unread count)\n# - Channel names with badges/bold text indicating unreads\n\n# Navigate to Activity tab to see all unreads in one view\nagent-browser click @e14  # Activity tab (ref may vary)\nagent-browser wait 1000\nagent-browser screenshot activity-unreads.png\n\n# Or check DMs tab\nagent-browser click @e13  # DMs tab\nagent-browser screenshot dms.png\n\n# Or expand \"More unreads\" in sidebar\nagent-browser click @e21  # More unreads button\nagent-browser wait 500\nagent-browser screenshot expanded-unreads.png\n```\n\n### Navigating to a Channel\n\n```bash\n# Search for channel in sidebar or by name\nagent-browser snapshot -i\n\n# Look for channel name in the list (e.g., \"engineering\", \"product-design\")\n# Click on the channel treeitem ref\nagent-browser click @e94  # Example: engineering channel ref\nagent-browser wait --load networkidle\nagent-browser screenshot channel.png\n```\n\n### Finding Messages/Threads\n\n```bash\n# Use Slack search\nagent-browser snapshot -i\nagent-browser click @e5  # Search button (typical ref)\nagent-browser fill @e_search \"keyword\"\nagent-browser press Enter\nagent-browser wait --load networkidle\nagent-browser screenshot search-results.png\n```\n\n### Extracting Channel Information\n\n```bash\n# Get list of all visible channels\nagent-browser snapshot --json > slack-snapshot.json\n\n# Parse for channel names and metadata\n# Look for treeitem elements with level=2 (sub-channels under sections)\n```\n\n### Checking Channel Details\n\n```bash\n# Open a channel\nagent-browser click @e_channel_ref\nagent-browser wait 1000\n\n# Get channel info (members, description, etc.)\nagent-browser snapshot -i\nagent-browser screenshot channel-details.png\n\n# Scroll through messages\nagent-browser scroll down 500\nagent-browser screenshot channel-messages.png\n```\n\n### Taking Notes/Capturing State\n\nWhen you need to document findings from Slack:\n\n```bash\n# Take annotated screenshot (shows element numbers)\nagent-browser screenshot --annotate slack-state.png\n\n# Take full-page screenshot\nagent-browser screenshot --full slack-full.png\n\n# Get current URL for reference\nagent-browser get url\n\n# Get page title\nagent-browser get title\n```\n\n## Sidebar Structure\n\nUnderstanding Slack's sidebar helps you navigate efficiently:\n\n```\n- Threads\n- Huddles\n- Drafts & sent\n- Directories\n- [Section Headers - External connections, Starred, Channels, etc.]\n  - [Channels listed as treeitems]\n- Direct Messages\n  - [DMs listed]\n- Apps\n  - [App shortcuts]\n- [More unreads] button (toggles unread channels list)\n```\n\nKey refs to look for:\n- `@e12` - Home tab (usually)\n- `@e13` - DMs tab\n- `@e14` - Activity tab\n- `@e5` - Search button\n- `@e21` - More unreads button (varies by session)\n\n## Tabs in Slack\n\nAfter clicking on a channel, you'll see tabs:\n- **Messages** - Channel conversation\n- **Files** - Shared files\n- **Pins** - Pinned messages\n- **Add canvas** - Collaborative canvas\n- Other tabs depending on workspace setup\n\nClick tab refs to switch views and get different information.\n\n## Extracting Data from Slack\n\n### Get Text Content\n\n```bash\n# Get a message or element's text\nagent-browser get text @e_message_ref\n```\n\n### Parse Accessibility Tree\n\n```bash\n# Full snapshot as JSON for programmatic parsing\nagent-browser snapshot --json > output.json\n\n# Look for:\n# - Channel names (name field in treeitem)\n# - Message content (in listitem/document elements)\n# - User names (button elements with user info)\n# - Timestamps (link elements with time info)\n```\n\n### Count Unreads\n\n```bash\n# After expanding unreads section:\nagent-browser snapshot -i | grep -c \"treeitem\"\n# Each treeitem with a channel name in the unreads section is one unread\n```\n\n## Best Practices\n\n- **Connect to existing sessions**: Use `agent-browser connect 9222` if Slack is already open. This is faster than opening a new browser.\n- **Take snapshots before clicking**: Always `snapshot -i` to identify refs before clicking buttons.\n- **Re-snapshot after navigation**: After navigating to a new channel or section, take a fresh snapshot to find new refs.\n- **Use JSON snapshots for parsing**: When you need to extract structured data, use `snapshot --json` for machine-readable output.\n- **Pace interactions**: Add `sleep 1` between rapid interactions to let the UI update.\n- **Check accessibility tree**: The accessibility tree shows what screen readers (and your automation) can see. If an element isn't in the snapshot, it may be hidden or require scrolling.\n- **Scroll in sidebar**: Use `agent-browser scroll down 300 --selector \".p-sidebar\"` to scroll within the Slack sidebar if channel list is long.\n\n## Limitations\n\n- **Cannot access Slack API**: This uses browser automation, not the Slack API. No OAuth, webhooks, or bot tokens needed.\n- **Session-specific**: Screenshots and snapshots are tied to the current browser session.\n- **Rate limiting**: Slack may rate-limit rapid interactions. Add delays between commands if needed.\n- **Workspace-specific**: You interact with your own workspace -- no cross-workspace automation.\n\n## Debugging\n\n### Check console for errors\n\n```bash\nagent-browser console\nagent-browser errors\n```\n\n### View raw HTML of an element\n\n```bash\n# Snapshot shows the accessibility tree. If an element isn't there,\n# it may not be interactive (e.g., div instead of button)\n# Use snapshot -i -C to include cursor-interactive divs\nagent-browser snapshot -i -C\n```\n\n### Get current page state\n\n```bash\nagent-browser get url\nagent-browser get title\nagent-browser screenshot page-state.png\n```\n\n## Example: Full Unread Check\n\n```bash\n#!/bin/bash\n\n# Connect to Slack\nagent-browser connect 9222\n\n# Take initial snapshot\necho \"=== Checking Slack unreads ===\"\nagent-browser snapshot -i > snapshot.txt\n\n# Check Activity tab for unreads\nagent-browser click @e14  # Activity tab\nagent-browser wait 1000\nagent-browser screenshot activity.png\nACTIVITY_RESULT=$(agent-browser get text @e_main_area)\necho \"Activity: $ACTIVITY_RESULT\"\n\n# Check DMs\nagent-browser click @e13  # DMs tab\nagent-browser wait 1000\nagent-browser screenshot dms.png\n\n# Check unread channels in sidebar\nagent-browser click @e21  # More unreads button\nagent-browser wait 500\nagent-browser snapshot -i > unreads-expanded.txt\nagent-browser screenshot unreads.png\n\n# Summary\necho \"=== Summary ===\"\necho \"See activity.png, dms.png, and unreads.png for full details\"\n```\n\n## References\n\n- **Slack docs**: https://slack.com/help\n- **Web experience**: https://app.slack.com\n- **Keyboard shortcuts**: Type `?` in Slack for shortcut list\n"
  },
  {
    "path": "skills/slack/references/slack-tasks.md",
    "content": "# Common Slack Tasks & Patterns\n\nReference guide for common automations and data extraction patterns when interacting with Slack.\n\n## Task: Check All Unread Messages\n\n### Goal\nDetermine which channels and DMs have unread messages.\n\n### Steps\n\n1. **Connect to Slack**\n   ```bash\n   agent-browser connect 9222\n   ```\n\n2. **Check Activity Tab**\n   - Take snapshot: `agent-browser snapshot -i`\n   - Look for Activity tab ref (usually `@e14`)\n   - Click: `agent-browser click @e14`\n   - Wait: `agent-browser wait 1000`\n   - If you see \"You've read all the unreads\", you have no unread messages\n   - Screenshot: `agent-browser screenshot activity.png`\n\n3. **Check DMs**\n   - Click DMs tab ref (usually `@e13`)\n   - Look for \"Unreads\" toggle/badge\n   - Count visible conversations with indicators\n\n4. **Check Channels**\n   - Look for \"More unreads\" button (usually in sidebar)\n   - Click it to expand list of channels with unreads\n   - Screenshot the expanded view\n   - Parse channel names from snapshot\n\n5. **Summary**\n   - Activity + DMs + Channels = complete unread picture\n\n### Evidence Capture\n- Screenshot of Activity tab\n- Screenshot of DMs\n- Screenshot of expanded unreads sidebar\n\n---\n\n## Task: Find All Channels in Workspace\n\n### Goal\nGet a complete list of all channels you have access to.\n\n### Steps\n\n1. **Navigate to Channels section**\n   ```bash\n   agent-browser connect 9222\n   agent-browser snapshot -i\n   ```\n\n2. **Look for \"Channels\" treeitem**\n   - This is usually a collapsed section header\n   - Click to expand if collapsed\n   - Screenshot: `agent-browser screenshot all-channels.png`\n\n3. **Scroll through sidebar**\n   ```bash\n   # If the list is long, scroll within the sidebar\n   agent-browser scroll down 500 --selector \".p-sidebar\"\n   agent-browser screenshot channels-page-2.png\n   ```\n\n4. **Parse snapshot for channel list**\n   ```bash\n   agent-browser snapshot --json > channels.json\n   # Search JSON for treeitem elements with level=2 under \"Channels\" section\n   ```\n\n### Evidence\n- JSON snapshot with all channel refs\n- Screenshots of channel list\n- Count of total channels\n\n---\n\n## Task: Search for Messages Containing Keywords\n\n### Goal\nFind all messages/threads mentioning specific terms.\n\n### Steps\n\n1. **Open search**\n   ```bash\n   agent-browser snapshot -i\n   # Find Search button ref (usually @e5)\n   agent-browser click @e5\n   agent-browser wait 500\n   ```\n\n2. **Enter search term**\n   ```bash\n   # Identify search input ref from snapshot\n   agent-browser fill @e_search_input \"your keyword\"\n   agent-browser press Enter\n   agent-browser wait --load networkidle\n   ```\n\n3. **Capture results**\n   ```bash\n   agent-browser screenshot search-results.png\n   agent-browser snapshot -i > search-snapshot.txt\n   ```\n\n4. **Parse results**\n   - Look for result items in snapshot\n   - Extract message content, sender, channel, timestamp\n   - Follow links to view full context\n\n### Filters\nSlack search supports filters:\n- `in:channel-name` - Search in specific channel\n- `from:@user` - Messages from specific user\n- `before:2026-02-25` - Messages before date\n- `after:2026-02-20` - Messages after date\n- `has:file` - Messages with files\n- `has:emoji` - Messages with reactions\n\nExample search: `\"bug report\" in:engineering from:@alice after:2026-02-20`\n\n---\n\n## Task: Monitor a Specific Channel for Activity\n\n### Goal\nWatch a channel and capture new messages/engagement.\n\n### Steps\n\n1. **Navigate to channel**\n   ```bash\n   agent-browser connect 9222\n   agent-browser snapshot -i\n   # Find channel ref from sidebar\n   agent-browser click @e_channel_ref\n   agent-browser wait --load networkidle\n   ```\n\n2. **Check channel info**\n   - Screenshot channel details: `agent-browser screenshot channel-header.png`\n   - Look for member count, description, topic\n\n3. **View messages**\n   ```bash\n   # Jump to recent/unread\n   agent-browser press j  # Jump to unread in Slack\n   agent-browser wait 500\n   agent-browser screenshot recent-messages.png\n   ```\n\n4. **Scroll to see more**\n   ```bash\n   agent-browser scroll down 500\n   agent-browser screenshot more-messages.png\n   ```\n\n5. **Check threads**\n   - Click on messages with thread indicators\n   - View replies in thread view\n   - Screenshot: `agent-browser screenshot thread.png`\n\n### Evidence\n- Channel info screenshot\n- Message history screenshots\n- Thread examples\n\n---\n\n## Task: Extract User Information from a Conversation\n\n### Goal\nFind who said what, when, and in what context.\n\n### Steps\n\n1. **Navigate to relevant channel or DM**\n   ```bash\n   agent-browser click @e_conversation_ref\n   agent-browser wait 1000\n   ```\n\n2. **Take snapshot with context**\n   ```bash\n   agent-browser snapshot --json > conversation.json\n   ```\n\n3. **Find message blocks**\n   - In JSON, look for document/listitem elements\n   - These contain: user name (button), timestamp (link), message text, reactions\n\n4. **Extract structured data**\n   - User: Found in button element with username\n   - Time: Found in link with timestamp\n   - Content: Text content of message\n   - Reactions: Buttons showing emoji counts\n\n5. **Screenshot key messages**\n   ```bash\n   agent-browser screenshot important-message.png\n   agent-browser screenshot --annotate annotated-message.png\n   ```\n\n---\n\n## Task: Track Reactions to a Message\n\n### Goal\nSee who reacted to a message and with what emoji.\n\n### Steps\n\n1. **Find message with reactions**\n   ```bash\n   agent-browser snapshot -i\n   # Look for \"N reaction(s)\" buttons in messages\n   ```\n\n2. **Click reaction button to expand**\n   ```bash\n   agent-browser click @e_reaction_button\n   agent-browser wait 500\n   ```\n\n3. **Capture reaction details**\n   ```bash\n   agent-browser screenshot reactions.png\n   # You'll see emoji, count, and list of users who reacted\n   ```\n\n4. **Extract data**\n   - Emoji used\n   - Number of people who reacted\n   - User names (if visible in popup)\n\n---\n\n## Task: Find and Review Pinned Messages\n\n### Goal\nSee messages that have been pinned in a channel.\n\n### Steps\n\n1. **Open a channel**\n   ```bash\n   agent-browser click @e_channel_ref\n   agent-browser wait 1000\n   agent-browser snapshot -i\n   ```\n\n2. **Click Pins tab**\n   - In channel view, look for \"Pins\" tab ref (usually near Messages, Files tabs)\n   - Click it: `agent-browser click @e_pins_tab`\n   - Wait: `agent-browser wait 500`\n\n3. **View pinned messages**\n   ```bash\n   agent-browser screenshot pins.png\n   agent-browser snapshot -i > pins-snapshot.txt\n   ```\n\n4. **Review each pin**\n   - Click pin to see context\n   - Note who pinned it, when, and why\n   - Screenshot: `agent-browser screenshot pin-detail.png`\n\n---\n\n## Pattern: Extract Timestamp from Link\n\nIn Slack snapshot, message timestamps appear as links. Example:\n```\n- link \"Feb 25th at 10:26:22 AM\" [ref=e151]\n  - /url: https://vercel.slack.com/archives/C0A5RTN0856/p1772036782543189\n```\n\nThe URL contains the timestamp in the fragment (`p1772036782543189`). This is a Slack message ID that uniquely identifies the message.\n\n---\n\n## Pattern: Understanding Channel/Thread Structure\n\n```\n- treeitem \"channel-name\" [ref=e94] [level=2]\n  - group: (contains channel metadata or sub-items)\n```\n\n- **level=1**: Section headers (External connections, Starred, Channels, etc.)\n- **level=2**: Individual channels/items within sections\n- **level=3+**: Nested sub-items (rare in sidebar)\n\n---\n\n## Common Ref Patterns (Session-Dependent)\n\nThese refs vary per session, but follow patterns:\n\n| Element | Typical Ref Range | How to Find |\n|---------|------------------|------------|\n| Home tab | e10-e20 | `snapshot -i \\| grep \"Home\"` |\n| DMs tab | e10-e20 | `snapshot -i \\| grep \"DMs\"` |\n| Activity tab | e10-e20 | `snapshot -i \\| grep \"Activity\"` |\n| Search | e5-e10 | `snapshot -i \\| grep \"Search\"` |\n| More unreads | e20-e30 | `snapshot -i \\| grep \"More unreads\"` |\n| Channel refs | e30+ | `snapshot -i \\| grep \"treeitem\"` |\n\n**Always take a fresh snapshot** to find current refs for the current session.\n\n---\n\n## Debugging: Element Not Found\n\nIf you can't find an element:\n\n1. **Check it's visible**\n   ```bash\n   # Is the element on screen or off-screen?\n   agent-browser screenshot current-state.png\n   # Compare screenshot to what you expected\n   ```\n\n2. **Try expanding/scrolling**\n   ```bash\n   # Sidebar might need scrolling\n   agent-browser scroll down 300 --selector \".p-sidebar\"\n   agent-browser snapshot -i\n   ```\n\n3. **Try snapshot with extended range**\n   ```bash\n   # Include cursor-interactive elements (divs with onclick handlers)\n   agent-browser snapshot -i -C\n   ```\n\n4. **Check current URL**\n   ```bash\n   agent-browser get url\n   # Verify you're in the right section\n   ```\n\n5. **Wait for page to load**\n   ```bash\n   agent-browser wait --load networkidle\n   agent-browser wait 1000\n   agent-browser snapshot -i\n   ```\n"
  },
  {
    "path": "skills/slack/templates/slack-report-template.md",
    "content": "# Slack Analysis Report\n\n**Date**: [DATE]\n**Workspace**: [WORKSPACE_NAME]\n**Analyst**: [YOUR_NAME]\n**Scope**: [WHAT_YOU_ANALYZED]\n\n## Summary\n\n### Unread Counts\n- **Activity**: [NUMBER] unreads\n- **Direct Messages**: [NUMBER] unreads\n- **Channels**: [NUMBER] channels with unreads\n\n### Key Findings\n- [FINDING 1]\n- [FINDING 2]\n- [FINDING 3]\n\n---\n\n## Unread Channels\n\nList of channels with unread messages:\n\n| Channel | Unread Count | Last Activity | Notes |\n|---------|-------------|---------------|-------|\n| #engineering | 12 | Today 2:45 PM | Active discussion thread |\n| #announcements | 3 | Yesterday 5:30 PM | Team updates |\n| #random | 5 | Today 11:20 AM | Various topics |\n\n---\n\n## Unread Direct Messages\n\n| User/Group | Message Count | Last Message | Preview |\n|------------|--------------|--------------|---------|\n| @alice | 2 | Today 3:15 PM | \"Are you free to...\" |\n| @product-team | 5 | Today 2:00 PM | Sync scheduled for... |\n\n---\n\n## Channel Snapshot\n\n### Total Channels Accessible\n- **Public Channels**: [NUMBER]\n- **Private Channels**: [NUMBER]\n- **Group DMs**: [NUMBER]\n\n### Channel Categories\n- **External Connections**: [COUNT] channels\n- **Starred**: [COUNT] channels\n- **Main Channels**: [COUNT] channels\n\n---\n\n## Most Active Channels (by recent activity)\n\n| Rank | Channel | Activity | Participants |\n|------|---------|----------|--------------|\n| 1 | #engineering | High | 15+ active |\n| 2 | #general | High | 10+ active |\n| 3 | #product-design | Medium | 8+ active |\n\n---\n\n## Key Conversations\n\n### [TOPIC 1]: Channel #engineering\n- **Status**: Ongoing discussion\n- **Participants**: @alice, @bob, @charlie\n- **Latest Update**: [TIME]\n- **Thread Count**: 5 threads\n- **Files Shared**: 2 documents\n- **Screenshots**: See `engineering-thread.png`\n\n**Notes**: [Additional context about the conversation]\n\n### [TOPIC 2]: DM with @alice\n- **Unread Messages**: 2\n- **Last Message**: [TIME]\n- **Summary**: [Brief summary of conversation]\n- **Action Items**: [Any TODOs mentioned]\n\n---\n\n## Search Results\n\n### Query: \"[SEARCH_TERM]\"\n- **Results**: [NUMBER] messages\n- **Date Range**: [FROM] to [TO]\n- **Top Channels**: [LIST]\n- **Key Themes**: [PATTERNS OBSERVED]\n\n#### Sample Results\n1. **[Date/Time]** in #[channel]: [Message snippet]\n2. **[Date/Time]** in #[channel]: [Message snippet]\n3. **[Date/Time]** in #[channel]: [Message snippet]\n\n---\n\n## Reactions & Engagement\n\n### Most Reacted-To Messages\n| Message | Emoji | Count | Channel |\n|---------|-------|-------|---------|\n| \"Shipped to production\" | 🎉 | 8 | #engineering |\n| \"FYI the site is down\" | 🚨 | 12 | #incidents |\n\n---\n\n## Team Insights\n\n### Most Active Users (by message volume)\n1. @alice - [COUNT] messages\n2. @bob - [COUNT] messages\n3. @charlie - [COUNT] messages\n\n### Most Active Times\n- Peak hour: [TIME]\n- Peak day: [DAY]\n- Average messages per hour: [NUMBER]\n\n---\n\n## Issues / Observations\n\n### [ISSUE 1]: [Title]\n**Severity**: [Critical/High/Medium/Low]\n**Description**: [What was observed]\n**Evidence**: See `issue-1-screenshot.png`\n**Recommendation**: [Suggested action]\n\n---\n\n## Screenshots\n\n| File | Description |\n|------|-------------|\n| `activity-tab.png` | Activity tab showing unreads |\n| `dms-overview.png` | DM list with unread indicators |\n| `channels-full-list.png` | Complete channel list |\n| `engineering-thread.png` | Active engineering thread |\n\n---\n\n## Appendix: Raw Data\n\n### Snapshot Output\n```\n[Paste snapshot -i output here]\n```\n\n### JSON Snapshot (for parsing)\n```json\n[Paste snapshot --json output here]\n```\n\n---\n\n**Report Generated**: [DATE/TIME]\n**Analysis Duration**: [TIME]\n**Next Steps**: [TODO]\n"
  },
  {
    "path": "skills/vercel-sandbox/SKILL.md",
    "content": "---\nname: vercel-sandbox\ndescription: Run agent-browser + Chrome inside Vercel Sandbox microVMs for browser automation from any Vercel-deployed app. Use when the user needs browser automation in a Vercel app (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.), wants to run headless Chrome without binary size limits, needs persistent browser sessions across commands, or wants ephemeral isolated browser environments. Triggers include \"Vercel Sandbox browser\", \"microVM Chrome\", \"agent-browser in sandbox\", \"browser automation on Vercel\", or any task requiring Chrome in a Vercel Sandbox.\n---\n\n# Browser Automation with Vercel Sandbox\n\nRun agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs. A Linux VM spins up on demand, executes browser commands, and shuts down. Works with any Vercel-deployed framework (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.).\n\n## Dependencies\n\n```bash\npnpm add @vercel/sandbox\n```\n\nThe sandbox VM needs system dependencies for Chromium plus agent-browser itself. Use sandbox snapshots (below) to pre-install everything for sub-second startup.\n\n## Core Pattern\n\n```ts\nimport { Sandbox } from \"@vercel/sandbox\";\n\n// System libraries required by Chromium on the sandbox VM (Amazon Linux / dnf)\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\", \"nspr\", \"libxkbcommon\", \"atk\", \"at-spi2-atk\", \"at-spi2-core\",\n  \"libXcomposite\", \"libXdamage\", \"libXrandr\", \"libXfixes\", \"libXcursor\",\n  \"libXi\", \"libXtst\", \"libXScrnSaver\", \"libXext\", \"mesa-libgbm\", \"libdrm\",\n  \"mesa-libGL\", \"mesa-libEGL\", \"cups-libs\", \"alsa-lib\", \"pango\", \"cairo\",\n  \"gtk3\", \"dbus-libs\",\n];\n\nfunction getSandboxCredentials() {\n  if (\n    process.env.VERCEL_TOKEN &&\n    process.env.VERCEL_TEAM_ID &&\n    process.env.VERCEL_PROJECT_ID\n  ) {\n    return {\n      token: process.env.VERCEL_TOKEN,\n      teamId: process.env.VERCEL_TEAM_ID,\n      projectId: process.env.VERCEL_PROJECT_ID,\n    };\n  }\n  return {};\n}\n\nasync function withBrowser<T>(\n  fn: (sandbox: InstanceType<typeof Sandbox>) => Promise<T>,\n): Promise<T> {\n  const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;\n  const credentials = getSandboxCredentials();\n\n  const sandbox = snapshotId\n    ? await Sandbox.create({\n        ...credentials,\n        source: { type: \"snapshot\", snapshotId },\n        timeout: 120_000,\n      })\n    : await Sandbox.create({ ...credentials, runtime: \"node24\", timeout: 120_000 });\n\n  if (!snapshotId) {\n    await sandbox.runCommand(\"sh\", [\n      \"-c\",\n      `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n    ]);\n    await sandbox.runCommand(\"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n    await sandbox.runCommand(\"npx\", [\"agent-browser\", \"install\"]);\n  }\n\n  try {\n    return await fn(sandbox);\n  } finally {\n    await sandbox.stop();\n  }\n}\n```\n\n## Screenshot\n\nThe `screenshot --json` command saves to a file and returns the path. Read the file back as base64:\n\n```ts\nexport async function screenshotUrl(url: string) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const titleResult = await sandbox.runCommand(\"agent-browser\", [\n      \"get\", \"title\", \"--json\",\n    ]);\n    const title = JSON.parse(await titleResult.stdout())?.data?.title || url;\n\n    const ssResult = await sandbox.runCommand(\"agent-browser\", [\n      \"screenshot\", \"--json\",\n    ]);\n    const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;\n    const b64Result = await sandbox.runCommand(\"base64\", [\"-w\", \"0\", ssPath]);\n    const screenshot = (await b64Result.stdout()).trim();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n\n    return { title, screenshot };\n  });\n}\n```\n\n## Accessibility Snapshot\n\n```ts\nexport async function snapshotUrl(url: string) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const titleResult = await sandbox.runCommand(\"agent-browser\", [\n      \"get\", \"title\", \"--json\",\n    ]);\n    const title = JSON.parse(await titleResult.stdout())?.data?.title || url;\n\n    const snapResult = await sandbox.runCommand(\"agent-browser\", [\n      \"snapshot\", \"-i\", \"-c\",\n    ]);\n    const snapshot = await snapResult.stdout();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n\n    return { title, snapshot };\n  });\n}\n```\n\n## Multi-Step Workflows\n\nThe sandbox persists between commands, so you can run full automation sequences:\n\n```ts\nexport async function fillAndSubmitForm(url: string, data: Record<string, string>) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const snapResult = await sandbox.runCommand(\"agent-browser\", [\n      \"snapshot\", \"-i\",\n    ]);\n    const snapshot = await snapResult.stdout();\n    // Parse snapshot to find element refs...\n\n    for (const [ref, value] of Object.entries(data)) {\n      await sandbox.runCommand(\"agent-browser\", [\"fill\", ref, value]);\n    }\n\n    await sandbox.runCommand(\"agent-browser\", [\"click\", \"@e5\"]);\n    await sandbox.runCommand(\"agent-browser\", [\"wait\", \"--load\", \"networkidle\"]);\n\n    const ssResult = await sandbox.runCommand(\"agent-browser\", [\n      \"screenshot\", \"--json\",\n    ]);\n    const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;\n    const b64Result = await sandbox.runCommand(\"base64\", [\"-w\", \"0\", ssPath]);\n    const screenshot = (await b64Result.stdout()).trim();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n\n    return { screenshot };\n  });\n}\n```\n\n## Sandbox Snapshots (Fast Startup)\n\nA **sandbox snapshot** is a saved VM image of a Vercel Sandbox with system dependencies + agent-browser + Chromium already installed. Think of it like a Docker image -- instead of installing dependencies from scratch every time, the sandbox boots from the pre-built image.\n\nThis is unrelated to agent-browser's *accessibility snapshot* feature (`agent-browser snapshot`), which dumps a page's accessibility tree. A sandbox snapshot is a Vercel infrastructure concept for fast VM startup.\n\nWithout a sandbox snapshot, each run installs system deps + agent-browser + Chromium (~30s). With one, startup is sub-second.\n\n### Creating a sandbox snapshot\n\nThe snapshot must include system dependencies (via `dnf`), agent-browser, and Chromium:\n\n```ts\nimport { Sandbox } from \"@vercel/sandbox\";\n\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\", \"nspr\", \"libxkbcommon\", \"atk\", \"at-spi2-atk\", \"at-spi2-core\",\n  \"libXcomposite\", \"libXdamage\", \"libXrandr\", \"libXfixes\", \"libXcursor\",\n  \"libXi\", \"libXtst\", \"libXScrnSaver\", \"libXext\", \"mesa-libgbm\", \"libdrm\",\n  \"mesa-libGL\", \"mesa-libEGL\", \"cups-libs\", \"alsa-lib\", \"pango\", \"cairo\",\n  \"gtk3\", \"dbus-libs\",\n];\n\nasync function createSnapshot(): Promise<string> {\n  const sandbox = await Sandbox.create({\n    runtime: \"node24\",\n    timeout: 300_000,\n  });\n\n  await sandbox.runCommand(\"sh\", [\n    \"-c\",\n    `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n  ]);\n  await sandbox.runCommand(\"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n  await sandbox.runCommand(\"npx\", [\"agent-browser\", \"install\"]);\n\n  const snapshot = await sandbox.snapshot();\n  return snapshot.snapshotId;\n}\n```\n\nRun this once, then set the environment variable:\n\n```bash\nAGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx\n```\n\nA helper script is available in the demo app:\n\n```bash\nnpx tsx examples/environments/scripts/create-snapshot.ts\n```\n\nRecommended for any production deployment using the Sandbox pattern.\n\n## Authentication\n\nOn Vercel deployments, the Sandbox SDK authenticates automatically via OIDC. For local development or explicit control, set:\n\n```bash\nVERCEL_TOKEN=<personal-access-token>\nVERCEL_TEAM_ID=<team-id>\nVERCEL_PROJECT_ID=<project-id>\n```\n\nThese are spread into `Sandbox.create()` calls. When absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel).\n\n## Scheduled Workflows (Cron)\n\nCombine with Vercel Cron Jobs for recurring browser tasks:\n\n```ts\n// app/api/cron/route.ts  (or equivalent in your framework)\nexport async function GET() {\n  const result = await withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", \"https://example.com/pricing\"]);\n    const snap = await sandbox.runCommand(\"agent-browser\", [\"snapshot\", \"-i\", \"-c\"]);\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n    return await snap.stdout();\n  });\n\n  // Process results, send alerts, store data...\n  return Response.json({ ok: true, snapshot: result });\n}\n```\n\n```json\n// vercel.json\n{ \"crons\": [{ \"path\": \"/api/cron\", \"schedule\": \"0 9 * * *\" }] }\n```\n\n## Environment Variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `AGENT_BROWSER_SNAPSHOT_ID` | No (but recommended) | Pre-built sandbox snapshot ID for sub-second startup (see above) |\n| `VERCEL_TOKEN` | No | Vercel personal access token (for local dev; OIDC is automatic on Vercel) |\n| `VERCEL_TEAM_ID` | No | Vercel team ID (for local dev) |\n| `VERCEL_PROJECT_ID` | No | Vercel project ID (for local dev) |\n\n## Framework Examples\n\nThe pattern works identically across frameworks. The only difference is where you put the server-side code:\n\n| Framework | Server code location |\n|---|---|\n| Next.js | Server actions, API routes, route handlers |\n| SvelteKit | `+page.server.ts`, `+server.ts` |\n| Nuxt | `server/api/`, `server/routes/` |\n| Remix | `loader`, `action` functions |\n| Astro | `.astro` frontmatter, API routes |\n\n## Example\n\nSee `examples/environments/` in the agent-browser repo for a working app with the Vercel Sandbox pattern, including a sandbox snapshot creation script, streaming progress UI, and rate limiting.\n"
  }
]