From 3bb5cb00d20aee71cf0ce836cc664dd700f1b3e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 23 May 2026 22:50:13 +0000 Subject: [PATCH 1/4] feat(agent): add agent-browser provider with bundled MCP shim Adds agent_provider agent-browser wired through a small Node MCP server that proxies Vercel agent-browser (npx) with HAR start/stop to recording.har. Documents setup for VPS installs, expands dry-run/settings/docs, and lists bundled MCP node_modules in gitignore. Co-authored-by: kalil0321 --- .gitignore | 4 + CHANGELOG.md | 4 +- README.md | 13 +- pyproject.toml | 2 + src/reverse_api/agent_browser_bundle.py | 60 + src/reverse_api/agent_browser_mcp/README.md | 37 + .../agent_browser_mcp/package-lock.json | 1159 +++++++++++++++++ .../agent_browser_mcp/package.json | 14 + src/reverse_api/agent_browser_mcp/server.mjs | 279 ++++ src/reverse_api/auto_engineer.py | 77 +- src/reverse_api/cli.py | 61 +- src/reverse_api/config.py | 2 +- src/reverse_api/cursor_engineer.py | 17 + .../prompts/auto/user_agent_browser.md | 37 + tests/test_agent_browser_bundle.py | 35 + tests/test_auto_engineer.py | 71 + website/content/docs/cli/scripted-usage.mdx | 2 +- website/content/docs/configuration/agent.mdx | 12 + website/content/docs/modes/agent.mdx | 12 +- 19 files changed, 1875 insertions(+), 23 deletions(-) create mode 100644 src/reverse_api/agent_browser_bundle.py create mode 100644 src/reverse_api/agent_browser_mcp/README.md create mode 100644 src/reverse_api/agent_browser_mcp/package-lock.json create mode 100644 src/reverse_api/agent_browser_mcp/package.json create mode 100644 src/reverse_api/agent_browser_mcp/server.mjs create mode 100644 src/reverse_api/prompts/auto/user_agent_browser.md create mode 100644 tests/test_agent_browser_bundle.py diff --git a/.gitignore b/.gitignore index de2c13f..22f4a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,7 @@ debug/ store-assets/ .playwright-mcp/ + +# Node deps for bundled MCP shims (installed per README; never commit) +src/reverse_api/cursor_bridge/node_modules/ +src/reverse_api/agent_browser_mcp/node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9314915..498fcb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.9.0] - 2026-05-14 +### Added + +- **`agent_provider: "agent-browser"`**: MCP bridge bundled under `reverse_api/agent_browser_mcp/` that shells Vercel’s `agent-browser` CLI (via `npx`) for VPS/CI-friendly headless capture with HAR export to `recording.har`; see `src/reverse_api/agent_browser_mcp/README.md` for rollout notes. ### Added - **Cursor SDK support**: Added `sdk=cursor` / `--sdk cursor` engineering support through a bundled Node bridge around the Cursor TypeScript SDK. Cursor runs use the configured Cursor model (default `composer-2`), accept MCP server configuration, resume Cursor agents across follow-up turns, and normalize streamed tool output plus token usage into the existing TUI/message-store flow diff --git a/README.md b/README.md index dd6a0c9..4ba85aa 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,22 @@ Cycle modes with **Shift+Tab**: | Mode | What it does | |------|--------------| | `manual` | You drive the browser; AI generates the client from captured traffic. | -| `agent` | An AI agent drives the browser autonomously (Playwright MCP or Chrome DevTools MCP). | +| `agent` | An AI agent drives the browser autonomously (Playwright MCP, Chrome DevTools MCP, or Vercel agent-browser). | | `engineer` | Re-run generation on a previous capture (`engineer `). | | `collector` | Agent collects structured data (JSON/CSV) using web search + fetch. | Agent mode providers: - **auto** (default): Playwright MCP, single workflow for browsing + reverse engineering. - **chrome-mcp**: drives your real Chrome so you keep existing sessions/cookies. Requires Chrome 146+ and Node.js 20.19+. +- **agent-browser**: MCP bridge around [Vercel agent-browser](https://github.com/vercel-labs/agent-browser)—great for VPS/CI/headless where you want snapshots + refs without Playwright MCP. Requires **Node**, **npx**, the bundled MCP dependencies (`npm install --prefix …/agent_browser_mcp`), and **`agent-browser install`** for Chromium. See **`src/reverse_api/agent_browser_mcp/README.md`** in this repo for the checklist and roadmap. + +After installing/upgrading the Python package: + +```bash +npm install --prefix "$(python -c 'from pathlib import Path; import reverse_api; print(Path(reverse_api.__file__).parent / \"agent_browser_mcp\")')" +npm install -g agent-browser && agent-browser install +# Linux VPS: append --with-deps to the installer when prompted +``` ## Configuration @@ -119,7 +128,7 @@ Pass `--no-interactive` (and/or `--json`) to skip prompts. With `--json`, stdout | `run_id` | `string` \| `null` | Use with `show` / `engineer` / `run`. | | `prompt` | `string` | | | `url` | `string` \| `null` | | -| `mode` | `string` \| `null` | `"auto"` or `"chrome-mcp"`. | +| `mode` | `string` \| `null` | `"auto"`, `"chrome-mcp"`, or `"agent-browser"`. | | `har_path` | `string` \| `null` | Captured HAR. | | `script_path` | `string` \| `null` | Generated client. | | `usage` | `object` | `{input_tokens, output_tokens, total_cost}`. | diff --git a/pyproject.toml b/pyproject.toml index 2be5581..a09c686 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ allow-direct-references = true packages = ["src/reverse_api"] exclude = [ "src/reverse_api/cursor_bridge/node_modules/**", + "src/reverse_api/agent_browser_mcp/node_modules/**", ] [tool.hatch.build.targets.sdist] @@ -73,6 +74,7 @@ exclude = [ ".claude/worktrees/**", ".claude/plans/**", "src/reverse_api/cursor_bridge/node_modules/**", + "src/reverse_api/agent_browser_mcp/node_modules/**", ] [dependency-groups] diff --git a/src/reverse_api/agent_browser_bundle.py b/src/reverse_api/agent_browser_bundle.py new file mode 100644 index 0000000..32dd6c7 --- /dev/null +++ b/src/reverse_api/agent_browser_bundle.py @@ -0,0 +1,60 @@ +"""Bundled stdio MCP server for Vercel agent-browser (used by agent-provider ``agent-browser``).""" + +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Any + +_AGENT_BROWSER_HOME = Path(__file__).resolve().parent / "agent_browser_mcp" +_SERVER_JS = _AGENT_BROWSER_HOME / "server.mjs" +_MCP_MARKER = _AGENT_BROWSER_HOME / "node_modules" / "@modelcontextprotocol" + + +def bundled_agent_browser_mcp_server_js() -> Path: + return _SERVER_JS + + +def agent_browser_bundle_error() -> str | None: + """Return a user-facing setup message if bundled MCP deps are missing.""" + + if not _SERVER_JS.is_file(): + return f"Bundled agent-browser MCP is missing {_SERVER_JS.name} (broken install)." + if not _MCP_MARKER.is_dir(): + return ( + "agent-browser mode requires MCP bundle dependencies: " + f"npm install --prefix {_AGENT_BROWSER_HOME}" + ) + if not shutil.which("node"): + return "node executable not found in PATH (required for agent-browser MCP)." + if not shutil.which("npx"): + return "npx not found in PATH (downloads agent-browser on demand via MCP tools)." + return None + + +def agent_browser_stdio_mcp_config(*, har_path: Path, run_id: str, headless: bool) -> tuple[str, dict[str, Any]]: + """Return (server_name, mcp_servers entry) compatible with ClaudeAgentOptions.mcp_servers.""" + + cmd = bundled_agent_browser_mcp_server_js() + args = [ + str(cmd), + "--har-out", + str(har_path), + "--session", + run_id, + ] + if not headless: + args.append("--headed") + return "agent-browser", { + "type": "stdio", + "command": "node", + "args": args, + } + + +def agent_browser_command_list(*, har_path: Path, run_id: str, headless: bool) -> list[str]: + """Flat CLI command list used by OpenCode (``config.command`` expects argv).""" + + _name, cfg = agent_browser_stdio_mcp_config(har_path=har_path, run_id=run_id, headless=headless) + argv: list[str] = [cfg["command"], *(cfg["args"] or [])] + return argv diff --git a/src/reverse_api/agent_browser_mcp/README.md b/src/reverse_api/agent_browser_mcp/README.md new file mode 100644 index 0000000..cb84f8c --- /dev/null +++ b/src/reverse_api/agent_browser_mcp/README.md @@ -0,0 +1,37 @@ +# RAE bundled MCP for Vercel `agent-browser` + +This directory hosts a minimal **stdio MCP server** (`server.mjs`) that proxies [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) subprocesses so every supported SDK (`claude`, `opencode`, `copilot`, `cursor`) can drive the browser with the **same toolchain**. + +## Setup (every machine / after `pip install` upgrades) + +```bash +npm install --prefix "$(python -c 'from pathlib import Path; import reverse_api; print(Path(reverse_api.__file__).parent / \"agent_browser_mcp\")')" +npm install -g agent-browser +agent-browser install +# Linux VPS without desktop deps yet: +agent-browser install --with-deps +``` + +Smoke-test MCP wiring inside reverse-api-engineer: + +```bash +reverse-api-engineer agent --dry-run --json | jq '.checks[] | select(.name=="agent-browser:MCP-bundle")' +``` + +## Why this exists + +Agent-browser targets **AI workloads**: accessibility snapshots (`@eN`), batch command mode, guarded sessions, CLI-native HAR export (`network har start|stop`). That aligns with RAE’s need for repeatable **headless** capture paths on VPS/CI shells where Playwright-heavy stacks are inconvenient. + +Third-party MCP bridges exist on npm, but none guaranteed RAE-compatible HAR filenames/paths plus prompt/tool naming parity — so we maintain a deliberately small in-tree adapter. + +## Roadmap + +| Stage | Goal | +|-------|------| +| **Now** | `agent_provider: "agent-browser"` + bundled MCP shim + mirrored tool names (`browser_navigate`, …) | +| **Next** | Optional publish to npm (`rae-agent-browser-mcp`) for faster cold starts (`npx` cache) outside the repo | +| **Later** | Optional extra tools (`batch`, annotated screenshots, tab routing) behind feature flags | + +## Development + +Requires Node ≥ 18 (`@modelcontextprotocol/sdk` pulls `zod`). After editing `server.mjs`, restart any agent sessions so MCP processes pick up changes. diff --git a/src/reverse_api/agent_browser_mcp/package-lock.json b/src/reverse_api/agent_browser_mcp/package-lock.json new file mode 100644 index 0000000..d3e454e --- /dev/null +++ b/src/reverse_api/agent_browser_mcp/package-lock.json @@ -0,0 +1,1159 @@ +{ + "name": "rae-agent-browser-mcp-bundle", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rae-agent-browser-mcp-bundle", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^3.25.76" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.22", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.22.tgz", + "integrity": "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/src/reverse_api/agent_browser_mcp/package.json b/src/reverse_api/agent_browser_mcp/package.json new file mode 100644 index 0000000..515eaab --- /dev/null +++ b/src/reverse_api/agent_browser_mcp/package.json @@ -0,0 +1,14 @@ +{ + "name": "rae-agent-browser-mcp-bundle", + "version": "1.0.0", + "private": true, + "description": "Stdio MCP server that wraps Vercel agent-browser for reverse-api-engineer agent mode.", + "type": "module", + "engines": { + "node": ">=18.17" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^3.25.76" + } +} diff --git a/src/reverse_api/agent_browser_mcp/server.mjs b/src/reverse_api/agent_browser_mcp/server.mjs new file mode 100644 index 0000000..4090606 --- /dev/null +++ b/src/reverse_api/agent_browser_mcp/server.mjs @@ -0,0 +1,279 @@ +/** + * Stdio MCP server: proxies RAE agent mode to Vercel agent-browser (Rust CLI via npx). + * Logs diagnostics to stderr only; stdout is reserved for MCP. + */ +import { spawnSync } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { dirname } from "node:path"; + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import * as z from "zod"; + +function parseArgs(argv) { + let harOut = ""; + let session = ""; + let headed = false; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--har-out") harOut = argv[++i] ?? ""; + else if (a === "--session") session = argv[++i] ?? ""; + else if (a === "--headed") headed = true; + } + return { harOut, session, headed }; +} + +function agentBrowserArgv(globalHeaded, sub) { + const g = [...(globalHeaded ? ["--headed"] : []), "--json"]; + return ["-y", "agent-browser@latest", ...g, ...sub]; +} + +function runAb(globalHeaded, subcommandArgs) { + return spawnSync("npx", agentBrowserArgv(globalHeaded, subcommandArgs), { + encoding: "utf8", + maxBuffer: 50 * 1024 * 1024, + env: { ...process.env }, + }); +} + +function asToolResult(r) { + const text = (r.stdout || "").trim() || (r.stderr || "").trim() || "(no output)"; + if (r.status !== 0 && r.status != null) { + return { content: [{ type: "text", text }], isError: true }; + } + return { content: [{ type: "text", text }] }; +} + +const { harOut, session, headed } = parseArgs(process.argv.slice(2)); + +if (!harOut || !session) { + console.error("rae-agent-browser-mcp: missing --har-out or --session"); + process.exit(2); +} + +process.env.AGENT_BROWSER_SESSION = session; +mkdirSync(dirname(harOut), { recursive: true }); + +const boot = runAb(headed, ["network", "har", "start"]); +if (boot.status !== 0) { + console.error( + "rae-agent-browser-mcp: warning: network har start failed (continuing)", + boot.stderr || boot.stdout, + ); +} + +const mcpServer = new McpServer({ + name: "rae-agent-browser", + version: "1.0.0", +}); + +const globalHeaded = headed; + +mcpServer.registerTool( + "browser_navigate", + { + description: "Open URL in the controlled browser session (agent-browser open). HTTPS is inferred when no scheme is given.", + inputSchema: { + url: z.string().describe("Target URL or hostname"), + }, + }, + async ({ url }) => asToolResult(runAb(globalHeaded, ["open", url])), +); + +mcpServer.registerTool( + "browser_snapshot", + { + description: "Accessibility tree snapshot with @eN refs for clicks/fills. Prefer interactive-only (-i) to save tokens.", + inputSchema: { + interactive_only: z + .boolean() + .optional() + .describe("When true (default), only interactive controls are returned."), + }, + }, + async ({ interactive_only = true }) => { + const args = interactive_only ? ["snapshot", "-i"] : ["snapshot"]; + return asToolResult(runAb(globalHeaded, args)); + }, +); + +mcpServer.registerTool( + "browser_click", + { + description: "Click an element by @eN ref or CSS selector.", + inputSchema: { + element: z.string().describe("Selector or @ref from snapshot"), + }, + }, + async ({ element }) => asToolResult(runAb(globalHeaded, ["click", element])), +); + +mcpServer.registerTool( + "browser_fill", + { + description: "Clear and fill an input or textarea (@ref or selector).", + inputSchema: { + element: z.string(), + text: z.string(), + }, + }, + async ({ element, text }) => + asToolResult(runAb(globalHeaded, ["fill", element, text])), +); + +mcpServer.registerTool( + "browser_type", + { + description: "Type into an element without clearing existing value.", + inputSchema: { + element: z.string(), + text: z.string(), + }, + }, + async ({ element, text }) => + asToolResult(runAb(globalHeaded, ["type", element, text])), +); + +mcpServer.registerTool( + "browser_press_key", + { + description: 'Press a key (e.g. Enter, Tab, Control+a).', + inputSchema: { + key: z.string(), + }, + }, + async ({ key }) => asToolResult(runAb(globalHeaded, ["press", key])), +); + +mcpServer.registerTool( + "browser_wait_for", + { + description: + "Wait for a CSS selector visibility, elapsed milliseconds, or page text (--text). Exactly one mode should be supplied.", + inputSchema: { + selector: z.string().optional(), + milliseconds: z.number().int().positive().optional(), + text: z.string().optional(), + }, + }, + async ({ selector, milliseconds, text }) => { + const modes = [selector != null && selector !== "", milliseconds != null, text != null && text !== ""].filter(Boolean); + if (modes.length !== 1) { + return { + content: [ + { + type: "text", + text: "Provide exactly one of: selector, milliseconds, or text", + }, + ], + isError: true, + }; + } + if (milliseconds != null) { + return asToolResult(runAb(globalHeaded, ["wait", String(milliseconds)])); + } + if (text) { + return asToolResult(runAb(globalHeaded, ["wait", "--text", text])); + } + return asToolResult(runAb(globalHeaded, ["wait", selector])); + }, +); + +mcpServer.registerTool( + "browser_scroll", + { + description: "Scroll the page (up, down, left, right). Optional pixels (default sensible). Optional --selector scope.", + inputSchema: { + direction: z.enum(["up", "down", "left", "right"]), + pixels: z.number().int().positive().optional(), + selector: z.string().optional(), + }, + }, + async ({ direction, pixels, selector }) => { + const args = ["scroll", direction]; + if (pixels != null) args.push(String(pixels)); + if (selector) { + args.push("--selector", selector); + } + return asToolResult(runAb(globalHeaded, args)); + }, +); + +mcpServer.registerTool( + "browser_evaluate", + { + description: "Evaluate JavaScript in the page context (agent-browser eval).", + inputSchema: { + script: z.string(), + }, + }, + async ({ script }) => + asToolResult(runAb(globalHeaded, ["eval", script])), +); + +mcpServer.registerTool( + "browser_take_screenshot", + { + description: "Save a screenshot (--full optional). Omit path for a temp file chosen by agent-browser.", + inputSchema: { + path: z.string().optional(), + full_page: z.boolean().optional(), + }, + }, + async ({ path, full_page = false }) => { + const args = ["screenshot"]; + if (full_page) args.push("--full"); + if (path) args.push(path); + return asToolResult(runAb(globalHeaded, args)); + }, +); + +mcpServer.registerTool( + "browser_network_requests", + { + description: "Inspect captured requests (XHR/fetch/etc.) with optional filter; add clear=true to reset the buffer.", + inputSchema: { + filter: z.string().optional(), + clear: z.boolean().optional(), + }, + }, + async ({ filter, clear }) => { + const args = ["network", "requests"]; + if (filter) { + args.push("--filter", filter); + } + if (clear) { + args.push("--clear"); + } + return asToolResult(runAb(globalHeaded, args)); + }, +); + +mcpServer.registerTool( + "browser_close", + { + description: + "Stop HAR recording to the configured RAE recording path, close the browser, and finish capture for reverse engineering.", + inputSchema: {}, + }, + async () => { + const stop = runAb(globalHeaded, ["network", "har", "stop", harOut]); + const clo = runAb(globalHeaded, ["close"]); + const msg = [asToolResult(stop).content?.[0]?.text, asToolResult(clo).content?.[0]?.text].join("\n---\n"); + const failed = (stop.status !== 0 && stop.status != null) || (clo.status !== 0 && clo.status != null); + if (failed) { + return { content: [{ type: "text", text: msg }], isError: true }; + } + return { content: [{ type: "text", text: msg }] }; + }, +); + +async function main() { + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); +} + +main().catch((err) => { + console.error("rae-agent-browser-mcp:", err); + process.exit(1); +}); diff --git a/src/reverse_api/auto_engineer.py b/src/reverse_api/auto_engineer.py index 05191f8..57a39ba 100644 --- a/src/reverse_api/auto_engineer.py +++ b/src/reverse_api/auto_engineer.py @@ -15,6 +15,7 @@ ToolPermissionContext, ) +from .agent_browser_bundle import agent_browser_bundle_error, agent_browser_stdio_mcp_config from .engineer import ClaudeEngineer from .opencode_engineer import OpenCodeEngineer, debug_log, format_error from .utils import get_har_dir @@ -73,7 +74,7 @@ def _build_auto_prompts(self) -> tuple[str, str]: browser_tool_label = ( "Chrome DevTools MCP" if self.agent_provider == "chrome-mcp" - else "MCP" + else ("agent-browser (Vercel) via MCP" if self.agent_provider == "agent-browser" else "MCP") ) system_prompt = load( @@ -84,11 +85,12 @@ def _build_auto_prompts(self) -> tuple[str, str]: output_files=output_files, ) - template = ( - "auto/user_chrome_mcp" - if self.agent_provider == "chrome-mcp" - else "auto/user_playwright" - ) + if self.agent_provider == "chrome-mcp": + template = "auto/user_chrome_mcp" + elif self.agent_provider == "agent-browser": + template = "auto/user_agent_browser" + else: + template = "auto/user_playwright" template_kwargs = { "prompt": self.prompt, @@ -122,6 +124,12 @@ def _get_mcp_config(self) -> tuple[str, dict]: debugging server, so it is dropped in headless mode and the MCP spawns its own headless Chromium instead. """ + if self.agent_provider == "agent-browser": + return agent_browser_stdio_mcp_config( + har_path=self.har_path, + run_id=self.mcp_run_id, + headless=self.headless, + ) if self.agent_provider == "chrome-mcp": args = ["chrome-devtools-mcp@latest", "--no-usage-statistics"] if self.headless: @@ -155,6 +163,13 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.ui.header(self.run_id, self.prompt, self.model, mode="agent") self.ui.start_analysis() + if self.agent_provider == "agent-browser": + berr = agent_browser_bundle_error() + if berr: + self.ui.error(berr) + self.message_store.save_error(berr) + return None + system_prompt, user_message = self._get_active_prompts() self.message_store.save_prompt(user_message) @@ -215,6 +230,9 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: if self.agent_provider == "chrome-mcp": self.ui.console.print("\n[dim]Make sure chrome-devtools-mcp is available: npx chrome-devtools-mcp@latest[/dim]") self.ui.console.print("[dim]Chrome 146+ required with auto-connect enabled at chrome://inspect/#remote-debugging[/dim]") + elif self.agent_provider == "agent-browser": + self.ui.console.print("\n[dim]Bundled MCP: npm install --prefix /agent_browser_mcp[/dim]") + self.ui.console.print("[dim]Global CLI Chromium setup: npm i -g agent-browser && agent-browser install[/dim]") else: self.ui.console.print("\n[dim]Make sure rae-playwright-mcp is installed: npm install -g rae-playwright-mcp[/dim]") else: @@ -253,6 +271,24 @@ def _get_opencode_mcp_config(self) -> dict: so it is dropped in headless mode in favor of an MCP-spawned headless Chromium. """ + if self.agent_provider == "agent-browser": + self.mcp_name = f"agent-browser-{self._session_id}" + from .agent_browser_bundle import agent_browser_command_list + + cmd = agent_browser_command_list( + har_path=self.har_path, + run_id=self.mcp_run_id, + headless=self.headless, + ) + return { + "name": self.mcp_name, + "config": { + "type": "local", + "command": cmd, + "enabled": True, + "timeout": 30000, + }, + } if self.agent_provider == "chrome-mcp": self.mcp_name = f"chrome-devtools-{self._session_id}" cmd = ["npx", "-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"] @@ -295,6 +331,13 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.opencode_ui.header(self.run_id, self.prompt, self.opencode_model, mode="agent") self.opencode_ui.start_analysis() + if self.agent_provider == "agent-browser": + berr = agent_browser_bundle_error() + if berr: + self.opencode_ui.error(berr) + self.message_store.save_error(berr) + return None + system_prompt, user_message = self._get_active_prompts() active_prompt = f"{system_prompt}\n\n{user_message}" self.message_store.save_prompt(user_message) @@ -572,6 +615,28 @@ def on_event(event: Any) -> None: "tools": ["*"], "timeout": 30000, } + elif self.agent_provider == "agent-browser": + berr = agent_browser_bundle_error() + if berr: + eng.ui.error(berr) + eng.message_store.save_error(berr) + return None + + from .agent_browser_bundle import agent_browser_command_list + + argv = agent_browser_command_list( + har_path=self._engineer.har_path, + run_id=self.mcp_run_id, + headless=self.headless, + ) + mcp_server_name = "agent-browser" + mcp_config = { + "type": "local", + "command": argv[0], + "args": argv[1:], + "tools": ["*"], + "timeout": 30000, + } else: mcp_server_name = "playwright" pw_args = [ diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index e769d9d..3fcee65 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -209,6 +209,8 @@ def _build_dry_run_payload( import shutil import subprocess + from .agent_browser_bundle import agent_browser_bundle_error + checks: list[dict] = [] # 1. Prompt @@ -232,13 +234,13 @@ def _build_dry_run_payload( # 3. Agent provider agent_provider = config_manager.get("agent_provider", "auto") - if agent_provider in ("auto", "chrome-mcp"): + if agent_provider in ("auto", "chrome-mcp", "agent-browser"): checks.append({"name": "agent_provider", "status": "ok", "message": agent_provider}) else: checks.append({ "name": "agent_provider", "status": "error", - "message": f"unknown agent_provider {agent_provider!r}; expected 'auto' or 'chrome-mcp'", + "message": f"unknown agent_provider {agent_provider!r}; expected 'auto', 'chrome-mcp', or 'agent-browser'", }) # 4. SDK + API key presence (we only check env var existence, not validity) @@ -264,15 +266,14 @@ def _build_dry_run_payload( else: checks.append({"name": f"sdk:{sdk}", "status": "ok", "message": f"{sdk_env_var} present"}) - # 5. Node.js + npx for MCP servers (both auto and chrome-mcp shell out to - # `npx `; minimal Docker images sometimes ship `node` without - # `npx`, so checking only `node` would lull dry-run into a false ok). + # 5. Node.js + npx for MCP servers (Playwright MCP, bundled agent-browser MCP, + # chrome-mcp wrapper, etc.). node = shutil.which("node") if node is None: checks.append({ "name": "node", "status": "error", - "message": "node not found in PATH; required by both auto (rae-playwright-mcp) and chrome-mcp", + "message": "node not found in PATH; required for MCP-based agent modes", }) else: try: @@ -288,7 +289,7 @@ def _build_dry_run_payload( checks.append({ "name": "npx", "status": "error", - "message": "npx not found in PATH; both MCP servers are launched via `npx `", + "message": "npx not found in PATH; required for MCP-based agent tooling (downloads packages on demand)", }) else: checks.append({"name": "npx", "status": "ok", "message": npx}) @@ -301,7 +302,16 @@ def _build_dry_run_payload( "message": "chrome-mcp without --headless requires Chrome 146+ with auto-connect enabled at chrome://inspect/#remote-debugging — this is not auto-checkable", }) - # 7. Output dir writability — probe with a unique filename so we never + # 7. Bundled MCP for agent-browser + if agent_provider == "agent-browser": + berr = agent_browser_bundle_error() + checks.append({ + "name": "agent-browser:MCP-bundle", + "status": "error" if berr else "ok", + "message": berr or "bundled agent-browser MCP (npm deps in agent_browser_mcp/)", + }) + + # 8. Output dir writability — probe with a unique filename so we never # clobber a real user file (a fixed name like `.dry_run_write_probe` # could legitimately exist in someone's output dir). import secrets @@ -1009,6 +1019,7 @@ def handle_settings(mode_color=THEME_PRIMARY): provider_choices = [ Choice(title="auto (Playwright MCP)", value="auto"), Choice(title="chrome-mcp (Chrome DevTools MCP)", value="chrome-mcp"), + Choice(title="agent-browser (Vercel CLI via bundled MCP)", value="agent-browser"), Choice(title="back", value="back"), ] provider = questionary.select( @@ -1026,7 +1037,18 @@ def handle_settings(mode_color=THEME_PRIMARY): if provider and provider != "back": config_manager.set("agent_provider", provider) console.print(f" [dim]updated[/dim] agent provider: {provider}") - if provider == "chrome-mcp": + if provider == "agent-browser": + from pathlib import Path + + import reverse_api + + bundle = Path(reverse_api.__file__).resolve().parent / "agent_browser_mcp" + console.print() + console.print(f" [dim]install bundled MCP deps (once): npm install --prefix {bundle}[/dim]") + console.print(" [dim]install Chromium helper CLI: npm i -g agent-browser && agent-browser install[/dim]") + console.print(" [dim]Linux servers: agent-browser install --with-deps[/dim]") + console.print() + elif provider == "chrome-mcp": console.print() console.print(" [dim]chrome devtools mcp setup:[/dim]") console.print(" [dim] 1. open chrome (version 146 or newer)[/dim]") @@ -1422,7 +1444,7 @@ def manual(prompt, url, reverse_engineer, model, output_dir): "run_id": "" | null, "prompt": "...", "url": "..." | null, - "mode": "auto" | "chrome-mcp" | null, + "mode": "auto" | "chrome-mcp" | "agent-browser" | null, "har_path": "/abs/path/recording.har" | null, "script_path": "/abs/path/api_client.py" | null, "usage": { "input_tokens": ..., "output_tokens": ..., "total_cost": ... }, @@ -1726,6 +1748,18 @@ def run_auto_capture( console.print(" [dim]auto-connect disabled — mcp will spawn its own headless chrome[/dim]") console.print() + elif agent_provider == "agent-browser": + from pathlib import Path + + import reverse_api + + bundle = Path(reverse_api.__file__).resolve().parent / "agent_browser_mcp" + console.print() + console.print(" [dim]agent-browser: MCP bridge to Vercel's agent-browser CLI (strong default for VPS/headless hosts).[/dim]") + console.print(f" [dim]install MCP bundle deps:[/dim] npm install --prefix [cyan]{bundle}[/cyan]") + console.print(" [dim]install chromium helper:[/dim] npm i -g agent-browser && agent-browser install [dim]# add --with-deps on Linux[/dim]") + console.print() + sdk = config_manager.get("sdk", "claude") if model is None: model = default_model_for_configured_sdk(sdk) @@ -1733,7 +1767,12 @@ def run_auto_capture( run_id = generate_run_id() timestamp = get_timestamp() - mode_label = "chrome-mcp" if agent_provider == "chrome-mcp" else "auto" + if agent_provider == "chrome-mcp": + mode_label = "chrome-mcp" + elif agent_provider == "agent-browser": + mode_label = "agent-browser" + else: + mode_label = "auto" session_manager.add_run( run_id=run_id, prompt=prompt, diff --git a/src/reverse_api/config.py b/src/reverse_api/config.py index 90c108c..93571f8 100644 --- a/src/reverse_api/config.py +++ b/src/reverse_api/config.py @@ -5,7 +5,7 @@ from typing import Any DEFAULT_CONFIG = { - "agent_provider": "auto", # "auto" (Playwright MCP) or "chrome-mcp" (Chrome DevTools MCP) + "agent_provider": "auto", # "auto" | "chrome-mcp" | "agent-browser" "claude_code_model": "claude-sonnet-4-6", "collector_model": "claude-sonnet-4-6", # Model for collector mode "cursor_model": "composer-2", # Model id for Cursor SDK (see Cursor.models.list()) diff --git a/src/reverse_api/cursor_engineer.py b/src/reverse_api/cursor_engineer.py index 5e4e481..0670d73 100644 --- a/src/reverse_api/cursor_engineer.py +++ b/src/reverse_api/cursor_engineer.py @@ -10,6 +10,7 @@ from pathlib import Path from typing import Any +from .agent_browser_bundle import agent_browser_bundle_error from .base_engineer import BaseEngineer from .tui import ClaudeUI @@ -434,6 +435,15 @@ def __init__( self.headless = headless def _cursor_mcp_servers(self) -> dict[str, Any]: + if self.agent_provider == "agent-browser": + from .agent_browser_bundle import agent_browser_stdio_mcp_config + + name, cfg = agent_browser_stdio_mcp_config( + har_path=self.har_path, + run_id=self.mcp_run_id, + headless=self.headless, + ) + return {name: cfg} if self.agent_provider == "chrome-mcp": args = ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"] if self.headless: @@ -483,6 +493,13 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.ui.console.print("\n[dim]Create an API key at https://cursor.com/dashboard/integrations[/dim]") return None + if self.agent_provider == "agent-browser": + berr = agent_browser_bundle_error() + if berr: + self.ui.error(berr) + self.message_store.save_error(berr) + return None + system_prompt, user_message = ClaudeAutoEngineer._build_auto_prompts(self) self.message_store.save_prompt(user_message) combined = f"{system_prompt}\n\n{user_message}" diff --git a/src/reverse_api/prompts/auto/user_agent_browser.md b/src/reverse_api/prompts/auto/user_agent_browser.md new file mode 100644 index 0000000..bc81003 --- /dev/null +++ b/src/reverse_api/prompts/auto/user_agent_browser.md @@ -0,0 +1,37 @@ + +{prompt} + + + +{scripts_dir} + + + +## Workflow + +Uses **Vercel [agent-browser](https://github.com/vercel-labs/agent-browser)** (Rust CLI invoked through MCP). Interaction model: take an accessibility **snapshot** to obtain `@eN` element refs, then **click**, **fill**, or **type** using those refs. Run `snapshot` again after navigations before assuming refs are valid. + +### Phase 1: BROWSE +Use MCP tools (names mirror the Playwright MCP style for familiarity): + +| Tool | Role | +|------|------| +| `browser_navigate` | Open URLs | +| `browser_snapshot` | Accessibility tree (`-i` by default); use refs like `@e1` | +| `browser_click`, `browser_fill`, `browser_type`, `browser_press_key` | Interactions | +| `browser_wait_for` | Wait by selector **or** milliseconds **or** visible text substring | +| `browser_scroll` | Scroll the page | +| `browser_evaluate` | Execute JavaScript in the page | +| `browser_take_screenshot` | Optional visual aid (prefer snapshots to save tokens and avoid large images) | + +### Phase 2: MONITOR +Call `browser_network_requests` periodically. Watch for APIs, tokens, cookies, redirects, CORS quirks, and graph-style endpoints (`/graphql`, protobuf, etc.). + +### Phase 3: CAPTURE +When you have explored enough traffic, call `browser_close` once to **flush HAR JSON** into the canonical path `{har_path}` and shut down Chromium. Omitting this prevents reverse engineering later. + +### Phase 4: REVERSE ENGINEER +Analyze `recording.har` at `{har_path}` and emit the scripted client/source files under `{scripts_dir}` using the codegen rules from the system prompt. + +**Headless VPS tips:** Provider default is usually headless. First-time setups run `npm install -g agent-browser && agent-browser install` so Chrome/Chromium for Testing downloads; Linux may need `agent-browser install --with-deps`. `agent-browser doctor` diagnoses environment issues. + diff --git a/tests/test_agent_browser_bundle.py b/tests/test_agent_browser_bundle.py new file mode 100644 index 0000000..ae75933 --- /dev/null +++ b/tests/test_agent_browser_bundle.py @@ -0,0 +1,35 @@ +"""Tests for bundled agent-browser MCP wiring.""" + +from __future__ import annotations + +from pathlib import Path + +from reverse_api.agent_browser_bundle import agent_browser_command_list, agent_browser_stdio_mcp_config + + +def test_stdio_mcp_config_headless_writes_har_argv(tmp_path: Path): + har = tmp_path / "recording.har" + name, cfg = agent_browser_stdio_mcp_config(har_path=har, run_id="run_xyz", headless=True) + assert name == "agent-browser" + assert cfg["command"] == "node" + assert "--har-out" in cfg["args"] + har_i = cfg["args"].index("--har-out") + assert cfg["args"][har_i + 1] == str(har) + sess_i = cfg["args"].index("--session") + assert cfg["args"][sess_i + 1] == "run_xyz" + assert "--headed" not in cfg["args"] + + +def test_stdio_mcp_config_headed_requests_window(tmp_path: Path): + har = tmp_path / "recording.har" + _name, cfg = agent_browser_stdio_mcp_config(har_path=har, run_id="abc", headless=False) + assert "--headed" in cfg["args"] + + +def test_command_list_matches_stdio_shape(tmp_path: Path): + har = tmp_path / "recording.har" + flat = agent_browser_command_list(har_path=har, run_id="r1", headless=True) + assert flat[0] == "node" + _name, cfg = agent_browser_stdio_mcp_config(har_path=har, run_id="r1", headless=True) + assert flat == [cfg["command"], *cfg["args"]] + diff --git a/tests/test_auto_engineer.py b/tests/test_auto_engineer.py index bd132a4..2f2bd37 100644 --- a/tests/test_auto_engineer.py +++ b/tests/test_auto_engineer.py @@ -238,6 +238,52 @@ def test_get_active_prompts_auto(self, tmp_path): assert "browser_navigate" in user_p +class TestAgentBrowserPrompt: + """Prompts for bundled agent-browser MCP provider.""" + + def _make_engineer(self, tmp_path, **kwargs): + defaults = { + "run_id": "test123", + "prompt": "browse and capture", + "model": "claude-sonnet-4-6", + "output_dir": str(tmp_path), + "agent_provider": "agent-browser", + } + defaults.update(kwargs) + with patch("reverse_api.auto_engineer.get_har_dir", return_value=tmp_path / "har"): + with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"): + with patch("reverse_api.base_engineer.MessageStore"): + return ClaudeAutoEngineer(**defaults) + + def test_system_labels_tooling(self, tmp_path): + eng = self._make_engineer(tmp_path) + sys_p, _user_p = eng._build_auto_prompts() + assert "agent-browser" in sys_p + + def test_user_template_covers_close_and_har(self, tmp_path): + eng = self._make_engineer(tmp_path) + _sys_p, user_p = eng._build_auto_prompts() + assert "browser_close" in user_p + assert "recording.har" in user_p + assert str(eng.har_path) in user_p + + def test_opencode_delegates_prompts(self, tmp_path): + with patch("reverse_api.auto_engineer.get_har_dir", return_value=tmp_path / "har"): + with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"): + with patch("reverse_api.base_engineer.MessageStore"): + with patch("reverse_api.opencode_engineer.OpenCodeUI"): + eng = OpenCodeAutoEngineer( + run_id="test123", + prompt="browse and capture", + output_dir=str(tmp_path), + agent_provider="agent-browser", + opencode_provider="anthropic", + opencode_model="claude-opus-4-6", + ) + sys_p, user_p = eng._get_active_prompts() + assert "agent-browser" in sys_p + + class TestChromeMcpConfig: """Test MCP configuration selection.""" @@ -270,6 +316,21 @@ def test_auto_mcp_config(self, tmp_path): assert "rae-playwright-mcp@latest" in config["args"] assert "--run-id" in config["args"] + def test_agent_browser_mcp_config(self, tmp_path): + """agent-browser bundles node stdio shim with har + session wiring.""" + eng = self._make_engineer(tmp_path, agent_provider="agent-browser") + name, config = eng._get_mcp_config() + assert name == "agent-browser" + assert config["type"] == "stdio" + assert config["command"] == "node" + ss = "--session" + hh = "--har-out" + assert hh in config["args"] and ss in config["args"] + hi = config["args"].index(hh) + si = config["args"].index(ss) + assert config["args"][hi + 1].endswith("recording.har") + assert config["args"][si + 1] == "test123" + class TestOpenCodeChromeMcpConfig: """Test OpenCodeAutoEngineer Chrome MCP config.""" @@ -305,6 +366,16 @@ def test_opencode_auto_mcp_config(self, tmp_path): assert "playwright" in eng.mcp_name assert "rae-playwright-mcp@latest" in config["config"]["command"] + def test_opencode_agent_browser_mcp_config(self, tmp_path): + """OpenCode agent-browser mounts bundled node MCP as a flat command.""" + eng = self._make_engineer(tmp_path, agent_provider="agent-browser") + oc = eng._get_opencode_mcp_config() + assert "agent-browser" in eng.mcp_name + cmd = oc["config"]["command"] + assert cmd[0] == "node" + assert "--har-out" in cmd + assert "--session" in cmd + def test_opencode_chrome_mcp_prompt(self, tmp_path): """OpenCode chrome-mcp uses Chrome DevTools prompt.""" eng = self._make_engineer(tmp_path, agent_provider="chrome-mcp") diff --git a/website/content/docs/cli/scripted-usage.mdx b/website/content/docs/cli/scripted-usage.mdx index c524d06..79b4838 100644 --- a/website/content/docs/cli/scripted-usage.mdx +++ b/website/content/docs/cli/scripted-usage.mdx @@ -55,7 +55,7 @@ reverse-api-engineer run --file api_client.py \ | `run_id` | `string \| null` | Stable id for follow-up `show` / `engineer` / `run` calls. | | `prompt` | `string` | The prompt passed in. | | `url` | `string \| null` | Optional starting URL. | -| `mode` | `string \| null` | Provider used (`"auto"` for Playwright MCP, `"chrome-mcp"`). | +| `mode` | `string \| null` | Provider (`"auto"`, `"chrome-mcp"`, `"agent-browser"`). | | `har_path` | `string \| null` | Absolute path to the captured HAR (`recording.har`). | | `script_path` | `string \| null` | Absolute path to the generated client when reverse engineering ran. | | `usage` | `object` | Normalized token plus cost usage (`input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `total_cost_usd`) plus raw SDK usage under `raw`. | diff --git a/website/content/docs/configuration/agent.mdx b/website/content/docs/configuration/agent.mdx index 361e564..638366e 100644 --- a/website/content/docs/configuration/agent.mdx +++ b/website/content/docs/configuration/agent.mdx @@ -31,6 +31,18 @@ profile. - Node.js 20.19+ - Auto-connect enabled at `chrome://inspect/#remote-debugging` +### `agent-browser` + +Controls **Chrome via [Vercel agent-browser](https://github.com/vercel-labs/agent-browser)** through a bundled MCP shim. Tune this profile when you primarily run on VPS/CI and want deterministic headless tooling without depending on Cursor’s embedded Browser tab. + +**Bootstrap checklist** + +1. `npm install --prefix /reverse_api/agent_browser_mcp` +2. `npm install -g agent-browser && agent-browser install` (`--with-deps` on bare Linux hosts) +3. Set `"agent_provider": "agent-browser"` (settings UI or JSON) + +Operational notes and backlog live in `src/reverse_api/agent_browser_mcp/README.md`. + ## How to switch providers In the CLI: diff --git a/website/content/docs/modes/agent.mdx b/website/content/docs/modes/agent.mdx index d11ba4f..8d99f72 100644 --- a/website/content/docs/modes/agent.mdx +++ b/website/content/docs/modes/agent.mdx @@ -45,7 +45,7 @@ codes). Pick the provider in `/settings` then "agent provider". - + **Playwright MCP** browser automation with your configured SDK. Combines browser control and real-time reverse-engineering in a single workflow. @@ -62,6 +62,16 @@ Pick the provider in `/settings` then "agent provider". - Node.js 20.19+ - Auto-connect enabled at `chrome://inspect/#remote-debugging` + + **Vercel [agent-browser](https://github.com/vercel-labs/agent-browser)** via a + small bundled MCP shim. Targets headless VPS/CI workflows: installs its own + Chromium channel through `npm i -g agent-browser && agent-browser install` + (`--with-deps` on Linux) and persists traffic to `recording.har`. + + Requires **npm install** inside `reverse_api/agent_browser_mcp/` once per + install (CLI prints the path). See **`src/reverse_api/agent_browser_mcp/README.md`** + in this repository for the checklist and roadmap notes. + ## Reasoning model From 4b12721235fc340e615cf33ca5b7edb51e8c56cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 24 May 2026 00:19:14 +0000 Subject: [PATCH 2/4] refactor(agent-browser): CLI prefetch and prompts instead of MCP shim - Add agent_browser module: npx package resolution, prefetch check, prompts and Claude allow-list tools (Bash plus file/read tools). - Claude/OpenCode/Copilot/Cursor flows skip bundled browser MCP for agent-browser; OpenCode chrome-mcp config uses a dedicated branch again. - Remove agent_browser_bundle and agent_browser_mcp shim; tighten docs, changelog, website, and README. - Fix TestAgentBrowserPrompt mocks (patch lifetime) and extend config tests. Co-authored-by: kalil0321 --- .gitignore | 3 +- CHANGELOG.md | 2 +- README.md | 12 +- pyproject.toml | 2 - src/reverse_api/agent_browser.py | 117 ++ src/reverse_api/agent_browser_bundle.py | 60 - src/reverse_api/agent_browser_mcp/README.md | 37 - .../agent_browser_mcp/package-lock.json | 1159 ----------------- .../agent_browser_mcp/package.json | 14 - src/reverse_api/agent_browser_mcp/server.mjs | 279 ---- src/reverse_api/auto_engineer.py | 178 ++- src/reverse_api/cli.py | 39 +- src/reverse_api/config.py | 2 + src/reverse_api/cursor_engineer.py | 21 +- .../prompts/auto/user_agent_browser.md | 64 +- tests/test_agent_browser.py | 72 + tests/test_agent_browser_bundle.py | 35 - tests/test_auto_engineer.py | 88 +- tests/test_config.py | 2 + website/content/docs/configuration/agent.mdx | 23 +- website/content/docs/modes/agent.mdx | 12 +- 21 files changed, 413 insertions(+), 1808 deletions(-) create mode 100644 src/reverse_api/agent_browser.py delete mode 100644 src/reverse_api/agent_browser_bundle.py delete mode 100644 src/reverse_api/agent_browser_mcp/README.md delete mode 100644 src/reverse_api/agent_browser_mcp/package-lock.json delete mode 100644 src/reverse_api/agent_browser_mcp/package.json delete mode 100644 src/reverse_api/agent_browser_mcp/server.mjs create mode 100644 tests/test_agent_browser.py delete mode 100644 tests/test_agent_browser_bundle.py diff --git a/.gitignore b/.gitignore index 22f4a7f..988caf3 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,5 @@ store-assets/ .playwright-mcp/ -# Node deps for bundled MCP shims (installed per README; never commit) src/reverse_api/cursor_bridge/node_modules/ -src/reverse_api/agent_browser_mcp/node_modules/ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 498fcb5..6e56a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **`agent_provider: "agent-browser"`**: MCP bridge bundled under `reverse_api/agent_browser_mcp/` that shells Vercel’s `agent-browser` CLI (via `npx`) for VPS/CI-friendly headless capture with HAR export to `recording.har`; see `src/reverse_api/agent_browser_mcp/README.md` for rollout notes. +- **`agent_provider: "agent-browser"`**: Prompt-only CLI mode—agents run **`npx -y … agent-browser`** (plus bundled **skills**) from their shell tooling; Reverse API Engineer prefetch-checks **`npx`**, skips browser MCP surfaces, exposes optional `agent_browser_npx_package` / `agent_browser_notes` knobs. ### Added - **Cursor SDK support**: Added `sdk=cursor` / `--sdk cursor` engineering support through a bundled Node bridge around the Cursor TypeScript SDK. Cursor runs use the configured Cursor model (default `composer-2`), accept MCP server configuration, resume Cursor agents across follow-up turns, and normalize streamed tool output plus token usage into the existing TUI/message-store flow diff --git a/README.md b/README.md index 4ba85aa..f3a16a3 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,14 @@ Cycle modes with **Shift+Tab**: Agent mode providers: - **auto** (default): Playwright MCP, single workflow for browsing + reverse engineering. - **chrome-mcp**: drives your real Chrome so you keep existing sessions/cookies. Requires Chrome 146+ and Node.js 20.19+. -- **agent-browser**: MCP bridge around [Vercel agent-browser](https://github.com/vercel-labs/agent-browser)—great for VPS/CI/headless where you want snapshots + refs without Playwright MCP. Requires **Node**, **npx**, the bundled MCP dependencies (`npm install --prefix …/agent_browser_mcp`), and **`agent-browser install`** for Chromium. See **`src/reverse_api/agent_browser_mcp/README.md`** in this repo for the checklist and roadmap. +- **agent-browser**: [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) as a **pure CLI**. No bundled browser MCP—the agent shells `npx -y …`, loads upstream **skills**, and captures HAR manually. Strong default for VPS/CI. Tune with `agent_browser_npx_package`, `agent_browser_notes` (optional), env `RAE_AGENT_BROWSER_PACKAGE` / `RAE_AGENT_BROWSER_NOTES`. First-time Chromium bootstrap: `npx -y agent-browser install` (`--with-deps` on bare Linux). -After installing/upgrading the Python package: + +Optionally prefetch for faster runs: ```bash -npm install --prefix "$(python -c 'from pathlib import Path; import reverse_api; print(Path(reverse_api.__file__).parent / \"agent_browser_mcp\")')" -npm install -g agent-browser && agent-browser install -# Linux VPS: append --with-deps to the installer when prompted +npx -y agent-browser@0 --help >/dev/null +npx -y agent-browser@0 doctor --offline --quick || true ``` ## Configuration @@ -79,6 +79,8 @@ Settings live in `~/.reverse-api/config.json` and can be edited via `/settings` ```json { "agent_provider": "auto", + "agent_browser_npx_package": "agent-browser@0", + "agent_browser_notes": "", "claude_code_model": "claude-sonnet-4-6", "collector_model": "claude-sonnet-4-6", "opencode_model": "claude-sonnet-4-6", diff --git a/pyproject.toml b/pyproject.toml index a09c686..2be5581 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ allow-direct-references = true packages = ["src/reverse_api"] exclude = [ "src/reverse_api/cursor_bridge/node_modules/**", - "src/reverse_api/agent_browser_mcp/node_modules/**", ] [tool.hatch.build.targets.sdist] @@ -74,7 +73,6 @@ exclude = [ ".claude/worktrees/**", ".claude/plans/**", "src/reverse_api/cursor_bridge/node_modules/**", - "src/reverse_api/agent_browser_mcp/node_modules/**", ] [dependency-groups] diff --git a/src/reverse_api/agent_browser.py b/src/reverse_api/agent_browser.py new file mode 100644 index 0000000..66ef38d --- /dev/null +++ b/src/reverse_api/agent_browser.py @@ -0,0 +1,117 @@ +"""Helpers for agent-browser provider: bootstrap + prompt context. + +This mode avoids a custom MCP shim. Agents drive Vercel's ``agent-browser`` CLI directly +via their shell tooling (e.g. Bash), after we verify ``npx`` can fetch the CLI. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from typing import Any + +from .utils import get_config_path + +_AGENT_BROWSER_TOOLS = frozenset( + { + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "Bash", + "WebFetch", + "WebSearch", + "AskUserQuestion", + }, +) + + +def _config_manager_snapshot() -> dict[str, Any]: + """Load config defaults merged with ~/.reverse-api/config.json.""" + try: + from .config import ConfigManager + + cm = ConfigManager(get_config_path()) + return cm.config.copy() + except Exception: + return {} + + +def agent_browser_npx_package() -> str: + """Pinned npm specifier passed to ``npx -y `` (override with ``RAE_AGENT_BROWSER_PACKAGE``).""" + + env = os.environ.get("RAE_AGENT_BROWSER_PACKAGE", "").strip() + if env: + return env + cfg = _config_manager_snapshot() + return str(cfg.get("agent_browser_npx_package") or "agent-browser@0") + + +def agent_browser_extra_notes() -> str: + """Optional user guidance (cloud browsers, corp proxy, …) from ``agent_browser_notes``.""" + + txt = (_config_manager_snapshot().get("agent_browser_notes") or "").strip() + if txt: + return txt + return os.environ.get("RAE_AGENT_BROWSER_NOTES", "").strip() + + +def ensure_agent_browser_runtime() -> str | None: + """Return None if agent-browser CLI is fetchable via npx; otherwise a short error.""" + + if shutil.which("node") is None: + return "node not found in PATH (needed to run agent-browser via npx)." + if shutil.which("npx") is None: + return "npx not found in PATH (needed to bootstrap agent-browser)." + pkg = agent_browser_npx_package() + try: + proc = subprocess.run( + ["npx", "-y", pkg, "--help"], + capture_output=True, + text=True, + timeout=240, + check=False, + ) + except subprocess.TimeoutExpired: + return f"timed out prefetching `{pkg}` with npx (network?)." + except OSError as e: + return f"failed to run npx: {e}" + + stderr = (proc.stderr or "").strip() + stdout = (proc.stdout or "").strip() + if proc.returncode == 0: + return None + + err_blob = stderr or stdout or "(no output)" + err_blob = err_blob[:1600] + hint = "`RAE_AGENT_BROWSER_PACKAGE` env or config `agent_browser_npx_package` can pin/coerce versions." + return ( + f"npx prefetch of `{pkg}` failed (exit {proc.returncode}). Output: {err_blob} " + f"Try `npm i -g agent-browser && agent-browser install`, or adjust {hint}" + ) + + +def allowed_tools_agent_browser_agent_mode() -> list[str]: + """Tool allow-list for Claude auto engineer when MCP browser is omitted.""" + + return sorted(_AGENT_BROWSER_TOOLS) + + +def agent_browser_prompt_fields(*, run_id: str, headless: bool) -> dict[str, str]: + """Variables for ``prompts/auto/user_agent_browser.md``.""" + + pkg = agent_browser_npx_package() + session = f"rae-{run_id}" + headed = "" if headless else "Use the global `--headed` flag on subcommands that show a window when you need a visible browser (local debugging only).\n\n" + notes = agent_browser_extra_notes() + notes_block = "" + if notes: + notes_block = f"\n## Extra operator notes (from config or RAE_AGENT_BROWSER_NOTES)\n\n{notes}\n" + return { + "agent_browser_npx_package": pkg, + "agent_browser_session": session, + "agent_browser_headed_hint": headed, + "agent_browser_notes_block": notes_block, + } diff --git a/src/reverse_api/agent_browser_bundle.py b/src/reverse_api/agent_browser_bundle.py deleted file mode 100644 index 32dd6c7..0000000 --- a/src/reverse_api/agent_browser_bundle.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Bundled stdio MCP server for Vercel agent-browser (used by agent-provider ``agent-browser``).""" - -from __future__ import annotations - -import shutil -from pathlib import Path -from typing import Any - -_AGENT_BROWSER_HOME = Path(__file__).resolve().parent / "agent_browser_mcp" -_SERVER_JS = _AGENT_BROWSER_HOME / "server.mjs" -_MCP_MARKER = _AGENT_BROWSER_HOME / "node_modules" / "@modelcontextprotocol" - - -def bundled_agent_browser_mcp_server_js() -> Path: - return _SERVER_JS - - -def agent_browser_bundle_error() -> str | None: - """Return a user-facing setup message if bundled MCP deps are missing.""" - - if not _SERVER_JS.is_file(): - return f"Bundled agent-browser MCP is missing {_SERVER_JS.name} (broken install)." - if not _MCP_MARKER.is_dir(): - return ( - "agent-browser mode requires MCP bundle dependencies: " - f"npm install --prefix {_AGENT_BROWSER_HOME}" - ) - if not shutil.which("node"): - return "node executable not found in PATH (required for agent-browser MCP)." - if not shutil.which("npx"): - return "npx not found in PATH (downloads agent-browser on demand via MCP tools)." - return None - - -def agent_browser_stdio_mcp_config(*, har_path: Path, run_id: str, headless: bool) -> tuple[str, dict[str, Any]]: - """Return (server_name, mcp_servers entry) compatible with ClaudeAgentOptions.mcp_servers.""" - - cmd = bundled_agent_browser_mcp_server_js() - args = [ - str(cmd), - "--har-out", - str(har_path), - "--session", - run_id, - ] - if not headless: - args.append("--headed") - return "agent-browser", { - "type": "stdio", - "command": "node", - "args": args, - } - - -def agent_browser_command_list(*, har_path: Path, run_id: str, headless: bool) -> list[str]: - """Flat CLI command list used by OpenCode (``config.command`` expects argv).""" - - _name, cfg = agent_browser_stdio_mcp_config(har_path=har_path, run_id=run_id, headless=headless) - argv: list[str] = [cfg["command"], *(cfg["args"] or [])] - return argv diff --git a/src/reverse_api/agent_browser_mcp/README.md b/src/reverse_api/agent_browser_mcp/README.md deleted file mode 100644 index cb84f8c..0000000 --- a/src/reverse_api/agent_browser_mcp/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# RAE bundled MCP for Vercel `agent-browser` - -This directory hosts a minimal **stdio MCP server** (`server.mjs`) that proxies [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) subprocesses so every supported SDK (`claude`, `opencode`, `copilot`, `cursor`) can drive the browser with the **same toolchain**. - -## Setup (every machine / after `pip install` upgrades) - -```bash -npm install --prefix "$(python -c 'from pathlib import Path; import reverse_api; print(Path(reverse_api.__file__).parent / \"agent_browser_mcp\")')" -npm install -g agent-browser -agent-browser install -# Linux VPS without desktop deps yet: -agent-browser install --with-deps -``` - -Smoke-test MCP wiring inside reverse-api-engineer: - -```bash -reverse-api-engineer agent --dry-run --json | jq '.checks[] | select(.name=="agent-browser:MCP-bundle")' -``` - -## Why this exists - -Agent-browser targets **AI workloads**: accessibility snapshots (`@eN`), batch command mode, guarded sessions, CLI-native HAR export (`network har start|stop`). That aligns with RAE’s need for repeatable **headless** capture paths on VPS/CI shells where Playwright-heavy stacks are inconvenient. - -Third-party MCP bridges exist on npm, but none guaranteed RAE-compatible HAR filenames/paths plus prompt/tool naming parity — so we maintain a deliberately small in-tree adapter. - -## Roadmap - -| Stage | Goal | -|-------|------| -| **Now** | `agent_provider: "agent-browser"` + bundled MCP shim + mirrored tool names (`browser_navigate`, …) | -| **Next** | Optional publish to npm (`rae-agent-browser-mcp`) for faster cold starts (`npx` cache) outside the repo | -| **Later** | Optional extra tools (`batch`, annotated screenshots, tab routing) behind feature flags | - -## Development - -Requires Node ≥ 18 (`@modelcontextprotocol/sdk` pulls `zod`). After editing `server.mjs`, restart any agent sessions so MCP processes pick up changes. diff --git a/src/reverse_api/agent_browser_mcp/package-lock.json b/src/reverse_api/agent_browser_mcp/package-lock.json deleted file mode 100644 index d3e454e..0000000 --- a/src/reverse_api/agent_browser_mcp/package-lock.json +++ /dev/null @@ -1,1159 +0,0 @@ -{ - "name": "rae-agent-browser-mcp-bundle", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "rae-agent-browser-mcp-bundle", - "version": "1.0.0", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "zod": "^3.25.76" - }, - "engines": { - "node": ">=18.17" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.22", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.22.tgz", - "integrity": "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } -} diff --git a/src/reverse_api/agent_browser_mcp/package.json b/src/reverse_api/agent_browser_mcp/package.json deleted file mode 100644 index 515eaab..0000000 --- a/src/reverse_api/agent_browser_mcp/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "rae-agent-browser-mcp-bundle", - "version": "1.0.0", - "private": true, - "description": "Stdio MCP server that wraps Vercel agent-browser for reverse-api-engineer agent mode.", - "type": "module", - "engines": { - "node": ">=18.17" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "zod": "^3.25.76" - } -} diff --git a/src/reverse_api/agent_browser_mcp/server.mjs b/src/reverse_api/agent_browser_mcp/server.mjs deleted file mode 100644 index 4090606..0000000 --- a/src/reverse_api/agent_browser_mcp/server.mjs +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Stdio MCP server: proxies RAE agent mode to Vercel agent-browser (Rust CLI via npx). - * Logs diagnostics to stderr only; stdout is reserved for MCP. - */ -import { spawnSync } from "node:child_process"; -import { mkdirSync } from "node:fs"; -import { dirname } from "node:path"; - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import * as z from "zod"; - -function parseArgs(argv) { - let harOut = ""; - let session = ""; - let headed = false; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === "--har-out") harOut = argv[++i] ?? ""; - else if (a === "--session") session = argv[++i] ?? ""; - else if (a === "--headed") headed = true; - } - return { harOut, session, headed }; -} - -function agentBrowserArgv(globalHeaded, sub) { - const g = [...(globalHeaded ? ["--headed"] : []), "--json"]; - return ["-y", "agent-browser@latest", ...g, ...sub]; -} - -function runAb(globalHeaded, subcommandArgs) { - return spawnSync("npx", agentBrowserArgv(globalHeaded, subcommandArgs), { - encoding: "utf8", - maxBuffer: 50 * 1024 * 1024, - env: { ...process.env }, - }); -} - -function asToolResult(r) { - const text = (r.stdout || "").trim() || (r.stderr || "").trim() || "(no output)"; - if (r.status !== 0 && r.status != null) { - return { content: [{ type: "text", text }], isError: true }; - } - return { content: [{ type: "text", text }] }; -} - -const { harOut, session, headed } = parseArgs(process.argv.slice(2)); - -if (!harOut || !session) { - console.error("rae-agent-browser-mcp: missing --har-out or --session"); - process.exit(2); -} - -process.env.AGENT_BROWSER_SESSION = session; -mkdirSync(dirname(harOut), { recursive: true }); - -const boot = runAb(headed, ["network", "har", "start"]); -if (boot.status !== 0) { - console.error( - "rae-agent-browser-mcp: warning: network har start failed (continuing)", - boot.stderr || boot.stdout, - ); -} - -const mcpServer = new McpServer({ - name: "rae-agent-browser", - version: "1.0.0", -}); - -const globalHeaded = headed; - -mcpServer.registerTool( - "browser_navigate", - { - description: "Open URL in the controlled browser session (agent-browser open). HTTPS is inferred when no scheme is given.", - inputSchema: { - url: z.string().describe("Target URL or hostname"), - }, - }, - async ({ url }) => asToolResult(runAb(globalHeaded, ["open", url])), -); - -mcpServer.registerTool( - "browser_snapshot", - { - description: "Accessibility tree snapshot with @eN refs for clicks/fills. Prefer interactive-only (-i) to save tokens.", - inputSchema: { - interactive_only: z - .boolean() - .optional() - .describe("When true (default), only interactive controls are returned."), - }, - }, - async ({ interactive_only = true }) => { - const args = interactive_only ? ["snapshot", "-i"] : ["snapshot"]; - return asToolResult(runAb(globalHeaded, args)); - }, -); - -mcpServer.registerTool( - "browser_click", - { - description: "Click an element by @eN ref or CSS selector.", - inputSchema: { - element: z.string().describe("Selector or @ref from snapshot"), - }, - }, - async ({ element }) => asToolResult(runAb(globalHeaded, ["click", element])), -); - -mcpServer.registerTool( - "browser_fill", - { - description: "Clear and fill an input or textarea (@ref or selector).", - inputSchema: { - element: z.string(), - text: z.string(), - }, - }, - async ({ element, text }) => - asToolResult(runAb(globalHeaded, ["fill", element, text])), -); - -mcpServer.registerTool( - "browser_type", - { - description: "Type into an element without clearing existing value.", - inputSchema: { - element: z.string(), - text: z.string(), - }, - }, - async ({ element, text }) => - asToolResult(runAb(globalHeaded, ["type", element, text])), -); - -mcpServer.registerTool( - "browser_press_key", - { - description: 'Press a key (e.g. Enter, Tab, Control+a).', - inputSchema: { - key: z.string(), - }, - }, - async ({ key }) => asToolResult(runAb(globalHeaded, ["press", key])), -); - -mcpServer.registerTool( - "browser_wait_for", - { - description: - "Wait for a CSS selector visibility, elapsed milliseconds, or page text (--text). Exactly one mode should be supplied.", - inputSchema: { - selector: z.string().optional(), - milliseconds: z.number().int().positive().optional(), - text: z.string().optional(), - }, - }, - async ({ selector, milliseconds, text }) => { - const modes = [selector != null && selector !== "", milliseconds != null, text != null && text !== ""].filter(Boolean); - if (modes.length !== 1) { - return { - content: [ - { - type: "text", - text: "Provide exactly one of: selector, milliseconds, or text", - }, - ], - isError: true, - }; - } - if (milliseconds != null) { - return asToolResult(runAb(globalHeaded, ["wait", String(milliseconds)])); - } - if (text) { - return asToolResult(runAb(globalHeaded, ["wait", "--text", text])); - } - return asToolResult(runAb(globalHeaded, ["wait", selector])); - }, -); - -mcpServer.registerTool( - "browser_scroll", - { - description: "Scroll the page (up, down, left, right). Optional pixels (default sensible). Optional --selector scope.", - inputSchema: { - direction: z.enum(["up", "down", "left", "right"]), - pixels: z.number().int().positive().optional(), - selector: z.string().optional(), - }, - }, - async ({ direction, pixels, selector }) => { - const args = ["scroll", direction]; - if (pixels != null) args.push(String(pixels)); - if (selector) { - args.push("--selector", selector); - } - return asToolResult(runAb(globalHeaded, args)); - }, -); - -mcpServer.registerTool( - "browser_evaluate", - { - description: "Evaluate JavaScript in the page context (agent-browser eval).", - inputSchema: { - script: z.string(), - }, - }, - async ({ script }) => - asToolResult(runAb(globalHeaded, ["eval", script])), -); - -mcpServer.registerTool( - "browser_take_screenshot", - { - description: "Save a screenshot (--full optional). Omit path for a temp file chosen by agent-browser.", - inputSchema: { - path: z.string().optional(), - full_page: z.boolean().optional(), - }, - }, - async ({ path, full_page = false }) => { - const args = ["screenshot"]; - if (full_page) args.push("--full"); - if (path) args.push(path); - return asToolResult(runAb(globalHeaded, args)); - }, -); - -mcpServer.registerTool( - "browser_network_requests", - { - description: "Inspect captured requests (XHR/fetch/etc.) with optional filter; add clear=true to reset the buffer.", - inputSchema: { - filter: z.string().optional(), - clear: z.boolean().optional(), - }, - }, - async ({ filter, clear }) => { - const args = ["network", "requests"]; - if (filter) { - args.push("--filter", filter); - } - if (clear) { - args.push("--clear"); - } - return asToolResult(runAb(globalHeaded, args)); - }, -); - -mcpServer.registerTool( - "browser_close", - { - description: - "Stop HAR recording to the configured RAE recording path, close the browser, and finish capture for reverse engineering.", - inputSchema: {}, - }, - async () => { - const stop = runAb(globalHeaded, ["network", "har", "stop", harOut]); - const clo = runAb(globalHeaded, ["close"]); - const msg = [asToolResult(stop).content?.[0]?.text, asToolResult(clo).content?.[0]?.text].join("\n---\n"); - const failed = (stop.status !== 0 && stop.status != null) || (clo.status !== 0 && clo.status != null); - if (failed) { - return { content: [{ type: "text", text: msg }], isError: true }; - } - return { content: [{ type: "text", text: msg }] }; - }, -); - -async function main() { - const transport = new StdioServerTransport(); - await mcpServer.connect(transport); -} - -main().catch((err) => { - console.error("rae-agent-browser-mcp:", err); - process.exit(1); -}); diff --git a/src/reverse_api/auto_engineer.py b/src/reverse_api/auto_engineer.py index 57a39ba..85b94c8 100644 --- a/src/reverse_api/auto_engineer.py +++ b/src/reverse_api/auto_engineer.py @@ -15,7 +15,11 @@ ToolPermissionContext, ) -from .agent_browser_bundle import agent_browser_bundle_error, agent_browser_stdio_mcp_config +from .agent_browser import ( + agent_browser_prompt_fields, + allowed_tools_agent_browser_agent_mode, + ensure_agent_browser_runtime, +) from .engineer import ClaudeEngineer from .opencode_engineer import OpenCodeEngineer, debug_log, format_error from .utils import get_har_dir @@ -74,7 +78,7 @@ def _build_auto_prompts(self) -> tuple[str, str]: browser_tool_label = ( "Chrome DevTools MCP" if self.agent_provider == "chrome-mcp" - else ("agent-browser (Vercel) via MCP" if self.agent_provider == "agent-browser" else "MCP") + else ("Vercel agent-browser (shell CLI)" if self.agent_provider == "agent-browser" else "MCP") ) system_prompt = load( @@ -98,6 +102,10 @@ def _build_auto_prompts(self) -> tuple[str, str]: } if self.agent_provider != "chrome-mcp": template_kwargs["har_path"] = str(self.har_path) + if self.agent_provider == "agent-browser": + template_kwargs.update( + agent_browser_prompt_fields(run_id=self.mcp_run_id, headless=self.headless), + ) user_message = load(template, **template_kwargs) return system_prompt, user_message @@ -125,11 +133,7 @@ def _get_mcp_config(self) -> tuple[str, dict]: spawns its own headless Chromium instead. """ if self.agent_provider == "agent-browser": - return agent_browser_stdio_mcp_config( - har_path=self.har_path, - run_id=self.mcp_run_id, - headless=self.headless, - ) + raise RuntimeError("browser MCP disabled for agent-browser provider") if self.agent_provider == "chrome-mcp": args = ["chrome-devtools-mcp@latest", "--no-usage-statistics"] if self.headless: @@ -164,27 +168,39 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.ui.start_analysis() if self.agent_provider == "agent-browser": - berr = agent_browser_bundle_error() - if berr: - self.ui.error(berr) - self.message_store.save_error(berr) + abe = ensure_agent_browser_runtime() + if abe: + self.ui.error(abe) + self.message_store.save_error(abe) return None system_prompt, user_message = self._get_active_prompts() self.message_store.save_prompt(user_message) - mcp_name, mcp_config = self._get_mcp_config() - - options = ClaudeAgentOptions( - system_prompt=system_prompt, - mcp_servers={mcp_name: mcp_config}, - permission_mode="bypassPermissions", - can_use_tool=self._handle_tool_permission, - cwd=str(self.scripts_dir.parent.parent), - model=self.model, - env={"CLAUDECODE": ""}, - stderr=self._handle_cli_stderr, - ) + if self.agent_provider == "agent-browser": + options = ClaudeAgentOptions( + system_prompt=system_prompt, + mcp_servers={}, + allowed_tools=allowed_tools_agent_browser_agent_mode(), + permission_mode="bypassPermissions", + can_use_tool=self._handle_tool_permission, + cwd=str(self.scripts_dir.parent.parent), + model=self.model, + env={"CLAUDECODE": ""}, + stderr=self._handle_cli_stderr, + ) + else: + mcp_name, mcp_config = self._get_mcp_config() + options = ClaudeAgentOptions( + system_prompt=system_prompt, + mcp_servers={mcp_name: mcp_config}, + permission_mode="bypassPermissions", + can_use_tool=self._handle_tool_permission, + cwd=str(self.scripts_dir.parent.parent), + model=self.model, + env={"CLAUDECODE": ""}, + stderr=self._handle_cli_stderr, + ) last_result: dict[str, Any] | None = None @@ -231,8 +247,7 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.ui.console.print("\n[dim]Make sure chrome-devtools-mcp is available: npx chrome-devtools-mcp@latest[/dim]") self.ui.console.print("[dim]Chrome 146+ required with auto-connect enabled at chrome://inspect/#remote-debugging[/dim]") elif self.agent_provider == "agent-browser": - self.ui.console.print("\n[dim]Bundled MCP: npm install --prefix /agent_browser_mcp[/dim]") - self.ui.console.print("[dim]Global CLI Chromium setup: npm i -g agent-browser && agent-browser install[/dim]") + self.ui.console.print("\n[dim]agent-browser: verify `npx` can run the configured package; adjust RAE_AGENT_BROWSER_PACKAGE env or agent_browser_npx_package in ~/.reverse-api/config.json[/dim]") else: self.ui.console.print("\n[dim]Make sure rae-playwright-mcp is installed: npm install -g rae-playwright-mcp[/dim]") else: @@ -264,31 +279,18 @@ def __init__(self, run_id: str, prompt: str, output_dir: str | None = None, agen def _get_active_prompts(self) -> tuple[str, str]: return ClaudeAutoEngineer._build_auto_prompts(self) - def _get_opencode_mcp_config(self) -> dict: + def _get_opencode_mcp_config(self) -> dict | None: """Return OpenCode MCP registration payload based on agent_provider. Auto-connect requires a headed Chrome with a remote debugging server, so it is dropped in headless mode in favor of an MCP-spawned headless Chromium. + + agent-browser skips MCP—the model shells the upstream CLI directly. """ if self.agent_provider == "agent-browser": - self.mcp_name = f"agent-browser-{self._session_id}" - from .agent_browser_bundle import agent_browser_command_list - - cmd = agent_browser_command_list( - har_path=self.har_path, - run_id=self.mcp_run_id, - headless=self.headless, - ) - return { - "name": self.mcp_name, - "config": { - "type": "local", - "command": cmd, - "enabled": True, - "timeout": 30000, - }, - } + self.mcp_name = None + return None if self.agent_provider == "chrome-mcp": self.mcp_name = f"chrome-devtools-{self._session_id}" cmd = ["npx", "-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"] @@ -332,10 +334,10 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.opencode_ui.start_analysis() if self.agent_provider == "agent-browser": - berr = agent_browser_bundle_error() - if berr: - self.opencode_ui.error(berr) - self.message_store.save_error(berr) + abe = ensure_agent_browser_runtime() + if abe: + self.opencode_ui.error(abe) + self.message_store.save_error(abe) return None system_prompt, user_message = self._get_active_prompts() @@ -374,14 +376,15 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: mcp_config = self._get_opencode_mcp_config() - try: - debug_log(f"Registering MCP server: {self.mcp_name}") - mcp_r = await client.post("/mcp", json=mcp_config) - mcp_r.raise_for_status() - debug_log("MCP server registered successfully") - except Exception as e: - self.opencode_ui.error(f"Failed to register MCP server: {e}") - return None + if mcp_config is not None: + try: + debug_log(f"Registering MCP server: {self.mcp_name}") + mcp_r = await client.post("/mcp", json=mcp_config) + mcp_r.raise_for_status() + debug_log("MCP server registered successfully") + except Exception as e: + self.opencode_ui.error(f"Failed to register MCP server: {e}") + return None # Start event stream BEFORE sending message event_task = asyncio.create_task(self._stream_events(client)) @@ -412,13 +415,13 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.opencode_ui.stop_streaming() # Deregister MCP server - try: - if self.mcp_name: + if self.mcp_name: + try: debug_log(f"Deregistering MCP server: {self.mcp_name}") await client.delete(f"/mcp/{self.mcp_name}") debug_log("MCP server deregistered") - except Exception as e: - debug_log(f"Failed to deregister MCP server: {e}") + except Exception as e: + debug_log(f"Failed to deregister MCP server: {e}") # Check for errors if self._last_error: @@ -602,43 +605,28 @@ def on_event(event: Any) -> None: await client.start() if self.agent_provider == "chrome-mcp": - mcp_server_name = "chrome-devtools" chrome_args = ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"] if self.headless: chrome_args.append("--headless") else: chrome_args.append("--autoConnect") - mcp_config = { - "type": "local", - "command": "npx", - "args": chrome_args, - "tools": ["*"], - "timeout": 30000, + mcp_servers_payload: dict[str, Any] = { + "chrome-devtools": { + "type": "local", + "command": "npx", + "args": chrome_args, + "tools": ["*"], + "timeout": 30000, + }, } elif self.agent_provider == "agent-browser": - berr = agent_browser_bundle_error() - if berr: - eng.ui.error(berr) - eng.message_store.save_error(berr) + abe = ensure_agent_browser_runtime() + if abe: + eng.ui.error(abe) + eng.message_store.save_error(abe) return None - - from .agent_browser_bundle import agent_browser_command_list - - argv = agent_browser_command_list( - har_path=self._engineer.har_path, - run_id=self.mcp_run_id, - headless=self.headless, - ) - mcp_server_name = "agent-browser" - mcp_config = { - "type": "local", - "command": argv[0], - "args": argv[1:], - "tools": ["*"], - "timeout": 30000, - } + mcp_servers_payload = {} else: - mcp_server_name = "playwright" pw_args = [ "-y", "rae-playwright-mcp@latest", @@ -648,12 +636,14 @@ def on_event(event: Any) -> None: ] if self.headless: pw_args.append("--headless") - mcp_config = { - "type": "local", - "command": "npx", - "args": pw_args, - "tools": ["*"], - "timeout": 30000, + mcp_servers_payload = { + "playwright": { + "type": "local", + "command": "npx", + "args": pw_args, + "tools": ["*"], + "timeout": 30000, + }, } async def on_pre_tool_use(input: dict, _invocation: dict) -> dict: @@ -676,7 +666,7 @@ async def on_post_tool_use(input: dict, invocation: dict) -> dict: "model": eng.copilot_model, "streaming": True, "infinite_sessions": {"enabled": True}, - "mcp_servers": {mcp_server_name: mcp_config}, + "mcp_servers": mcp_servers_payload, "on_permission_request": PermissionHandler.approve_all, "hooks": { "on_pre_tool_use": on_pre_tool_use, diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index 3fcee65..9b9811a 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -209,7 +209,7 @@ def _build_dry_run_payload( import shutil import subprocess - from .agent_browser_bundle import agent_browser_bundle_error + from .agent_browser import ensure_agent_browser_runtime checks: list[dict] = [] @@ -266,8 +266,7 @@ def _build_dry_run_payload( else: checks.append({"name": f"sdk:{sdk}", "status": "ok", "message": f"{sdk_env_var} present"}) - # 5. Node.js + npx for MCP servers (Playwright MCP, bundled agent-browser MCP, - # chrome-mcp wrapper, etc.). + # 5. Node.js + npx for MCP servers (Playwright MCP, chrome-mcp, agent-browser via npx, …). node = shutil.which("node") if node is None: checks.append({ @@ -302,13 +301,13 @@ def _build_dry_run_payload( "message": "chrome-mcp without --headless requires Chrome 146+ with auto-connect enabled at chrome://inspect/#remote-debugging — this is not auto-checkable", }) - # 7. Bundled MCP for agent-browser + # 7. agent-browser CLI bootstrap (npx prefetch) if agent_provider == "agent-browser": - berr = agent_browser_bundle_error() + abe = ensure_agent_browser_runtime() checks.append({ - "name": "agent-browser:MCP-bundle", - "status": "error" if berr else "ok", - "message": berr or "bundled agent-browser MCP (npm deps in agent_browser_mcp/)", + "name": "agent-browser:cli", + "status": "error" if abe else "ok", + "message": abe or "npx can resolve agent_browser_npx_package / RAE_AGENT_BROWSER_PACKAGE", }) # 8. Output dir writability — probe with a unique filename so we never @@ -1019,7 +1018,7 @@ def handle_settings(mode_color=THEME_PRIMARY): provider_choices = [ Choice(title="auto (Playwright MCP)", value="auto"), Choice(title="chrome-mcp (Chrome DevTools MCP)", value="chrome-mcp"), - Choice(title="agent-browser (Vercel CLI via bundled MCP)", value="agent-browser"), + Choice(title="agent-browser (Vercel CLI — shell only, no browser MCP)", value="agent-browser"), Choice(title="back", value="back"), ] provider = questionary.select( @@ -1038,15 +1037,11 @@ def handle_settings(mode_color=THEME_PRIMARY): config_manager.set("agent_provider", provider) console.print(f" [dim]updated[/dim] agent provider: {provider}") if provider == "agent-browser": - from pathlib import Path - - import reverse_api - - bundle = Path(reverse_api.__file__).resolve().parent / "agent_browser_mcp" console.print() - console.print(f" [dim]install bundled MCP deps (once): npm install --prefix {bundle}[/dim]") - console.print(" [dim]install Chromium helper CLI: npm i -g agent-browser && agent-browser install[/dim]") - console.print(" [dim]Linux servers: agent-browser install --with-deps[/dim]") + console.print(" [dim]no bundled MCP: the model runs `npx -y agent-browser …` via Bash.[/dim]") + console.print(" [dim]pin with `agent_browser_npx_package` in config or `RAE_AGENT_BROWSER_PACKAGE` env.[/dim]") + console.print(" [dim]optional extra instructions: `agent_browser_notes` (or RAE_AGENT_BROWSER_NOTES).[/dim]") + console.print(" [dim]first-time chromium: `npx -y agent-browser@0 install` (add --with-deps on Linux).[/dim]") console.print() elif provider == "chrome-mcp": console.print() @@ -1749,15 +1744,9 @@ def run_auto_capture( console.print() elif agent_provider == "agent-browser": - from pathlib import Path - - import reverse_api - - bundle = Path(reverse_api.__file__).resolve().parent / "agent_browser_mcp" console.print() - console.print(" [dim]agent-browser: MCP bridge to Vercel's agent-browser CLI (strong default for VPS/headless hosts).[/dim]") - console.print(f" [dim]install MCP bundle deps:[/dim] npm install --prefix [cyan]{bundle}[/cyan]") - console.print(" [dim]install chromium helper:[/dim] npm i -g agent-browser && agent-browser install [dim]# add --with-deps on Linux[/dim]") + console.print(" [dim]agent-browser: no Playwright/Chrome MCP — the SDK agent shells Vercel's CLI (`npx -y …`).[/dim]") + console.print(" [dim]We prefetch with npx once; chromium bytes: global `npm i -g agent-browser && agent-browser install` if preferred.[/dim]") console.print() sdk = config_manager.get("sdk", "claude") diff --git a/src/reverse_api/config.py b/src/reverse_api/config.py index 93571f8..aed033c 100644 --- a/src/reverse_api/config.py +++ b/src/reverse_api/config.py @@ -6,6 +6,8 @@ DEFAULT_CONFIG = { "agent_provider": "auto", # "auto" | "chrome-mcp" | "agent-browser" + "agent_browser_notes": "", # extra instructions merged into agent-browser prompt / RAE_AGENT_BROWSER_NOTES env + "agent_browser_npx_package": "agent-browser@0", "claude_code_model": "claude-sonnet-4-6", "collector_model": "claude-sonnet-4-6", # Model for collector mode "cursor_model": "composer-2", # Model id for Cursor SDK (see Cursor.models.list()) diff --git a/src/reverse_api/cursor_engineer.py b/src/reverse_api/cursor_engineer.py index 0670d73..89463e7 100644 --- a/src/reverse_api/cursor_engineer.py +++ b/src/reverse_api/cursor_engineer.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any -from .agent_browser_bundle import agent_browser_bundle_error +from .agent_browser import ensure_agent_browser_runtime from .base_engineer import BaseEngineer from .tui import ClaudeUI @@ -407,7 +407,7 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: class CursorAutoEngineer(CursorEngineer): - """Agent + capture using Cursor SDK with MCP browser servers.""" + """Agent capture using Cursor SDK; browser via MCP unless provider is CLI-only.""" def __init__( self, @@ -436,14 +436,7 @@ def __init__( def _cursor_mcp_servers(self) -> dict[str, Any]: if self.agent_provider == "agent-browser": - from .agent_browser_bundle import agent_browser_stdio_mcp_config - - name, cfg = agent_browser_stdio_mcp_config( - har_path=self.har_path, - run_id=self.mcp_run_id, - headless=self.headless, - ) - return {name: cfg} + return {} if self.agent_provider == "chrome-mcp": args = ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"] if self.headless: @@ -494,10 +487,10 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: return None if self.agent_provider == "agent-browser": - berr = agent_browser_bundle_error() - if berr: - self.ui.error(berr) - self.message_store.save_error(berr) + abe = ensure_agent_browser_runtime() + if abe: + self.ui.error(abe) + self.message_store.save_error(abe) return None system_prompt, user_message = ClaudeAutoEngineer._build_auto_prompts(self) diff --git a/src/reverse_api/prompts/auto/user_agent_browser.md b/src/reverse_api/prompts/auto/user_agent_browser.md index bc81003..1171058 100644 --- a/src/reverse_api/prompts/auto/user_agent_browser.md +++ b/src/reverse_api/prompts/auto/user_agent_browser.md @@ -7,31 +7,61 @@ +## Mandatory tooling + +Reverse engineering relies on **`recording.har`** at **`{har_path}`**. You MUST drive browsing **only through the Vercel [agent-browser](https://github.com/vercel-labs/agent-browser) CLI** invoked from the shell (**Bash** / terminal MCP). Do **not** claim you used browser MCP tools — none are attached in this provider. + +Warm up context once: + +```bash +export AGENT_BROWSER_SESSION="{agent_browser_session}" +npx -y {agent_browser_npx_package} skills get core --full +``` + +{agent_browser_headed_hint}### Session + package + +- Stable session env: **`AGENT_BROWSER_SESSION={agent_browser_session}`** before every invocation (isolates refs/HAR for this run). +- Package pin: **`npx -y {agent_browser_npx_package}`** (already verified by the host). Users can override via `RAE_AGENT_BROWSER_PACKAGE` or config `agent_browser_npx_package`. + +### Cloud / remote browsers + +If the operator hints at cloud backends (Bedrock AgentCore, Vercel Sandbox, …), run `skills list` then `skills get ` for the relevant skill bundle and prefer those flows—they stay version-matched to the CLI. +{agent_browser_notes_block} + ## Workflow -Uses **Vercel [agent-browser](https://github.com/vercel-labs/agent-browser)** (Rust CLI invoked through MCP). Interaction model: take an accessibility **snapshot** to obtain `@eN` element refs, then **click**, **fill**, or **type** using those refs. Run `snapshot` again after navigations before assuming refs are valid. +Interaction model identical to upstream docs: **`snapshot`** for `@eN` refs → **`click` / `fill` / …** → **`snapshot`** after navigation. + ### Phase 1: BROWSE -Use MCP tools (names mirror the Playwright MCP style for familiarity): - -| Tool | Role | -|------|------| -| `browser_navigate` | Open URLs | -| `browser_snapshot` | Accessibility tree (`-i` by default); use refs like `@e1` | -| `browser_click`, `browser_fill`, `browser_type`, `browser_press_key` | Interactions | -| `browser_wait_for` | Wait by selector **or** milliseconds **or** visible text substring | -| `browser_scroll` | Scroll the page | -| `browser_evaluate` | Execute JavaScript in the page | -| `browser_take_screenshot` | Optional visual aid (prefer snapshots to save tokens and avoid large images) | + +Use shell commands shaped like: + +```bash +export AGENT_BROWSER_SESSION="{agent_browser_session}" +npx -y {agent_browser_npx_package} network har start +npx -y {agent_browser_npx_package} open https://example.com +npx -y {agent_browser_npx_package} snapshot -i --json +# … iterate … +``` + ### Phase 2: MONITOR -Call `browser_network_requests` periodically. Watch for APIs, tokens, cookies, redirects, CORS quirks, and graph-style endpoints (`/graphql`, protobuf, etc.). -### Phase 3: CAPTURE -When you have explored enough traffic, call `browser_close` once to **flush HAR JSON** into the canonical path `{har_path}` and shut down Chromium. Omitting this prevents reverse engineering later. +Use **`network requests --json`** (with filters when noisy) plus occasional snapshots. + +### Phase 3: CAPTURE → `recording.har` + +Before reverse engineering MUST flush HAR to the canonical file **exact path** below (create parent dirs if needed): + +```bash +export AGENT_BROWSER_SESSION="{agent_browser_session}" +npx -y {agent_browser_npx_package} network har stop {har_path} +npx -y {agent_browser_npx_package} close +``` ### Phase 4: REVERSE ENGINEER -Analyze `recording.har` at `{har_path}` and emit the scripted client/source files under `{scripts_dir}` using the codegen rules from the system prompt. -**Headless VPS tips:** Provider default is usually headless. First-time setups run `npm install -g agent-browser && agent-browser install` so Chrome/Chromium for Testing downloads; Linux may need `agent-browser install --with-deps`. `agent-browser doctor` diagnoses environment issues. +Read **`{har_path}`** and emit code under **`{scripts_dir}`** per the system prompt. +**VPS tips:** first-time hosts run `npx -y {agent_browser_npx_package} install` (add `--with-deps` on Linux). `doctor` diagnoses missing Chrome or permissions. diff --git a/tests/test_agent_browser.py b/tests/test_agent_browser.py new file mode 100644 index 0000000..b21b54c --- /dev/null +++ b/tests/test_agent_browser.py @@ -0,0 +1,72 @@ +"""Tests for reverse_api/agent_browser helpers.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from reverse_api import agent_browser + + +def test_allowed_tools_contains_bash(): + tools = agent_browser.allowed_tools_agent_browser_agent_mode() + assert "Bash" in tools + + +def test_ensure_agent_browser_missing_node(): + with patch.object(agent_browser.shutil, "which", return_value=None): + err = agent_browser.ensure_agent_browser_runtime() + assert err is not None + assert "node" in err.lower() + + +def test_ensure_agent_browser_missing_npx(): + def which_side(cmd: str) -> str | None: + return "/fake/node" if cmd == "node" else None + + with patch.object(agent_browser.shutil, "which", side_effect=which_side): + err = agent_browser.ensure_agent_browser_runtime() + assert err is not None + assert "npx" in err.lower() + + +def test_ensure_prefetch_ok(): + def which_side(cmd: str) -> str | None: + return f"/fake/{cmd}" if cmd in ("node", "npx") else None + + proc = MagicMock(returncode=0, stderr="", stdout="usage") + + with patch.object(agent_browser.shutil, "which", side_effect=which_side): + with patch.object(agent_browser.subprocess, "run", return_value=proc): + with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="agent-browser@test"): + err = agent_browser.ensure_agent_browser_runtime() + assert err is None + + +def test_ensure_prefetch_nonzero(): + def which_side(cmd: str) -> str | None: + return f"/fake/{cmd}" if cmd in ("node", "npx") else None + + proc = MagicMock(returncode=127, stderr="ENOTFOUND", stdout="") + + with patch.object(agent_browser.shutil, "which", side_effect=which_side): + with patch.object(agent_browser.subprocess, "run", return_value=proc): + with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="agent-browser@test"): + err = agent_browser.ensure_agent_browser_runtime() + assert err is not None + assert "prefetch" in err.lower() + + +def test_npx_package_env_overrides(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("RAE_AGENT_BROWSER_PACKAGE", "agent-browser@fixture") + assert agent_browser.agent_browser_npx_package() == "agent-browser@fixture" + + +def test_prompt_fields_includes_notes_block(): + with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="pkg@x"): + with patch("reverse_api.agent_browser.agent_browser_extra_notes", return_value="cloud hint"): + fields = agent_browser.agent_browser_prompt_fields(run_id="run1", headless=True) + assert fields["agent_browser_session"] == "rae-run1" + assert fields["agent_browser_npx_package"] == "pkg@x" + assert "cloud hint" in fields["agent_browser_notes_block"] diff --git a/tests/test_agent_browser_bundle.py b/tests/test_agent_browser_bundle.py deleted file mode 100644 index ae75933..0000000 --- a/tests/test_agent_browser_bundle.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Tests for bundled agent-browser MCP wiring.""" - -from __future__ import annotations - -from pathlib import Path - -from reverse_api.agent_browser_bundle import agent_browser_command_list, agent_browser_stdio_mcp_config - - -def test_stdio_mcp_config_headless_writes_har_argv(tmp_path: Path): - har = tmp_path / "recording.har" - name, cfg = agent_browser_stdio_mcp_config(har_path=har, run_id="run_xyz", headless=True) - assert name == "agent-browser" - assert cfg["command"] == "node" - assert "--har-out" in cfg["args"] - har_i = cfg["args"].index("--har-out") - assert cfg["args"][har_i + 1] == str(har) - sess_i = cfg["args"].index("--session") - assert cfg["args"][sess_i + 1] == "run_xyz" - assert "--headed" not in cfg["args"] - - -def test_stdio_mcp_config_headed_requests_window(tmp_path: Path): - har = tmp_path / "recording.har" - _name, cfg = agent_browser_stdio_mcp_config(har_path=har, run_id="abc", headless=False) - assert "--headed" in cfg["args"] - - -def test_command_list_matches_stdio_shape(tmp_path: Path): - har = tmp_path / "recording.har" - flat = agent_browser_command_list(har_path=har, run_id="r1", headless=True) - assert flat[0] == "node" - _name, cfg = agent_browser_stdio_mcp_config(har_path=har, run_id="r1", headless=True) - assert flat == [cfg["command"], *cfg["args"]] - diff --git a/tests/test_auto_engineer.py b/tests/test_auto_engineer.py index 2f2bd37..6b56d73 100644 --- a/tests/test_auto_engineer.py +++ b/tests/test_auto_engineer.py @@ -1,5 +1,6 @@ """Tests for auto_engineer.py - Auto mode engineers.""" +from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path from typing import Any @@ -239,9 +240,10 @@ def test_get_active_prompts_auto(self, tmp_path): class TestAgentBrowserPrompt: - """Prompts for bundled agent-browser MCP provider.""" + """Prompts for shell-driven agent-browser provider.""" - def _make_engineer(self, tmp_path, **kwargs): + @contextmanager + def _engineer_with_pkg_mock(self, tmp_path, pkg="agent-browser@test", **kwargs): defaults = { "run_id": "test123", "prompt": "browse and capture", @@ -253,35 +255,42 @@ def _make_engineer(self, tmp_path, **kwargs): with patch("reverse_api.auto_engineer.get_har_dir", return_value=tmp_path / "har"): with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"): with patch("reverse_api.base_engineer.MessageStore"): - return ClaudeAutoEngineer(**defaults) - - def test_system_labels_tooling(self, tmp_path): - eng = self._make_engineer(tmp_path) - sys_p, _user_p = eng._build_auto_prompts() - assert "agent-browser" in sys_p - - def test_user_template_covers_close_and_har(self, tmp_path): - eng = self._make_engineer(tmp_path) - _sys_p, user_p = eng._build_auto_prompts() - assert "browser_close" in user_p - assert "recording.har" in user_p - assert str(eng.har_path) in user_p + with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value=pkg): + yield ClaudeAutoEngineer(**defaults) + + def test_system_labels_shell_cli(self, tmp_path): + with self._engineer_with_pkg_mock(tmp_path) as eng: + sys_p, _user_p = eng._build_auto_prompts() + assert "shell CLI" in sys_p + + def test_user_prompt_uses_ag_cli_session_and_har(self, tmp_path): + with self._engineer_with_pkg_mock(tmp_path) as eng: + _sys_p, user_p = eng._build_auto_prompts() + assert "skills get core" in user_p + assert "AGENT_BROWSER_SESSION" in user_p + assert "npx -y agent-browser@test" in user_p + assert str(eng.har_path) in user_p def test_opencode_delegates_prompts(self, tmp_path): with patch("reverse_api.auto_engineer.get_har_dir", return_value=tmp_path / "har"): with patch("reverse_api.base_engineer.get_scripts_dir", return_value=tmp_path / "scripts"): with patch("reverse_api.base_engineer.MessageStore"): with patch("reverse_api.opencode_engineer.OpenCodeUI"): - eng = OpenCodeAutoEngineer( - run_id="test123", - prompt="browse and capture", - output_dir=str(tmp_path), - agent_provider="agent-browser", - opencode_provider="anthropic", - opencode_model="claude-opus-4-6", - ) - sys_p, user_p = eng._get_active_prompts() - assert "agent-browser" in sys_p + with patch( + "reverse_api.agent_browser.agent_browser_npx_package", + return_value="agent-browser@test", + ): + eng = OpenCodeAutoEngineer( + run_id="test123", + prompt="browse and capture", + output_dir=str(tmp_path), + agent_provider="agent-browser", + opencode_provider="anthropic", + opencode_model="claude-opus-4-6", + ) + sys_p, user_p = eng._get_active_prompts() + assert "shell CLI" in sys_p + assert "npx" in user_p class TestChromeMcpConfig: @@ -316,20 +325,11 @@ def test_auto_mcp_config(self, tmp_path): assert "rae-playwright-mcp@latest" in config["args"] assert "--run-id" in config["args"] - def test_agent_browser_mcp_config(self, tmp_path): - """agent-browser bundles node stdio shim with har + session wiring.""" + def test_agent_browser_disables_mcp_helpers(self, tmp_path): + """Browser MCP shim is deliberately absent for agent-browser.""" eng = self._make_engineer(tmp_path, agent_provider="agent-browser") - name, config = eng._get_mcp_config() - assert name == "agent-browser" - assert config["type"] == "stdio" - assert config["command"] == "node" - ss = "--session" - hh = "--har-out" - assert hh in config["args"] and ss in config["args"] - hi = config["args"].index(hh) - si = config["args"].index(ss) - assert config["args"][hi + 1].endswith("recording.har") - assert config["args"][si + 1] == "test123" + with pytest.raises(RuntimeError, match="browser MCP disabled"): + eng._get_mcp_config() class TestOpenCodeChromeMcpConfig: @@ -366,15 +366,11 @@ def test_opencode_auto_mcp_config(self, tmp_path): assert "playwright" in eng.mcp_name assert "rae-playwright-mcp@latest" in config["config"]["command"] - def test_opencode_agent_browser_mcp_config(self, tmp_path): - """OpenCode agent-browser mounts bundled node MCP as a flat command.""" + def test_opencode_agent_browser_skips_mcp(self, tmp_path): + """CLI-only agent-browser mode does not register OpenCode MCP.""" eng = self._make_engineer(tmp_path, agent_provider="agent-browser") - oc = eng._get_opencode_mcp_config() - assert "agent-browser" in eng.mcp_name - cmd = oc["config"]["command"] - assert cmd[0] == "node" - assert "--har-out" in cmd - assert "--session" in cmd + assert eng._get_opencode_mcp_config() is None + assert eng.mcp_name is None def test_opencode_chrome_mcp_prompt(self, tmp_path): """OpenCode chrome-mcp uses Chrome DevTools prompt.""" diff --git a/tests/test_config.py b/tests/test_config.py index 47fa160..6a9c6e8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -143,6 +143,8 @@ def test_has_required_keys(self): """DEFAULT_CONFIG contains all expected keys.""" expected_keys = { "agent_provider", + "agent_browser_notes", + "agent_browser_npx_package", "claude_code_model", "collector_model", "copilot_model", diff --git a/website/content/docs/configuration/agent.mdx b/website/content/docs/configuration/agent.mdx index 638366e..0ba1df8 100644 --- a/website/content/docs/configuration/agent.mdx +++ b/website/content/docs/configuration/agent.mdx @@ -3,8 +3,7 @@ title: Agent configuration description: Configure the AI agent that drives the browser in agent mode. --- -Agent mode runs an AI loop that drives a browser via MCP. The provider you -pick determines *which* browser and *how* it's controlled. +Agent mode runs an AI loop. Depending on agent provider this may attach **browser MCP**, or—in **`agent-browser` mode—instruct the model to invoke the **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)** directly from terminal tools. ## Agent providers @@ -33,15 +32,23 @@ profile. ### `agent-browser` -Controls **Chrome via [Vercel agent-browser](https://github.com/vercel-labs/agent-browser)** through a bundled MCP shim. Tune this profile when you primarily run on VPS/CI and want deterministic headless tooling without depending on Cursor’s embedded Browser tab. +Uses **[Vercel agent-browser](https://github.com/vercel-labs/agent-browser)** as a standalone CLI—there is **no** bundled browser MCP shim in Reverse API Engineer for this slot. -**Bootstrap checklist** +Reverse API Engineer prefetch-checks `npx` so the pinned package can hydrate from the registry/cache, emits prompts referencing `skills get …`, and configures Claude-backed runs with **terminal-friendly tool allow-lists**. Other SDKs omit auxiliary browser MCP registration for `agent-browser` and leave shell access to whichever capabilities their upstream runtimes expose. -1. `npm install --prefix /reverse_api/agent_browser_mcp` -2. `npm install -g agent-browser && agent-browser install` (`--with-deps` on bare Linux hosts) -3. Set `"agent_provider": "agent-browser"` (settings UI or JSON) +Suggested JSON knobs: -Operational notes and backlog live in `src/reverse_api/agent_browser_mcp/README.md`. +```json +{ + "agent_provider": "agent-browser", + "agent_browser_npx_package": "agent-browser@0", + "agent_browser_notes": "Optional operator guidance (AgentCore endpoints, corp proxy tweaks, sandbox IDs, …)" +} +``` + +You can alternatively drive the same knobs with **`RAE_AGENT_BROWSER_PACKAGE`** or **`RAE_AGENT_BROWSER_NOTES`**. + +First-time infra still needs Chromium bits just like upstream docs describe (`npm i -g agent-browser && agent-browser install`, `--with-deps` on trimmed Linux VMs). ## How to switch providers diff --git a/website/content/docs/modes/agent.mdx b/website/content/docs/modes/agent.mdx index 8d99f72..6d72bae 100644 --- a/website/content/docs/modes/agent.mdx +++ b/website/content/docs/modes/agent.mdx @@ -4,8 +4,7 @@ description: Fully autonomous browser interaction. The AI browses, captures, and --- Agent mode hands the steering wheel to the AI. Describe the goal in plain -English and the agent drives a browser through MCP while reverse-engineering -the API in real time. +English; providers such as Playwright MCP or Chrome DevTools MCP route browser actions over MCP surfaces, whereas **`agent-browser`** mode describes shell-driven flows that call upstream's CLI (`npx`) directly. - **Vercel [agent-browser](https://github.com/vercel-labs/agent-browser)** via a - small bundled MCP shim. Targets headless VPS/CI workflows: installs its own - Chromium channel through `npm i -g agent-browser && agent-browser install` - (`--with-deps` on Linux) and persists traffic to `recording.har`. - - Requires **npm install** inside `reverse_api/agent_browser_mcp/` once per - install (CLI prints the path). See **`src/reverse_api/agent_browser_mcp/README.md`** - in this repository for the checklist and roadmap notes. + **`agent-browser`** is the standalone [Vercel CLI](https://github.com/vercel-labs/agent-browser). Reverse API Engineer prefetch-checks `npx`, injects workflows that call **`skills get core --full`**, **`network har`** capture, `@eN` snapshot ergonomics, and optional cloud-browser skill bundles—all via the shell primitives your SDK exposes (Bash-first on Claude SDK runs). Pin versions with **`agent_browser_npx_package`** or **`RAE_AGENT_BROWSER_PACKAGE`** and pass operator-only guidance through **`agent_browser_notes`** when you rely on SaaS-hosted browsers. From 4b28b2fc4e768df2070081c3f1104698390c84f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 24 May 2026 00:26:36 +0000 Subject: [PATCH 3/4] docs(agent-browser): align CLI vs MCP wording and describe npx/skills preflight - Replace misleading 'browser MCP disabled' / MCP-only docstrings with CLI-accurate messages - Dry-run and settings copy: node/npx rationale covers MCP + agent-browser CLI - user_agent_browser/system prompts: host --help probe vs model skills list/get - CHANGELOG, README, website: consistent download/cache, cloud skills, no RAE browser MCP - Test: rename and match new RuntimeError for _get_mcp_config guard Co-authored-by: kalil0321 --- CHANGELOG.md | 2 +- README.md | 4 +- src/reverse_api/agent_browser.py | 17 +++++-- src/reverse_api/auto_engineer.py | 43 +++++++++------- src/reverse_api/cli.py | 49 ++++++++++++++----- src/reverse_api/cursor_engineer.py | 2 +- src/reverse_api/prompts/auto/system.md | 8 ++- .../prompts/auto/user_agent_browser.md | 14 ++++-- tests/test_auto_engineer.py | 6 +-- website/content/docs/configuration/agent.mdx | 6 +-- website/content/docs/installation.mdx | 3 +- website/content/docs/modes/agent.mdx | 4 +- 12 files changed, 104 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e56a7e..5c6b25a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **`agent_provider: "agent-browser"`**: Prompt-only CLI mode—agents run **`npx -y … agent-browser`** (plus bundled **skills**) from their shell tooling; Reverse API Engineer prefetch-checks **`npx`**, skips browser MCP surfaces, exposes optional `agent_browser_npx_package` / `agent_browser_notes` knobs. +- **`agent_provider: "agent-browser"`**: Shell-driven **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)** workflow—agents run **`npx -y …`**, **`skills get …`** / **`skills list`** for cloud backends, and HAR captures per the injected prompt; Reverse API Engineer probes **`npx -y … --help`** upfront so npm **resolves the package** (downloads on first cache miss) and misconfiguration fails fast. No Reverse API Engineer browser MCP shim; optional knobs `agent_browser_npx_package` / `agent_browser_notes` (+ env equivalents). ### Added - **Cursor SDK support**: Added `sdk=cursor` / `--sdk cursor` engineering support through a bundled Node bridge around the Cursor TypeScript SDK. Cursor runs use the configured Cursor model (default `composer-2`), accept MCP server configuration, resume Cursor agents across follow-up turns, and normalize streamed tool output plus token usage into the existing TUI/message-store flow diff --git a/README.md b/README.md index f3a16a3..f702ea7 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,14 @@ Cycle modes with **Shift+Tab**: | Mode | What it does | |------|--------------| | `manual` | You drive the browser; AI generates the client from captured traffic. | -| `agent` | An AI agent drives the browser autonomously (Playwright MCP, Chrome DevTools MCP, or Vercel agent-browser). | +| `agent` | An AI agent drives capture autonomously (Playwright or Chrome MCP, or Vercel agent-browser CLI). | | `engineer` | Re-run generation on a previous capture (`engineer `). | | `collector` | Agent collects structured data (JSON/CSV) using web search + fetch. | Agent mode providers: - **auto** (default): Playwright MCP, single workflow for browsing + reverse engineering. - **chrome-mcp**: drives your real Chrome so you keep existing sessions/cookies. Requires Chrome 146+ and Node.js 20.19+. -- **agent-browser**: [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) as a **pure CLI**. No bundled browser MCP—the agent shells `npx -y …`, loads upstream **skills**, and captures HAR manually. Strong default for VPS/CI. Tune with `agent_browser_npx_package`, `agent_browser_notes` (optional), env `RAE_AGENT_BROWSER_PACKAGE` / `RAE_AGENT_BROWSER_NOTES`. First-time Chromium bootstrap: `npx -y agent-browser install` (`--with-deps` on bare Linux). +- **agent-browser**: [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) **CLI** (not a Reverse API Engineer browser MCP server). Before capture the host runs **`npx -y --help`** so npm resolves/caches the package; the prompt instructs **`skills get core --full`**, **`skills list`** when cloud backends apply, and manual HAR capture. Strong default for VPS/CI. Tune with `agent_browser_npx_package`, `agent_browser_notes` (optional), env `RAE_AGENT_BROWSER_PACKAGE` / `RAE_AGENT_BROWSER_NOTES`. First-time Chromium bootstrap: `npx -y agent-browser install` (`--with-deps` on bare Linux). Optionally prefetch for faster runs: diff --git a/src/reverse_api/agent_browser.py b/src/reverse_api/agent_browser.py index 66ef38d..65926d1 100644 --- a/src/reverse_api/agent_browser.py +++ b/src/reverse_api/agent_browser.py @@ -1,7 +1,9 @@ """Helpers for agent-browser provider: bootstrap + prompt context. -This mode avoids a custom MCP shim. Agents drive Vercel's ``agent-browser`` CLI directly -via their shell tooling (e.g. Bash), after we verify ``npx`` can fetch the CLI. +Reverse API Engineer wires the upstream Vercel ``agent-browser`` **CLI** (not a bundled +browser MCP server). Models invoke it via shell tooling such as Bash. Before a session +we probe ``npx -y --help`` so npm resolves the package (typically downloading into +the cache when it has not run before) and configuration errors surface early. """ from __future__ import annotations @@ -59,7 +61,14 @@ def agent_browser_extra_notes() -> str: def ensure_agent_browser_runtime() -> str | None: - """Return None if agent-browser CLI is fetchable via npx; otherwise a short error.""" + """Verify the configured package is runnable via ``npx`` (download/cache + early failure). + + Runs ``npx -y --help``. On a fresh machine npm may fetch the tarball into + its cache; on repeat runs npm reuses cached bits. ``None`` means the probe succeeded. + + Otherwise returns a short human-readable failure string (missing ``node``/``npx``, + timeouts, nonzero exit). + """ if shutil.which("node") is None: return "node not found in PATH (needed to run agent-browser via npx)." @@ -94,7 +103,7 @@ def ensure_agent_browser_runtime() -> str | None: def allowed_tools_agent_browser_agent_mode() -> list[str]: - """Tool allow-list for Claude auto engineer when MCP browser is omitted.""" + """Tool allow-list for Claude SDK runs when browsing is delegated to agent-browser.""" return sorted(_AGENT_BROWSER_TOOLS) diff --git a/src/reverse_api/auto_engineer.py b/src/reverse_api/auto_engineer.py index 85b94c8..168e9c5 100644 --- a/src/reverse_api/auto_engineer.py +++ b/src/reverse_api/auto_engineer.py @@ -1,6 +1,8 @@ -"""Auto mode engineers: LLM-controlled browser automation with real-time reverse engineering. +"""Auto mode engineers: LLM-controlled browsing with real-time reverse engineering. -Combines browser automation via MCP with simultaneous API reverse engineering. +Providers **auto** and **chrome-mcp** attach a browser MCP server to the SDK. Provider +**agent-browser** shells the upstream Vercel ``agent-browser`` CLI (validated with an +``npx`` prefetch) instead of attaching browser MCP here. """ import asyncio @@ -30,7 +32,7 @@ class ClaudeAutoEngineer(ClaudeEngineer): - """Auto mode using Claude SDK: LLM controls browser via MCP while reverse engineering.""" + """Auto mode using Claude SDK: LLM-led browsing plus reverse-engineering codegen.""" def __init__( self, @@ -41,9 +43,9 @@ def __init__( agent_provider: str = "auto", **kwargs, ): - """Initialize auto engineer with expected HAR path (created by MCP).""" - # `headless` is auto-engineer specific (controls the MCP-spawned browser), - # not BaseEngineer concept; pop before super() to avoid an unknown kwarg. + """Initialize Claude-backed agent engineer (HAR path derives from ``run_id``).""" + # `headless` is auto-engineer specific: for MCP providers it configures the MCP + # server's browser launch; for `agent-browser` it only adjusts prompt wording. headless = kwargs.pop("headless", False) har_dir = get_har_dir(run_id, output_dir) har_path = har_dir / "recording.har" @@ -126,14 +128,16 @@ async def _handle_tool_permission(self, tool_name: str, input_data: dict[str, An return PermissionResultAllow(updated_input=input_data) def _get_mcp_config(self) -> tuple[str, dict]: - """Return (server_name, mcp_config) based on agent_provider. + """Return ``(server_name, mcp_config)`` for Playwright or Chrome MCP providers. - Auto-connect requires a real headed Chrome instance with a remote - debugging server, so it is dropped in headless mode and the MCP - spawns its own headless Chromium instead. + Not applicable to ``agent-browser`` (calling this raises). Auto-connect Chrome + requires a headed instance with remote debugging unless ``headless`` is set. """ if self.agent_provider == "agent-browser": - raise RuntimeError("browser MCP disabled for agent-browser provider") + raise RuntimeError( + "agent-browser uses the Vercel agent-browser CLI from the shell, not a " + "registered browser MCP server (this helper only builds configs for MCP providers)" + ) if self.agent_provider == "chrome-mcp": args = ["chrome-devtools-mcp@latest", "--no-usage-statistics"] if self.headless: @@ -160,7 +164,7 @@ def _get_mcp_config(self) -> tuple[str, dict]: } async def analyze_and_generate(self) -> dict[str, Any] | None: - """Run auto mode with MCP browser integration. + """Run agent mode with browser automation appropriate to ``agent_provider``. Reuses _process_streaming_response and follow-up loop from ClaudeEngineer. """ @@ -256,10 +260,10 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: class OpenCodeAutoEngineer(OpenCodeEngineer): - """Auto mode using OpenCode SDK: Register MCP server dynamically.""" + """Agent mode via OpenCode: registers a browser MCP server when the provider uses MCP.""" def __init__(self, run_id: str, prompt: str, output_dir: str | None = None, agent_provider: str = "auto", **kwargs): - """Initialize auto engineer with expected HAR path (created by MCP).""" + """Initialize OpenCode-backed agent engineer.""" headless = kwargs.pop("headless", False) har_dir = get_har_dir(run_id, output_dir) har_path = har_dir / "recording.har" @@ -329,7 +333,7 @@ def _get_opencode_mcp_config(self) -> dict | None: } async def analyze_and_generate(self) -> dict[str, Any] | None: - """Run auto mode with OpenCode MCP integration.""" + """Run agent mode via OpenCode (browser MCP registration only for MCP-backed providers).""" self.opencode_ui.header(self.run_id, self.prompt, self.opencode_model, mode="agent") self.opencode_ui.start_analysis() @@ -511,10 +515,11 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: class CopilotAutoEngineer: - """Auto mode using Copilot SDK: LLM controls browser via MCP while reverse engineering. + """Agent mode via Copilot SDK. - Uses composition rather than inheritance since CopilotEngineer requires lazy imports. - Delegates to CopilotEngineer for the core logic and adds MCP browser integration. + Delegates to ``CopilotEngineer`` while wiring browser tooling: MCP for ``auto`` / + ``chrome-mcp``, validated ``agent-browser`` CLI bootstrap for ``agent-browser``. + Uses composition because ``CopilotEngineer`` relies on lazy imports. """ def __init__( @@ -551,7 +556,7 @@ def stop_sync(self) -> None: self._engineer.stop_sync() async def analyze_and_generate(self) -> dict[str, Any] | None: - """Run auto mode with Copilot SDK and MCP browser integration.""" + """Run agent mode with Copilot SDK (MCP browsers or agent-browser CLI per provider).""" try: from copilot import CopilotClient, PermissionHandler except ImportError: diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index 9b9811a..6346f64 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -266,13 +266,15 @@ def _build_dry_run_payload( else: checks.append({"name": f"sdk:{sdk}", "status": "ok", "message": f"{sdk_env_var} present"}) - # 5. Node.js + npx for MCP servers (Playwright MCP, chrome-mcp, agent-browser via npx, …). + # 5. Node.js + npx — browser MCP subprocesses (`auto`, `chrome-mcp`) and the + # agent-browser CLI (`npx -y …`) all require Node + npx in PATH. node = shutil.which("node") if node is None: checks.append({ "name": "node", "status": "error", - "message": "node not found in PATH; required for MCP-based agent modes", + "message": "node not found in PATH; required for agent-mode browser tooling " + "(MCP browser servers plus Vercel agent-browser via npx)", }) else: try: @@ -288,7 +290,8 @@ def _build_dry_run_payload( checks.append({ "name": "npx", "status": "error", - "message": "npx not found in PATH; required for MCP-based agent tooling (downloads packages on demand)", + "message": "npx not found in PATH; required so npm can spawn MCP helpers and " + "fetch/run the pinned agent-browser CLI on demand", }) else: checks.append({"name": "npx", "status": "ok", "message": npx}) @@ -307,7 +310,8 @@ def _build_dry_run_payload( checks.append({ "name": "agent-browser:cli", "status": "error" if abe else "ok", - "message": abe or "npx can resolve agent_browser_npx_package / RAE_AGENT_BROWSER_PACKAGE", + "message": abe + or "npx successfully ran --help against the pinned package (npm caches downloads after first fetch)", }) # 8. Output dir writability — probe with a unique filename so we never @@ -1018,7 +1022,7 @@ def handle_settings(mode_color=THEME_PRIMARY): provider_choices = [ Choice(title="auto (Playwright MCP)", value="auto"), Choice(title="chrome-mcp (Chrome DevTools MCP)", value="chrome-mcp"), - Choice(title="agent-browser (Vercel CLI — shell only, no browser MCP)", value="agent-browser"), + Choice(title="agent-browser (Vercel CLI via shell/Bash)", value="agent-browser"), Choice(title="back", value="back"), ] provider = questionary.select( @@ -1038,10 +1042,23 @@ def handle_settings(mode_color=THEME_PRIMARY): console.print(f" [dim]updated[/dim] agent provider: {provider}") if provider == "agent-browser": console.print() - console.print(" [dim]no bundled MCP: the model runs `npx -y agent-browser …` via Bash.[/dim]") - console.print(" [dim]pin with `agent_browser_npx_package` in config or `RAE_AGENT_BROWSER_PACKAGE` env.[/dim]") - console.print(" [dim]optional extra instructions: `agent_browser_notes` (or RAE_AGENT_BROWSER_NOTES).[/dim]") - console.print(" [dim]first-time chromium: `npx -y agent-browser@0 install` (add --with-deps on Linux).[/dim]") + console.print( + " [dim]Browsing runs through Vercel's agent-browser CLI (not an MCP shim).[/dim]" + ) + console.print( + " [dim]The host already ran an `npx -y … --help` probe so npm resolves/caches " + "the package before streaming starts.[/dim]" + ) + console.print( + " [dim]Pin with `agent_browser_npx_package` in config or `RAE_AGENT_BROWSER_PACKAGE` env.[/dim]" + ) + console.print( + " [dim]Extra operator hints (cloud backends, corp proxy…): " + "`agent_browser_notes` or `RAE_AGENT_BROWSER_NOTES`.[/dim]" + ) + console.print( + " [dim]First-time chromium: `npx -y agent-browser@0 install` (add --with-deps on Linux).[/dim]" + ) console.print() elif provider == "chrome-mcp": console.print() @@ -1745,8 +1762,18 @@ def run_auto_capture( elif agent_provider == "agent-browser": console.print() - console.print(" [dim]agent-browser: no Playwright/Chrome MCP — the SDK agent shells Vercel's CLI (`npx -y …`).[/dim]") - console.print(" [dim]We prefetch with npx once; chromium bytes: global `npm i -g agent-browser && agent-browser install` if preferred.[/dim]") + console.print( + " [dim]agent-browser: Vercel CLI via shell (Reverse API Engineer does not attach " + "Playwright/Chrome browser MCP for this provider).[/dim]" + ) + console.print( + " [dim]Startup runs `npx -y --help` so npm resolves/caches the CLI; " + "the model prompt covers `skills get …` and cloud backend flows.[/dim]" + ) + console.print( + " [dim]Chromium install: `npm i -g agent-browser && agent-browser install` if you prefer " + "globals over on-demand npx.[/dim]" + ) console.print() sdk = config_manager.get("sdk", "claude") diff --git a/src/reverse_api/cursor_engineer.py b/src/reverse_api/cursor_engineer.py index 89463e7..d68f9b8 100644 --- a/src/reverse_api/cursor_engineer.py +++ b/src/reverse_api/cursor_engineer.py @@ -407,7 +407,7 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: class CursorAutoEngineer(CursorEngineer): - """Agent capture using Cursor SDK; browser via MCP unless provider is CLI-only.""" + """Agent capture using Cursor SDK—browser via MCP for auto/chrome-mcp or Vercel agent-browser CLI prompts for agent-browser.""" def __init__( self, diff --git a/src/reverse_api/prompts/auto/system.md b/src/reverse_api/prompts/auto/system.md index 34cba02..fe7e028 100644 --- a/src/reverse_api/prompts/auto/system.md +++ b/src/reverse_api/prompts/auto/system.md @@ -1,5 +1,9 @@ -You are an autonomous AI agent with browser control via {browser_tool_label} tools. -Your mission is to browse, monitor network traffic, and generate production-ready {language_name} API code. +You are an autonomous AI agent that automates browsing and captures network traffic for reverse engineering. + +For this provider your integration surface is: + +**{browser_tool_label}.** + **Core principle:** The generated scripts must work immediately with zero user effort. Hardcode all credentials, cookies, tokens, and session data you discover. No env vars, no config files, no manual setup required. If you observe a token refresh, cookie renewal, or login flow in the traffic, implement automatic re-authentication so the script doesn't go stale. diff --git a/src/reverse_api/prompts/auto/user_agent_browser.md b/src/reverse_api/prompts/auto/user_agent_browser.md index 1171058..999e786 100644 --- a/src/reverse_api/prompts/auto/user_agent_browser.md +++ b/src/reverse_api/prompts/auto/user_agent_browser.md @@ -9,23 +9,29 @@ ## Mandatory tooling -Reverse engineering relies on **`recording.har`** at **`{har_path}`**. You MUST drive browsing **only through the Vercel [agent-browser](https://github.com/vercel-labs/agent-browser) CLI** invoked from the shell (**Bash** / terminal MCP). Do **not** claim you used browser MCP tools — none are attached in this provider. +Reverse engineering relies on **`recording.har`** at **`{har_path}`**. You MUST drive browsing **only through the Vercel [agent-browser](https://github.com/vercel-labs/agent-browser) CLI** invoked from shell commands (terminal tools such as Bash). In this provider Reverse API Engineer does **not** register browser automation over MCP; the model uses the CLI exclusively. -Warm up context once: +### Host bootstrap (runs before the streaming session) + +Reverse API Engineer probes **`npx -y {agent_browser_npx_package} --help`**. That command forces npm/`npx` to **resolve the pinned package**, which usually **downloads it into npm's cache when it never ran on this disk before**, and repeats quickly afterwards. Failures surfaced there block the session so you don't discover a broken toolchain mid-flight. + +Warm up upstream context locally once you start: ```bash export AGENT_BROWSER_SESSION="{agent_browser_session}" npx -y {agent_browser_npx_package} skills get core --full ``` +Treat **`skills get core --full`** as mandatory for default/local flows. Confirm it exits cleanly; if commands error, rerun with `skills list`, pick the documented bundle (`core` ships with upstream), or escalate with **`npx … doctor`**. + {agent_browser_headed_hint}### Session + package - Stable session env: **`AGENT_BROWSER_SESSION={agent_browser_session}`** before every invocation (isolates refs/HAR for this run). -- Package pin: **`npx -y {agent_browser_npx_package}`** (already verified by the host). Users can override via `RAE_AGENT_BROWSER_PACKAGE` or config `agent_browser_npx_package`. +- Package pin: **`npx -y {agent_browser_npx_package}`**. Users can override via `RAE_AGENT_BROWSER_PACKAGE` or config `agent_browser_npx_package`. ### Cloud / remote browsers -If the operator hints at cloud backends (Bedrock AgentCore, Vercel Sandbox, …), run `skills list` then `skills get ` for the relevant skill bundle and prefer those flows—they stay version-matched to the CLI. +Upstream documents SaaS-hosted and remote backends. When the operator hints at cloud targets (Bedrock AgentCore, Vercel Sandbox, …), run **`skills list`** first to see what bundles ship with this CLI revision, **`skills get `**, then adopt the workflow packaged inside so flags stay lined up with **`npx -y {agent_browser_npx_package}`**. {agent_browser_notes_block} ## Workflow diff --git a/tests/test_auto_engineer.py b/tests/test_auto_engineer.py index 6b56d73..9764e47 100644 --- a/tests/test_auto_engineer.py +++ b/tests/test_auto_engineer.py @@ -325,10 +325,10 @@ def test_auto_mcp_config(self, tmp_path): assert "rae-playwright-mcp@latest" in config["args"] assert "--run-id" in config["args"] - def test_agent_browser_disables_mcp_helpers(self, tmp_path): - """Browser MCP shim is deliberately absent for agent-browser.""" + def test_agent_browser_no_mcp_config_path(self, tmp_path): + """`_get_mcp_config` is only for MCP-backed providers.""" eng = self._make_engineer(tmp_path, agent_provider="agent-browser") - with pytest.raises(RuntimeError, match="browser MCP disabled"): + with pytest.raises(RuntimeError, match="agent-browser uses the Vercel agent-browser CLI"): eng._get_mcp_config() diff --git a/website/content/docs/configuration/agent.mdx b/website/content/docs/configuration/agent.mdx index 0ba1df8..b0c0ace 100644 --- a/website/content/docs/configuration/agent.mdx +++ b/website/content/docs/configuration/agent.mdx @@ -3,7 +3,7 @@ title: Agent configuration description: Configure the AI agent that drives the browser in agent mode. --- -Agent mode runs an AI loop. Depending on agent provider this may attach **browser MCP**, or—in **`agent-browser` mode—instruct the model to invoke the **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)** directly from terminal tools. +Agent mode runs an AI loop. **`auto`** and **`chrome-mcp`** attach browser automation via **MCP**. **`agent-browser`** instead delegates browsing to the **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)**; Reverse API Engineer runs **`npx -y … --help`** before streaming so npm resolves/caches the package and obvious misconfigurations fail fast, while prompts cover skills + cloud backends. ## Agent providers @@ -32,9 +32,9 @@ profile. ### `agent-browser` -Uses **[Vercel agent-browser](https://github.com/vercel-labs/agent-browser)** as a standalone CLI—there is **no** bundled browser MCP shim in Reverse API Engineer for this slot. +Uses **[Vercel agent-browser](https://github.com/vercel-labs/agent-browser)** as a standalone CLI—not a Reverse API Engineer MCP shim. -Reverse API Engineer prefetch-checks `npx` so the pinned package can hydrate from the registry/cache, emits prompts referencing `skills get …`, and configures Claude-backed runs with **terminal-friendly tool allow-lists**. Other SDKs omit auxiliary browser MCP registration for `agent-browser` and leave shell access to whichever capabilities their upstream runtimes expose. +Startup runs **`npx -y … --help`** against your pin so npm **downloads into its cache when needed**, then verifies the toolchain before streaming. Prompts steer the model through **`skills get core --full`** (retry with **`skills list`** / matching bundles when the operator names a cloud/runtime backend via config `agent_browser_notes`). Claude SDK runs widen the tool allow-list for Bash-heavy flows; OpenCode/Copilot/Cursor omit Reverse API Engineer browser MCP registration for `agent-browser` and rely on their own shell primitives. Suggested JSON knobs: diff --git a/website/content/docs/installation.mdx b/website/content/docs/installation.mdx index 4a7b612..5c1d71e 100644 --- a/website/content/docs/installation.mdx +++ b/website/content/docs/installation.mdx @@ -7,8 +7,7 @@ description: Install Reverse API Engineer with uv or pip, then set up Playwright - **Python 3.11+** - SDK credentials for your selected backend. For the default Claude SDK, set **`ANTHROPIC_API_KEY`**. -- **Node.js + npx** for agent mode. `chrome-mcp` specifically requires - Node.js 20.19+. +- **Node.js + npx** for agent mode tooling (browser MCP stacks on `auto` / `chrome-mcp`, plus **`npx` bootstrapping** for **`agent-browser`**). `chrome-mcp` targets Node.js 20.19+. - A few minutes diff --git a/website/content/docs/modes/agent.mdx b/website/content/docs/modes/agent.mdx index 6d72bae..bd5fa55 100644 --- a/website/content/docs/modes/agent.mdx +++ b/website/content/docs/modes/agent.mdx @@ -4,7 +4,7 @@ description: Fully autonomous browser interaction. The AI browses, captures, and --- Agent mode hands the steering wheel to the AI. Describe the goal in plain -English; providers such as Playwright MCP or Chrome DevTools MCP route browser actions over MCP surfaces, whereas **`agent-browser`** mode describes shell-driven flows that call upstream's CLI (`npx`) directly. +English; providers such as Playwright MCP or Chrome DevTools MCP route browser actions over MCP, whereas **`agent-browser`** shells upstream's CLI directly after Reverse API Engineer **`npx -y … --help`** preflight and prompt-injected skill workflows. - **`agent-browser`** is the standalone [Vercel CLI](https://github.com/vercel-labs/agent-browser). Reverse API Engineer prefetch-checks `npx`, injects workflows that call **`skills get core --full`**, **`network har`** capture, `@eN` snapshot ergonomics, and optional cloud-browser skill bundles—all via the shell primitives your SDK exposes (Bash-first on Claude SDK runs). Pin versions with **`agent_browser_npx_package`** or **`RAE_AGENT_BROWSER_PACKAGE`** and pass operator-only guidance through **`agent_browser_notes`** when you rely on SaaS-hosted browsers. + **`agent-browser`** is the standalone [Vercel CLI](https://github.com/vercel-labs/agent-browser). Reverse API Engineer runs **`npx -y … --help`** up front so the package lands in npm's cache on first use, injects prompts for **`skills get core --full`**, **`network har …`**, `@eN` snapshots, and documents **`skills list` / `skills get …`** when operators point at cloud-hosted browsers. Pin via **`agent_browser_npx_package`** / **`RAE_AGENT_BROWSER_PACKAGE`**; add operator-only guidance with **`agent_browser_notes`** for remote stacks (AgentCore, Vercel Sandbox, …). Shell access is Bash-first on Claude SDK runs. From 96795f2c06bac215f210a1a4d6d7c04483b72071 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 24 May 2026 00:42:08 +0000 Subject: [PATCH 4/4] feat(agent-browser): prefer PATH CLI, npm global install, prompt notices - ensure_agent_browser_runtime returns AgentBrowserSetup (error + notices) - Use agent-browser on PATH when available; else npm install -g with yellow console notices; fall back to npx -y when npm missing or install fails - Prompts use {agent_browser_shell} reflecting resolved invocation (shlex-safe) - Update dry-run checks, README, CHANGELOG, and website agent docs accordingly Co-authored-by: kalil0321 --- CHANGELOG.md | 2 +- README.md | 8 +- src/reverse_api/agent_browser.py | 194 +++++++++++++++--- src/reverse_api/auto_engineer.py | 38 ++-- src/reverse_api/cli.py | 32 +-- src/reverse_api/cursor_engineer.py | 12 +- .../prompts/auto/user_agent_browser.md | 22 +- tests/test_agent_browser.py | 110 +++++++--- website/content/docs/configuration/agent.mdx | 6 +- website/content/docs/modes/agent.mdx | 4 +- 10 files changed, 315 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6b25a..654068e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **`agent_provider: "agent-browser"`**: Shell-driven **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)** workflow—agents run **`npx -y …`**, **`skills get …`** / **`skills list`** for cloud backends, and HAR captures per the injected prompt; Reverse API Engineer probes **`npx -y … --help`** upfront so npm **resolves the package** (downloads on first cache miss) and misconfiguration fails fast. No Reverse API Engineer browser MCP shim; optional knobs `agent_browser_npx_package` / `agent_browser_notes` (+ env equivalents). +- **`agent_provider: "agent-browser"`**: Shell-driven **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)**—RAE prefers an `agent-browser` binary on `PATH`, otherwise runs **`npm install -g `** (with a console notice), validates **`--help`**, and only then falls back to **`npx -y `** if npm cannot install. Prompts embed the resolved shell prefix plus **`skills get …` / `skills list`**, HAR flows, and optional `agent_browser_notes`. No bundled browser MCP shim; pin via `agent_browser_npx_package` / `RAE_AGENT_BROWSER_PACKAGE`. ### Added - **Cursor SDK support**: Added `sdk=cursor` / `--sdk cursor` engineering support through a bundled Node bridge around the Cursor TypeScript SDK. Cursor runs use the configured Cursor model (default `composer-2`), accept MCP server configuration, resume Cursor agents across follow-up turns, and normalize streamed tool output plus token usage into the existing TUI/message-store flow diff --git a/README.md b/README.md index f702ea7..14cda7c 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,14 @@ Cycle modes with **Shift+Tab**: Agent mode providers: - **auto** (default): Playwright MCP, single workflow for browsing + reverse engineering. - **chrome-mcp**: drives your real Chrome so you keep existing sessions/cookies. Requires Chrome 146+ and Node.js 20.19+. -- **agent-browser**: [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) **CLI** (not a Reverse API Engineer browser MCP server). Before capture the host runs **`npx -y --help`** so npm resolves/caches the package; the prompt instructs **`skills get core --full`**, **`skills list`** when cloud backends apply, and manual HAR capture. Strong default for VPS/CI. Tune with `agent_browser_npx_package`, `agent_browser_notes` (optional), env `RAE_AGENT_BROWSER_PACKAGE` / `RAE_AGENT_BROWSER_NOTES`. First-time Chromium bootstrap: `npx -y agent-browser install` (`--with-deps` on bare Linux). +- **agent-browser**: [Vercel agent-browser](https://github.com/vercel-labs/agent-browser) **CLI** (not a Reverse API Engineer browser MCP server). At session start RAE uses whatever `agent-browser` is already on `PATH`, otherwise runs **`npm install -g `** (same pin as config / `RAE_AGENT_BROWSER_PACKAGE`), prints a yellow notice, validates with **`--help`**, and only then falls back to **`npx -y `** if npm cannot install. Prompts embed the resolved shell prefix alongside **`skills get core --full`**, **`skills list`**, HAR phases, cloud notes from `agent_browser_notes`. Tune with `agent_browser_npx_package` (optional), env `RAE_AGENT_BROWSER_*`. First Chromium fetch: **`agent-browser install`** (add `--with-deps` on trimmed Linux). -Optionally prefetch for faster runs: +Optional sanity checks: ```bash -npx -y agent-browser@0 --help >/dev/null -npx -y agent-browser@0 doctor --offline --quick || true +agent-browser doctor --offline --quick || true +agent-browser skills list >/dev/null ``` ## Configuration diff --git a/src/reverse_api/agent_browser.py b/src/reverse_api/agent_browser.py index 65926d1..0b78cb0 100644 --- a/src/reverse_api/agent_browser.py +++ b/src/reverse_api/agent_browser.py @@ -1,16 +1,19 @@ """Helpers for agent-browser provider: bootstrap + prompt context. -Reverse API Engineer wires the upstream Vercel ``agent-browser`` **CLI** (not a bundled -browser MCP server). Models invoke it via shell tooling such as Bash. Before a session -we probe ``npx -y --help`` so npm resolves the package (typically downloading into -the cache when it has not run before) and configuration errors surface early. +Prefer a globally installed ``agent-browser`` on ``PATH``. If missing, run +``npm install -g `` (from config / ``RAE_AGENT_BROWSER_PACKAGE``) so the daemon +pairs consistently with Chromium; notify the operator when Reverse API Engineer performs +that install. Fall back to ``npx -y `` only when npm/global install cannot run. +Models invoke the CLI from shell tooling (for example Bash on Claude SDK runs). """ from __future__ import annotations import os +import shlex import shutil import subprocess +from dataclasses import dataclass from typing import Any from .utils import get_config_path @@ -29,9 +32,38 @@ }, ) +_SHELL_INVOKER: str | None = None + + +@dataclass(frozen=True) +class AgentBrowserSetup: + """Outcome of resolving the CLI before an agent-browser session.""" + + error: str | None = None + notices: tuple[str, ...] = () + + @property + def ok(self) -> bool: + return self.error is None + + +def reset_agent_browser_setup_cache() -> None: + """Clear cached shell invocation (for tests only).""" + + global _SHELL_INVOKER + _SHELL_INVOKER = None + + +def print_agent_browser_setup_notices(console, setup: AgentBrowserSetup) -> None: + """Emit install/fallback chatter before streaming.""" + + for note in setup.notices: + console.print(f"\n[yellow]agent-browser[/yellow]: {note}") + def _config_manager_snapshot() -> dict[str, Any]: """Load config defaults merged with ~/.reverse-api/config.json.""" + try: from .config import ConfigManager @@ -42,7 +74,7 @@ def _config_manager_snapshot() -> dict[str, Any]: def agent_browser_npx_package() -> str: - """Pinned npm specifier passed to ``npx -y `` (override with ``RAE_AGENT_BROWSER_PACKAGE``).""" + """Pinned npm specifier passed to ``npm install -g`` / ``npx -y`` (``RAE_AGENT_BROWSER_PACKAGE``).""" env = os.environ.get("RAE_AGENT_BROWSER_PACKAGE", "").strip() if env: @@ -52,7 +84,7 @@ def agent_browser_npx_package() -> str: def agent_browser_extra_notes() -> str: - """Optional user guidance (cloud browsers, corp proxy, …) from ``agent_browser_notes``.""" + """Optional user guidance from ``agent_browser_notes``.""" txt = (_config_manager_snapshot().get("agent_browser_notes") or "").strip() if txt: @@ -60,47 +92,139 @@ def agent_browser_extra_notes() -> str: return os.environ.get("RAE_AGENT_BROWSER_NOTES", "").strip() -def ensure_agent_browser_runtime() -> str | None: - """Verify the configured package is runnable via ``npx`` (download/cache + early failure). +def _probe_help_argv(argv_without_help: list[str]) -> str | None: + try: + proc = subprocess.run( + [*argv_without_help, "--help"], + capture_output=True, + text=True, + timeout=240, + check=False, + ) + except subprocess.TimeoutExpired: + return f"timed out running `{shlex.join(argv_without_help)} --help`." + except OSError as e: + return f"failed subprocess `{shlex.join(argv_without_help)}`: {e}" + + stdout = (proc.stdout or "").strip() + stderr = (proc.stderr or "").strip() + if proc.returncode == 0: + return None + blob = stderr or stdout or "(no output)" + blob = blob[:1600] + return ( + f"`{shlex.join(argv_without_help)} --help` failed with exit " + f"{proc.returncode}. Output (truncated): {blob}" + ) + + +def agent_browser_shell_invoker() -> str: + """Shell command prefix embedded in prompts (requires prior successful ``ensure_*`` unless testing).""" - Runs ``npx -y --help``. On a fresh machine npm may fetch the tarball into - its cache; on repeat runs npm reuses cached bits. ``None`` means the probe succeeded. + global _SHELL_INVOKER + if _SHELL_INVOKER is not None: + return _SHELL_INVOKER + pkg = agent_browser_npx_package() + return shlex.join(["npx", "-y", pkg]) + + +def _finalize_npx_invoker(notices: list[str]) -> AgentBrowserSetup: + global _SHELL_INVOKER + + if shutil.which("npx") is None: + return AgentBrowserSetup( + error=( + "npx not found on PATH. Install agent-browser globally " + "`npm install -g agent-browser`, or fix PATH so npm's global bin is visible." + ), + notices=tuple(notices), + ) - Otherwise returns a short human-readable failure string (missing ``node``/``npx``, - timeouts, nonzero exit). + pkg = agent_browser_npx_package() + err = _probe_help_argv(["npx", "-y", pkg]) + if err: + hint = "`RAE_AGENT_BROWSER_PACKAGE` or config `agent_browser_npx_package` can pin/coerce versions." + return AgentBrowserSetup( + error=f"{err} Adjust {hint}", + notices=tuple(notices), + ) + + inv = shlex.join(["npx", "-y", pkg]) + _SHELL_INVOKER = inv + return AgentBrowserSetup(notices=tuple(notices)) + + +def ensure_agent_browser_runtime() -> AgentBrowserSetup: + """Resolve ``agent-browser`` on PATH or install globally, else fall back to ``npx -y``. + + Successful runs stash the shell snippet for ``agent_browser_prompt_fields``. """ + global _SHELL_INVOKER + notices: list[str] = [] + _SHELL_INVOKER = None + + if shutil.which("agent-browser"): + err = _probe_help_argv(["agent-browser"]) + if err: + return AgentBrowserSetup(error=err, notices=tuple(notices)) + _SHELL_INVOKER = "agent-browser" + return AgentBrowserSetup(notices=tuple(notices)) + if shutil.which("node") is None: - return "node not found in PATH (needed to run agent-browser via npx)." - if shutil.which("npx") is None: - return "npx not found in PATH (needed to bootstrap agent-browser)." + return AgentBrowserSetup(error="node not found in PATH (needed to install/run agent-browser).") + + npm = shutil.which("npm") pkg = agent_browser_npx_package() + if npm is None: + notices.append("npm was not found on PATH; falling back to `npx -y …` instead of global install.") + return _finalize_npx_invoker(notices) + try: proc = subprocess.run( - ["npx", "-y", pkg, "--help"], + [npm, "install", "-g", pkg], capture_output=True, text=True, - timeout=240, + timeout=600, check=False, ) except subprocess.TimeoutExpired: - return f"timed out prefetching `{pkg}` with npx (network?)." + notices.append("`npm install -g …` timed out; falling back to `npx -y …` for this session.") + return _finalize_npx_invoker(notices) except OSError as e: - return f"failed to run npx: {e}" + notices.append(f"Could not spawn npm globally ({e}); falling back to `npx -y …`.") + return _finalize_npx_invoker(notices) - stderr = (proc.stderr or "").strip() - stdout = (proc.stdout or "").strip() - if proc.returncode == 0: - return None + if proc.returncode != 0: + blob = (proc.stderr or proc.stdout or "").strip() + notices.append( + "`npm install -g …` returned a non-zero status (showing truncated output " + f"below); falling back to `npx -y …`. Output: {(blob[:800] + '…') if len(blob) > 800 else blob or '(empty)'}" + ) + return _finalize_npx_invoker(notices) - err_blob = stderr or stdout or "(no output)" - err_blob = err_blob[:1600] - hint = "`RAE_AGENT_BROWSER_PACKAGE` env or config `agent_browser_npx_package` can pin/coerce versions." - return ( - f"npx prefetch of `{pkg}` failed (exit {proc.returncode}). Output: {err_blob} " - f"Try `npm i -g agent-browser && agent-browser install`, or adjust {hint}" + notices.append( + f'Installed upstream agent-browser with `npm install -g {pkg}`. ' + 'If Chromium is missing, run `agent-browser install` once (add `--with-deps` on trimmed Linux VMs). ' + 'Reuse this global install across runs for consistent browser pairing.' ) + if not shutil.which("agent-browser"): + return AgentBrowserSetup( + error=( + "Installed via npm but `agent-browser` is still not on PATH. " + "Append `npm bin -g` to PATH or reinstall with a toolchain that exposes the global shim." + ), + notices=tuple(notices), + ) + + err = _probe_help_argv(["agent-browser"]) + if err: + return AgentBrowserSetup(error=err, notices=tuple(notices)) + + _SHELL_INVOKER = "agent-browser" + return AgentBrowserSetup(notices=tuple(notices)) + def allowed_tools_agent_browser_agent_mode() -> list[str]: """Tool allow-list for Claude SDK runs when browsing is delegated to agent-browser.""" @@ -111,15 +235,21 @@ def allowed_tools_agent_browser_agent_mode() -> list[str]: def agent_browser_prompt_fields(*, run_id: str, headless: bool) -> dict[str, str]: """Variables for ``prompts/auto/user_agent_browser.md``.""" - pkg = agent_browser_npx_package() + shell = agent_browser_shell_invoker() session = f"rae-{run_id}" - headed = "" if headless else "Use the global `--headed` flag on subcommands that show a window when you need a visible browser (local debugging only).\n\n" + headed = ( + "" + if headless + else "Use the global `--headed` flag on subcommands that show a window when you need " + "a visible browser (local debugging only).\n\n" + ) notes = agent_browser_extra_notes() notes_block = "" if notes: notes_block = f"\n## Extra operator notes (from config or RAE_AGENT_BROWSER_NOTES)\n\n{notes}\n" return { - "agent_browser_npx_package": pkg, + "agent_browser_shell": shell, + "agent_browser_npx_package": agent_browser_npx_package(), "agent_browser_session": session, "agent_browser_headed_hint": headed, "agent_browser_notes_block": notes_block, diff --git a/src/reverse_api/auto_engineer.py b/src/reverse_api/auto_engineer.py index 168e9c5..61ffb60 100644 --- a/src/reverse_api/auto_engineer.py +++ b/src/reverse_api/auto_engineer.py @@ -1,8 +1,7 @@ """Auto mode engineers: LLM-controlled browsing with real-time reverse engineering. Providers **auto** and **chrome-mcp** attach a browser MCP server to the SDK. Provider -**agent-browser** shells the upstream Vercel ``agent-browser`` CLI (validated with an -``npx`` prefetch) instead of attaching browser MCP here. +**agent-browser** shells the upstream Vercel ``agent-browser`` CLI (auto-install via npm when missing, validated with ``--help``) instead of attaching browser MCP here. """ import asyncio @@ -21,6 +20,7 @@ agent_browser_prompt_fields, allowed_tools_agent_browser_agent_mode, ensure_agent_browser_runtime, + print_agent_browser_setup_notices, ) from .engineer import ClaudeEngineer from .opencode_engineer import OpenCodeEngineer, debug_log, format_error @@ -172,10 +172,11 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.ui.start_analysis() if self.agent_provider == "agent-browser": - abe = ensure_agent_browser_runtime() - if abe: - self.ui.error(abe) - self.message_store.save_error(abe) + ab_setup = ensure_agent_browser_runtime() + print_agent_browser_setup_notices(self.ui.console, ab_setup) + if not ab_setup.ok: + self.ui.error(ab_setup.error or "agent-browser setup failed") + self.message_store.save_error(ab_setup.error or "agent-browser setup failed") return None system_prompt, user_message = self._get_active_prompts() @@ -251,7 +252,10 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.ui.console.print("\n[dim]Make sure chrome-devtools-mcp is available: npx chrome-devtools-mcp@latest[/dim]") self.ui.console.print("[dim]Chrome 146+ required with auto-connect enabled at chrome://inspect/#remote-debugging[/dim]") elif self.agent_provider == "agent-browser": - self.ui.console.print("\n[dim]agent-browser: verify `npx` can run the configured package; adjust RAE_AGENT_BROWSER_PACKAGE env or agent_browser_npx_package in ~/.reverse-api/config.json[/dim]") + self.ui.console.print( + "\n[dim]agent-browser: ensure `agent-browser` is global (`npm install -g`) or pinned " + "via agent_browser_npx_package / RAE_AGENT_BROWSER_PACKAGE, and rerun `reverse-api-engineer`.[/dim]" + ) else: self.ui.console.print("\n[dim]Make sure rae-playwright-mcp is installed: npm install -g rae-playwright-mcp[/dim]") else: @@ -338,10 +342,12 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: self.opencode_ui.start_analysis() if self.agent_provider == "agent-browser": - abe = ensure_agent_browser_runtime() - if abe: - self.opencode_ui.error(abe) - self.message_store.save_error(abe) + ab_setup = ensure_agent_browser_runtime() + print_agent_browser_setup_notices(self.opencode_ui.console, ab_setup) + if not ab_setup.ok: + msg = ab_setup.error or "agent-browser setup failed" + self.opencode_ui.error(msg) + self.message_store.save_error(msg) return None system_prompt, user_message = self._get_active_prompts() @@ -625,10 +631,12 @@ def on_event(event: Any) -> None: }, } elif self.agent_provider == "agent-browser": - abe = ensure_agent_browser_runtime() - if abe: - eng.ui.error(abe) - eng.message_store.save_error(abe) + ab_setup = ensure_agent_browser_runtime() + print_agent_browser_setup_notices(eng.ui.console, ab_setup) + if not ab_setup.ok: + err = ab_setup.error or "agent-browser setup failed" + eng.ui.error(err) + eng.message_store.save_error(err) return None mcp_servers_payload = {} else: diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index 6346f64..75fd4e4 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -304,14 +304,20 @@ def _build_dry_run_payload( "message": "chrome-mcp without --headless requires Chrome 146+ with auto-connect enabled at chrome://inspect/#remote-debugging — this is not auto-checkable", }) - # 7. agent-browser CLI bootstrap (npx prefetch) + # 7. agent-browser CLI: PATH binary preferred; npm global install fallback. if agent_provider == "agent-browser": - abe = ensure_agent_browser_runtime() + ab_setup = ensure_agent_browser_runtime() + parts: list[str] = [] + if ab_setup.error: + parts.append(ab_setup.error) + elif ab_setup.ok: + parts.append("`agent-browser` CLI reachable (`--help` OK)") + if ab_setup.notices: + parts.extend(ab_setup.notices) checks.append({ "name": "agent-browser:cli", - "status": "error" if abe else "ok", - "message": abe - or "npx successfully ran --help against the pinned package (npm caches downloads after first fetch)", + "status": "error" if not ab_setup.ok else "ok", + "message": " | ".join(parts) if parts else "configured", }) # 8. Output dir writability — probe with a unique filename so we never @@ -1046,18 +1052,18 @@ def handle_settings(mode_color=THEME_PRIMARY): " [dim]Browsing runs through Vercel's agent-browser CLI (not an MCP shim).[/dim]" ) console.print( - " [dim]The host already ran an `npx -y … --help` probe so npm resolves/caches " - "the package before streaming starts.[/dim]" + " [dim]On first agent run RAE installs the CLI with `npm install -g ` when " + "`agent-browser` is missing (you’ll see a banner), then reuses the global shim.[/dim]" ) console.print( - " [dim]Pin with `agent_browser_npx_package` in config or `RAE_AGENT_BROWSER_PACKAGE` env.[/dim]" + " [dim]Pin via `agent_browser_npx_package` in config or `RAE_AGENT_BROWSER_PACKAGE` env.[/dim]" ) console.print( " [dim]Extra operator hints (cloud backends, corp proxy…): " "`agent_browser_notes` or `RAE_AGENT_BROWSER_NOTES`.[/dim]" ) console.print( - " [dim]First-time chromium: `npx -y agent-browser@0 install` (add --with-deps on Linux).[/dim]" + " [dim]Chrome download: `agent-browser install` once (add `--with-deps` on Linux).[/dim]" ) console.print() elif provider == "chrome-mcp": @@ -1767,12 +1773,12 @@ def run_auto_capture( "Playwright/Chrome browser MCP for this provider).[/dim]" ) console.print( - " [dim]Startup runs `npx -y --help` so npm resolves/caches the CLI; " - "the model prompt covers `skills get …` and cloud backend flows.[/dim]" + " [dim]Startup prefers PATH `agent-browser`, otherwise installs via " + "`npm install -g ` so Chromium pairs with a stable shim (with a banner); " + "only falls back to `npx -y` if npm cannot run.[/dim]" ) console.print( - " [dim]Chromium install: `npm i -g agent-browser && agent-browser install` if you prefer " - "globals over on-demand npx.[/dim]" + " [dim]First-time Chrome download: `agent-browser install` (add `--with-deps` on Linux).[/dim]" ) console.print() diff --git a/src/reverse_api/cursor_engineer.py b/src/reverse_api/cursor_engineer.py index d68f9b8..d7483ea 100644 --- a/src/reverse_api/cursor_engineer.py +++ b/src/reverse_api/cursor_engineer.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any -from .agent_browser import ensure_agent_browser_runtime +from .agent_browser import ensure_agent_browser_runtime, print_agent_browser_setup_notices from .base_engineer import BaseEngineer from .tui import ClaudeUI @@ -487,10 +487,12 @@ async def analyze_and_generate(self) -> dict[str, Any] | None: return None if self.agent_provider == "agent-browser": - abe = ensure_agent_browser_runtime() - if abe: - self.ui.error(abe) - self.message_store.save_error(abe) + ab_setup = ensure_agent_browser_runtime() + print_agent_browser_setup_notices(self.ui.console, ab_setup) + if not ab_setup.ok: + err = ab_setup.error or "agent-browser setup failed" + self.ui.error(err) + self.message_store.save_error(err) return None system_prompt, user_message = ClaudeAutoEngineer._build_auto_prompts(self) diff --git a/src/reverse_api/prompts/auto/user_agent_browser.md b/src/reverse_api/prompts/auto/user_agent_browser.md index 999e786..ff98725 100644 --- a/src/reverse_api/prompts/auto/user_agent_browser.md +++ b/src/reverse_api/prompts/auto/user_agent_browser.md @@ -13,25 +13,25 @@ Reverse engineering relies on **`recording.har`** at **`{har_path}`**. You MUST ### Host bootstrap (runs before the streaming session) -Reverse API Engineer probes **`npx -y {agent_browser_npx_package} --help`**. That command forces npm/`npx` to **resolve the pinned package**, which usually **downloads it into npm's cache when it never ran on this disk before**, and repeats quickly afterwards. Failures surfaced there block the session so you don't discover a broken toolchain mid-flight. +Reverse API Engineer prefers an **`agent-browser` binary already on PATH** and validates it with **`{agent_browser_shell} --help`**. When nothing is installed it runs **`npm install -g {agent_browser_npx_package}`** (pin from config or **`RAE_AGENT_BROWSER_PACKAGE`**), notifies you, then re-checks PATH. Only if npm/global installs cannot run does it transparently reuse **`{agent_browser_shell}`** wired through **`npx -y`** for that session—which can mean extra registry traffic on cold machines. Warm up upstream context locally once you start: ```bash export AGENT_BROWSER_SESSION="{agent_browser_session}" -npx -y {agent_browser_npx_package} skills get core --full +{agent_browser_shell} skills get core --full ``` -Treat **`skills get core --full`** as mandatory for default/local flows. Confirm it exits cleanly; if commands error, rerun with `skills list`, pick the documented bundle (`core` ships with upstream), or escalate with **`npx … doctor`**. +Treat **`skills get core --full`** as mandatory for default/local flows. Confirm it exits cleanly; if commands error, run **`skills list`** (per upstream), pick the documented bundle (`core` ships with upstream), or escalate with **`{agent_browser_shell} doctor`**. {agent_browser_headed_hint}### Session + package - Stable session env: **`AGENT_BROWSER_SESSION={agent_browser_session}`** before every invocation (isolates refs/HAR for this run). -- Package pin: **`npx -y {agent_browser_npx_package}`**. Users can override via `RAE_AGENT_BROWSER_PACKAGE` or config `agent_browser_npx_package`. +- Invocation prefix (**exactly how the host resolved the CLI):** **`{agent_browser_shell}`**. Pin for installs is **`{agent_browser_npx_package}`** (config / **`RAE_AGENT_BROWSER_PACKAGE`**). ### Cloud / remote browsers -Upstream documents SaaS-hosted and remote backends. When the operator hints at cloud targets (Bedrock AgentCore, Vercel Sandbox, …), run **`skills list`** first to see what bundles ship with this CLI revision, **`skills get `**, then adopt the workflow packaged inside so flags stay lined up with **`npx -y {agent_browser_npx_package}`**. +Upstream documents SaaS-hosted and remote backends. When the operator hints at cloud targets (Bedrock AgentCore, Vercel Sandbox, …), run **`skills list`** first so you fetch bundles that ship with **this CLI copy**, **`skills get `**, then adopt workflow files inside—the flags stay paired with **`{agent_browser_shell}`**. {agent_browser_notes_block} ## Workflow @@ -45,9 +45,9 @@ Use shell commands shaped like: ```bash export AGENT_BROWSER_SESSION="{agent_browser_session}" -npx -y {agent_browser_npx_package} network har start -npx -y {agent_browser_npx_package} open https://example.com -npx -y {agent_browser_npx_package} snapshot -i --json +{agent_browser_shell} network har start +{agent_browser_shell} open https://example.com +{agent_browser_shell} snapshot -i --json # … iterate … ``` @@ -62,12 +62,12 @@ Before reverse engineering MUST flush HAR to the canonical file **exact path** b ```bash export AGENT_BROWSER_SESSION="{agent_browser_session}" -npx -y {agent_browser_npx_package} network har stop {har_path} -npx -y {agent_browser_npx_package} close +{agent_browser_shell} network har stop {har_path} +{agent_browser_shell} close ``` ### Phase 4: REVERSE ENGINEER Read **`{har_path}`** and emit code under **`{scripts_dir}`** per the system prompt. -**VPS tips:** first-time hosts run `npx -y {agent_browser_npx_package} install` (add `--with-deps` on Linux). `doctor` diagnoses missing Chrome or permissions. +**VPS tips:** first-time hosts run `{agent_browser_shell} install` (add `--with-deps` on Linux). `doctor` diagnoses missing Chrome or permissions. diff --git a/tests/test_agent_browser.py b/tests/test_agent_browser.py index b21b54c..0983e86 100644 --- a/tests/test_agent_browser.py +++ b/tests/test_agent_browser.py @@ -9,53 +9,107 @@ from reverse_api import agent_browser +@pytest.fixture(autouse=True) +def clear_setup_cache(): + agent_browser.reset_agent_browser_setup_cache() + yield + agent_browser.reset_agent_browser_setup_cache() + + def test_allowed_tools_contains_bash(): tools = agent_browser.allowed_tools_agent_browser_agent_mode() assert "Bash" in tools -def test_ensure_agent_browser_missing_node(): +def test_ensure_missing_node(): with patch.object(agent_browser.shutil, "which", return_value=None): - err = agent_browser.ensure_agent_browser_runtime() - assert err is not None - assert "node" in err.lower() + st = agent_browser.ensure_agent_browser_runtime() + assert not st.ok + assert st.error and "node" in st.error.lower() -def test_ensure_agent_browser_missing_npx(): - def which_side(cmd: str) -> str | None: - return "/fake/node" if cmd == "node" else None +def test_global_binary_short_circuits_npm(): + help_ok = MagicMock(returncode=0, stderr="", stdout="ok") + + def which_side(name: str): + return "/fake/agent-browser" if name == "agent-browser" else None with patch.object(agent_browser.shutil, "which", side_effect=which_side): - err = agent_browser.ensure_agent_browser_runtime() - assert err is not None - assert "npx" in err.lower() + with patch.object(agent_browser.subprocess, "run", return_value=help_ok) as run: + st = agent_browser.ensure_agent_browser_runtime() + assert st.ok + assert st.notices == () + assert agent_browser.agent_browser_shell_invoker() == "agent-browser" + run.assert_called_once() + argv = run.call_args[0][0] + assert argv == ["agent-browser", "--help"] -def test_ensure_prefetch_ok(): - def which_side(cmd: str) -> str | None: - return f"/fake/{cmd}" if cmd in ("node", "npx") else None +def test_global_help_failure(): + proc = MagicMock(returncode=1, stderr="broken", stdout="") - proc = MagicMock(returncode=0, stderr="", stdout="usage") + def which_side(name: str): + return "/broken/ab" if name == "agent-browser" else None with patch.object(agent_browser.shutil, "which", side_effect=which_side): with patch.object(agent_browser.subprocess, "run", return_value=proc): + st = agent_browser.ensure_agent_browser_runtime() + assert not st.ok + assert st.error and "failed" in st.error.lower() + + +def test_npm_global_install_then_help(): + help_ok = MagicMock(returncode=0, stderr="", stdout="usage") + state = {"have_ab": False} + + def which_fn(cmd: str) -> str | None: + if cmd == "agent-browser": + return "/usr/bin/agent-browser" if state["have_ab"] else None + if cmd == "node": + return "/usr/bin/node" + if cmd == "npm": + return "/usr/bin/npm" + return None + + def npm_then_help(argv, **_kwargs): + if argv and str(argv[0]).endswith("npm") and len(argv) >= 2 and argv[1] == "install": + state["have_ab"] = True + return MagicMock(returncode=0, stderr="", stdout="") + return help_ok + + with patch.object(agent_browser.shutil, "which", side_effect=which_fn): + with patch.object(agent_browser.subprocess, "run", side_effect=npm_then_help) as run: with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="agent-browser@test"): - err = agent_browser.ensure_agent_browser_runtime() - assert err is None + st = agent_browser.ensure_agent_browser_runtime() + assert st.ok + assert any("Installed upstream agent-browser" in n for n in st.notices) + assert agent_browser.agent_browser_shell_invoker() == "agent-browser" + first_cmd = run.call_args_list[0][0][0] + assert first_cmd[:4] == ["/usr/bin/npm", "install", "-g", "agent-browser@test"] -def test_ensure_prefetch_nonzero(): - def which_side(cmd: str) -> str | None: - return f"/fake/{cmd}" if cmd in ("node", "npx") else None - proc = MagicMock(returncode=127, stderr="ENOTFOUND", stdout="") +def test_npm_missing_falls_back_npx(): + help_ok = MagicMock(returncode=0, stderr="", stdout="usage") + + def which_side(cmd: str) -> str | None: + if cmd == "agent-browser": + return None + if cmd == "node": + return "/bin/node" + if cmd == "npm": + return None + if cmd == "npx": + return "/bin/npx" + return None with patch.object(agent_browser.shutil, "which", side_effect=which_side): - with patch.object(agent_browser.subprocess, "run", return_value=proc): - with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="agent-browser@test"): - err = agent_browser.ensure_agent_browser_runtime() - assert err is not None - assert "prefetch" in err.lower() + with patch.object(agent_browser.subprocess, "run", return_value=help_ok): + with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="agent-browser@x"): + st = agent_browser.ensure_agent_browser_runtime() + assert st.ok + assert any("npm was not found" in n.lower() for n in st.notices) + assert agent_browser.agent_browser_shell_invoker() == "npx -y agent-browser@x" def test_npx_package_env_overrides(monkeypatch: pytest.MonkeyPatch): @@ -63,10 +117,12 @@ def test_npx_package_env_overrides(monkeypatch: pytest.MonkeyPatch): assert agent_browser.agent_browser_npx_package() == "agent-browser@fixture" -def test_prompt_fields_includes_notes_block(): +def test_prompt_fields_includes_shell_and_notes(): with patch("reverse_api.agent_browser.agent_browser_npx_package", return_value="pkg@x"): with patch("reverse_api.agent_browser.agent_browser_extra_notes", return_value="cloud hint"): - fields = agent_browser.agent_browser_prompt_fields(run_id="run1", headless=True) + with patch("reverse_api.agent_browser.agent_browser_shell_invoker", return_value="agent-browser"): + fields = agent_browser.agent_browser_prompt_fields(run_id="run1", headless=True) assert fields["agent_browser_session"] == "rae-run1" assert fields["agent_browser_npx_package"] == "pkg@x" + assert fields["agent_browser_shell"] == "agent-browser" assert "cloud hint" in fields["agent_browser_notes_block"] diff --git a/website/content/docs/configuration/agent.mdx b/website/content/docs/configuration/agent.mdx index b0c0ace..571483f 100644 --- a/website/content/docs/configuration/agent.mdx +++ b/website/content/docs/configuration/agent.mdx @@ -3,7 +3,7 @@ title: Agent configuration description: Configure the AI agent that drives the browser in agent mode. --- -Agent mode runs an AI loop. **`auto`** and **`chrome-mcp`** attach browser automation via **MCP**. **`agent-browser`** instead delegates browsing to the **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)**; Reverse API Engineer runs **`npx -y … --help`** before streaming so npm resolves/caches the package and obvious misconfigurations fail fast, while prompts cover skills + cloud backends. +Agent mode runs an AI loop. **`auto`** and **`chrome-mcp`** attach browser automation via **MCP**. **`agent-browser`** instead delegates browsing to the **[Vercel agent-browser CLI](https://github.com/vercel-labs/agent-browser)**; Reverse API Engineer looks for `agent-browser` on `PATH`, otherwise runs **`npm install -g …`** (using the same pin as config), surfaces a console notice, verifies **`--help`**, and only falls back to **`npx -y …`** if npm cannot complete the install. ## Agent providers @@ -34,7 +34,7 @@ profile. Uses **[Vercel agent-browser](https://github.com/vercel-labs/agent-browser)** as a standalone CLI—not a Reverse API Engineer MCP shim. -Startup runs **`npx -y … --help`** against your pin so npm **downloads into its cache when needed**, then verifies the toolchain before streaming. Prompts steer the model through **`skills get core --full`** (retry with **`skills list`** / matching bundles when the operator names a cloud/runtime backend via config `agent_browser_notes`). Claude SDK runs widen the tool allow-list for Bash-heavy flows; OpenCode/Copilot/Cursor omit Reverse API Engineer browser MCP registration for `agent-browser` and rely on their own shell primitives. +After the CLI is available, Reverse API Engineer embeds the resolved shell prefix in the user prompt (**`agent-browser …`** vs **`npx -y …`**) and validates with **`--help`**. Prompts steer the model through **`skills get core --full`** (retry with **`skills list`** / matching bundles when operators name a cloud/runtime backend via `agent_browser_notes`). Claude SDK runs widen the tool allow-list for Bash-heavy flows; OpenCode/Copilot/Cursor omit Reverse API Engineer browser MCP registration for `agent-browser` and rely on their own shell primitives. Suggested JSON knobs: @@ -48,7 +48,7 @@ Suggested JSON knobs: You can alternatively drive the same knobs with **`RAE_AGENT_BROWSER_PACKAGE`** or **`RAE_AGENT_BROWSER_NOTES`**. -First-time infra still needs Chromium bits just like upstream docs describe (`npm i -g agent-browser && agent-browser install`, `--with-deps` on trimmed Linux VMs). +First-time infra still needs Chromium bits just like upstream docs describe (`agent-browser install`, `--with-deps` on trimmed Linux VMs). ## How to switch providers diff --git a/website/content/docs/modes/agent.mdx b/website/content/docs/modes/agent.mdx index bd5fa55..bb2043c 100644 --- a/website/content/docs/modes/agent.mdx +++ b/website/content/docs/modes/agent.mdx @@ -4,7 +4,7 @@ description: Fully autonomous browser interaction. The AI browses, captures, and --- Agent mode hands the steering wheel to the AI. Describe the goal in plain -English; providers such as Playwright MCP or Chrome DevTools MCP route browser actions over MCP, whereas **`agent-browser`** shells upstream's CLI directly after Reverse API Engineer **`npx -y … --help`** preflight and prompt-injected skill workflows. +English; providers such as Playwright MCP or Chrome DevTools MCP route browser actions over MCP, whereas **`agent-browser`** shells upstream's CLI directly after Reverse API Engineer resolves the binary (PATH **`agent-browser`**, else **`npm install -g `**, else **`npx -y`**) and prompt-injected skill workflows. - **`agent-browser`** is the standalone [Vercel CLI](https://github.com/vercel-labs/agent-browser). Reverse API Engineer runs **`npx -y … --help`** up front so the package lands in npm's cache on first use, injects prompts for **`skills get core --full`**, **`network har …`**, `@eN` snapshots, and documents **`skills list` / `skills get …`** when operators point at cloud-hosted browsers. Pin via **`agent_browser_npx_package`** / **`RAE_AGENT_BROWSER_PACKAGE`**; add operator-only guidance with **`agent_browser_notes`** for remote stacks (AgentCore, Vercel Sandbox, …). Shell access is Bash-first on Claude SDK runs. + **`agent-browser`** is the standalone [Vercel CLI](https://github.com/vercel-labs/agent-browser). Reverse API Engineer publishes a banner when it **`npm install -g`'s** the pin, verifies **`--help`**, prefers the PATH shim across runs, falls back to **`npx`** only when npm fails, injects **`skills get core --full`**, **`network har …`**, `@eN` snapshots, and **`skills list` / `skills get`** when clouds come up (see **`agent_browser_notes`**). Pins live in **`agent_browser_npx_package`** / **`RAE_AGENT_BROWSER_PACKAGE`**. Shell access is Bash-first on Claude SDK runs.