diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..7f12b46 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,12 @@ +{ + "name": "browserbase", + "interface": { "displayName": "Browserbase" }, + "plugins": [ + { + "name": "browse", + "source": { "source": "local", "path": "." }, + "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, + "category": "Automation" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..0c58734 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,17 @@ +{ + "name": "browserbase", + "owner": { "name": "Browserbase", "email": "support@browserbase.com" }, + "metadata": { "description": "Browse CLI - browser automation for agents" }, + "plugins": [ + { + "name": "browse", + "source": ".", + "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI.", + "author": { "name": "Browserbase" }, + "homepage": "https://browserbase.com", + "repository": "https://github.com/browserbase/stagehand", + "license": "MIT", + "keywords": ["browser", "automation", "web-scraping", "stagehand", "browserbase"] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..ab0a1c9 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "browse", + "version": "0.3.0", + "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI.", + "author": { "name": "Browserbase" }, + "homepage": "https://browserbase.com", + "repository": "https://github.com/browserbase/stagehand", + "license": "MIT", + "skills": "./skills/" +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..c09eb10 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "browse", + "description": "Browse CLI — browser automation for agents", + "author": { "name": "Browserbase", "url": "https://browserbase.com" }, + "homepage": "https://browserbase.com", + "repository": "https://github.com/browserbase/stagehand", + "license": "MIT", + "keywords": ["browser", "automation", "browserbase", "stagehand"], + "skills": "./skills/", + "interface": { + "displayName": "Browserbase", + "shortDescription": "Browser automation for agents", + "longDescription": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI.", + "developerName": "Browserbase", + "category": "Automation", + "websiteURL": "https://browserbase.com", + "logo": "./assets/logo.svg" + } +} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index c8491db..eba8e24 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -5,20 +5,13 @@ "email": "support@browserbase.com" }, "metadata": { - "description": "Browser automation tools for Cursor", - "version": "1.0.0", - "pluginRoot": "plugins" + "description": "Browse CLI — browser automation for agents" }, "plugins": [ { "name": "browse", - "source": "browse", - "description": "Automate web browser interactions using natural language. Navigate pages, click elements, fill forms, extract data, and take screenshots -- all controlled by Cursor's model via MCP tools." - }, - { - "name": "functions", - "source": "functions", - "description": "Deploy serverless browser automation to Browserbase cloud using the bb CLI. Create, test, and publish cloud functions for scheduled or webhook-triggered browser tasks." + "source": ".", + "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI." } ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..a4f0ecd --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "browse", + "displayName": "Browserbase", + "version": "0.3.0", + "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI.", + "author": { + "name": "Browserbase", + "email": "support@browserbase.com" + }, + "repository": "https://github.com/browserbase/stagehand", + "license": "MIT", + "keywords": ["browser", "automation", "web-scraping", "browserbase", "stagehand"], + "logo": "assets/logo.svg", + "skills": "./skills/" +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..cd79671 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @browserbase/gtm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5fe140..798b14a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,16 +19,10 @@ jobs: with: node-version: 20 - # ---------- browse plugin ---------- - - name: Install and build browse - working-directory: plugins/browse - run: npm ci # postinstall runs `npm run build` automatically - - - name: Prune browse devDependencies - working-directory: plugins/browse - run: npm prune --omit=dev - # ---------- Validate ---------- + # This repo is a static, no-codebase marketplace catalog: JSON manifests + # and the browse skill. There is nothing to install or build, so the + # release job only validates and packages the manifests. - name: Validate plugin structure run: node scripts/validate-template.mjs @@ -39,8 +33,6 @@ jobs: --exclude='.git' \ --exclude='.env' \ --exclude='.env.local' \ - --exclude='.chrome-profile' \ - --exclude='.chrome-pid' \ --exclude='.DS_Store' \ . diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..8aa927c --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,55 @@ +name: Validate + +on: + pull_request: + push: + branches: + - main + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Validate plugin structure + run: node scripts/validate-template.mjs + + # Tags plugin.json's current committed version if no matching tag exists + # yet. Checked against tag existence, not a commit-to-commit diff, so a + # version bump split across multiple commits (e.g. an unsquashed merge) + # still gets tagged the moment the state lands on main. Never pushes to + # main itself (only a new tag ref), so this works regardless of branch + # protection on main. The pushed tag is what triggers release.yml to + # actually build and publish the release. + tag-release: + needs: validate + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Tag plugin.json's version if untagged + run: | + VERSION=$(node -pe "JSON.parse(require('fs').readFileSync('plugin.json', 'utf8')).version") + TAG="v${VERSION}" + + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "Tag ${TAG} already exists." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag "$TAG" + git push origin "$TAG" diff --git a/.gitignore b/.gitignore index 9604cb4..f1d5b94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,12 @@ # Dependencies node_modules/ -# Build output -dist/ - -# Chrome browser profile (copied from user profile) -.chrome-profile/ -.chrome-pid - -# Runtime artifacts -agent/browser_screenshots/*.png -agent/downloads/* -!agent/browser_screenshots/.gitkeep -!agent/downloads/.gitkeep - # Environment .env .env.local -# Network debug captures -network-capture.json +# Release artifacts +release.tar.gz # OS files .DS_Store diff --git a/.grok-plugin/plugin.json b/.grok-plugin/plugin.json new file mode 100644 index 0000000..642b838 --- /dev/null +++ b/.grok-plugin/plugin.json @@ -0,0 +1 @@ +{ "name": "browse", "version": "0.3.0", "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI.", "skills": "./skills/", "logo": "assets/logo.svg" } diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..dea6841 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,354 @@ +# Browse CLI + +Use `browse` as the primary Browserbase command-line interface. + +It can: + +- drive a local or Browserbase-hosted browser session +- inspect pages through accessibility snapshots, screenshots, DOM/text reads, and network capture +- interact with pages by refs, selectors, XPath, keyboard, mouse, files, and viewport controls +- manage Browserbase projects, sessions, contexts, extensions, fetch, and search APIs +- develop, publish, and invoke Browserbase Functions +- browse and scaffold Browserbase templates +- diagnose local or remote browser setup issues +- discover and install Browse.sh catalog skills +- install or refresh this Browse CLI skill + +## Setup Check + +Verify the CLI exists before relying on it: + +```bash +which browse || npm install -g browse +browse --help +``` + +Install or refresh this skill with: + +```bash +browse skills install +``` + +Use `browse --help` for exact flags before running unfamiliar commands. + +## Browser Target Selection + +Browser driver commands auto-start the browse daemon when needed. Choose the browser target per command with flags: + +```bash +browse open https://example.com --local +browse open https://example.com --local --headed +browse open https://example.com --remote +browse open https://example.com --remote --verified --proxies +browse open https://example.com --auto-connect +browse open https://example.com --cdp 9222 +browse open https://example.com --cdp ws://127.0.0.1:9222/devtools/browser/ +``` + +Use local mode for development, localhost, trusted sites, and fast iteration. Use `--auto-connect` only when the user explicitly wants to attach to an already-running debuggable Chrome session with existing cookies or login state; use `--local` when no debuggable Chrome is available. Use remote mode when Browserbase credentials are available and the site needs hosted browser infrastructure, Verified browser mode, CAPTCHA solving, proxies, or session persistence. + +`--local` requires Chrome or Chromium already installed on the machine. In containers, CI, and sandboxes with no browser installed, use `--remote` instead of `--local`. If `--local` fails with "No Chrome or Chromium found" and `BROWSERBASE_API_KEY` is set, switch to `--remote` — do not retry `--local`. + +For a Verified and/or proxied remote session, add `--verified` and/or `--proxies` to `--remote` — a single command that keeps the Browserbase session identity, so `browse status` and `browse doctor` report the session ID and live-view URL. `--verified` requires a Browserbase Scale plan. These flags only apply to `--remote` and are sticky for the session's lifetime, like `--headed`. Reach for `browse cloud sessions create` + `--cdp` only when you need session options `open` doesn't expose (region, keep-alive, contexts). + +Choose headed/headless and local/remote mode when starting a session. A running session keeps its mode: passing a conflicting flag such as `--headed` to an already-running headless session fails until you run `browse stop --session ` or target a different session. + +Use named sessions for any non-trivial work, especially when multiple agents or parallel tasks may run at once. Every browser command accepts `--session ` (or `-s `); the `BROWSE_SESSION` env var sets the default, and commands without either share the `default` session. + +If `BROWSE_SESSION` is already set in the environment, every command already targets that session — do not pass `--session` or invent a new name. An explicit `--session ` always overrides `BROWSE_SESSION` for that command, so only pass it to deliberately target a different session. + +```bash +browse open https://example.com --session research --local +browse snapshot --session research +``` + +Remote browser and cloud API commands require: + +```bash +export BROWSERBASE_API_KEY=... +``` + +## Browser Automation Workflow + +Start by opening the page, then inspect state, act, and verify. + +```bash +browse open https://example.com --session research --local +browse snapshot --session research +browse click @0-5 --session research +browse type "hello" --session research +browse snapshot --session research +browse stop --session research +``` + +Prefer `browse snapshot` over screenshots for most browser work. It is structured, fast, and returns refs like `@0-5` for reliable element interaction. Use screenshots when visual layout, images, or pixel-level state matter. + +Refs are refreshed on every snapshot. After clicks, form submits, navigation, or UI re-renders, take a new snapshot before using another ref. + +## Parallel Browser Work + +Use a different `--session` value for each independent browser task. Sessions isolate tabs, cookies, refs, and daemon state; parallel tasks that omit `--session` share the `default` session and overwrite each other's active page. + +```bash +browse open https://example.com/search-a --session search-a --local +browse open https://example.com/search-b --session search-b --local +browse snapshot --session search-a +browse snapshot --session search-b +``` + +When a task is complete, stop only that task's session: + +```bash +browse stop --session search-a +``` + +## Core Browser Commands + +Navigation: + +```bash +browse open +browse reload +browse back +browse forward +browse wait load +browse wait selector "#result" +``` + +Page state: + +```bash +browse snapshot # formatted tree only +browse snapshot --full # also include ref maps (xpathMap, urlMap) +browse get url +browse get title +browse get text body +browse get html "#main" +browse get value "#email" +browse get markdown body # page/element content as markdown +browse eval "document.title" # run JavaScript in the active page +browse screenshot # saves screenshot-.png, prints { "saved": "" } +browse screenshot --path page.png # choose the output path +browse screenshot --base64 # legacy: print base64 JSON to stdout (avoid in agent loops) +``` + +Interaction: + +```bash +browse click @0-5 +browse fill @0-8 "search query" +browse type "text for the focused element" +browse press Enter +browse select "select[name=country]" "United States" +browse upload @0-12 ./file.pdf +browse highlight @0-5 +browse is visible "#modal" +``` + +Mouse and viewport: + +```bash +browse mouse click 240 320 +browse mouse hover 240 320 +browse mouse drag 80 80 310 100 +browse mouse scroll 500 300 0 600 +browse viewport 1280 720 +browse cursor # show a visible cursor overlay +``` + +Tabs, network, and CDP: + +```bash +browse tab list +browse tab new https://example.com +browse tab switch +browse tab close # refuses to close the last tab +browse network on +browse network off +browse network path +browse network clear +browse cdp 9222 --pretty +``` + +Session management: + +```bash +browse doctor +browse doctor --json +browse status +browse stop +browse stop --force +``` + +Use `browse doctor` before debugging a broken browser session. Use `browse doctor --json` when another agent or CI needs structured diagnostics. + +If a page command reports that no active page is available, inspect and recover the named session: + +```bash +browse status --session research +browse tab list --session research +browse tab new https://example.com --session research +browse open https://example.com --session research +``` + +## Cloud APIs + +Use `browse cloud` for Browserbase platform APIs: + +```bash +browse cloud projects list +browse cloud projects get +browse cloud projects usage +browse cloud sessions create +browse cloud sessions create --proxies --verified +browse cloud sessions list +browse cloud sessions get +browse cloud sessions update +browse cloud sessions debug +browse cloud sessions logs +browse cloud sessions downloads get +browse cloud sessions uploads create ./file.pdf +browse cloud contexts create --name github +browse cloud contexts add github +browse cloud contexts list +browse cloud contexts get +browse cloud contexts update +browse cloud contexts delete +browse cloud extensions upload ./extension.zip +browse cloud extensions get +browse cloud extensions delete +browse cloud fetch https://example.com +browse cloud search "browser automation" +``` + +For remote sessions with context persistence: + +```bash +browse cloud sessions create --context-id --persist +``` + +Contexts persist cookies and local storage (logins) across sessions. Name a +context once with `--name` to save a local alias, then reuse the name anywhere a +context ID is accepted instead of memorizing the ID: + +```bash +browse cloud contexts create --name github # saves github -> ctx_... +browse cloud contexts add github # name a context you already have +browse cloud sessions create --context-id github --persist +browse cloud contexts list # show saved names +``` + +Use `--verified` when the task needs Browserbase Verified browser mode. To drive a Verified/proxied session directly, prefer `browse open --remote --verified --proxies` over create-then-attach — it keeps the session identity so `browse status`/`browse doctor` can report it. Use `browse cloud sessions create` for session options the driver flags don't cover (region, keep-alive, contexts, full `--stdin` body). + +Use `browse cloud fetch` when the user needs a simple HTTP fetch without browser interaction. It returns markdown-formatted page content by default; pass `--format raw` for the original response body or `--format json --schema ` for structured extraction. Use `browse cloud search` when the user asks for web search results. + +## Browserbase Functions + +Use `browse functions` to create, develop, publish, and invoke Browserbase Functions: + +```bash +browse functions init my-function +browse functions dev index.ts +browse functions publish index.ts +browse functions publish index.ts --dry-run +browse functions invoke --params '{"url":"https://example.com"}' +browse functions invoke --check-status +``` + +Functions commands use `BROWSERBASE_API_KEY`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. + +## Templates + +Use `browse templates` to discover and scaffold Browserbase starter templates: + +```bash +browse templates list +browse templates list --tag Python --source Browserbase +browse templates find google-trends-keywords +browse templates find amazon --json +browse templates clone google-trends-keywords +browse templates clone amazon-product-scraping --language python ./my-scraper +browse templates clone dynamic-form-filling ./form-bot --language typescript +``` + +Use `browse templates find` before cloning when the exact slug is uncertain. Use `--language typescript` or `--language python` to choose the generated project runtime when a template supports both. + +## Skills + +Install or refresh this bundled CLI skill: + +```bash +browse skills install +``` + +### Discovering Browse.sh Skills + +Browse.sh (https://browse.sh) is a catalog of site-specific browser automation skills. Each skill is scoped to one task on one website and identified by a `/` slug. An installed skill encodes a proven strategy for that site — API endpoints, selectors, anti-bot handling — so it completes the task faster and more reliably than exploring the site from scratch. + +Search the catalog proactively when: + +- the user asks to complete a task on a specific website — search the domain before automating it by hand +- the user asks for a task in a common category like flights, food delivery, reviews, recipes, tickets, jobs, or shopping — search the keyword +- the user asks "is there a skill for X" or wants new capabilities + +```bash +browse skills list # browse the catalog +browse skills find # search by slug, domain, title, description, category, alias, or tag +browse skills add / # install an exact slug +``` + +### Search Strategy + +Search the domain first, then the task, then broaden: + +```bash +browse skills find yelp.com # 1. exact domain +browse skills find yelp # 2. site name +browse skills find reviews # 3. task keyword +browse skills find "restaurant reviews" # 4. multi-word query +browse skills find food --limit 5 # 5. category keyword, capped +``` + +- Querying an exact slug (`browse skills find yelp.com/extract-reviews-2ikb22`) prints a detail view with the full description and install command. +- If a search returns nothing, try synonyms (`flights` vs `travel`, `food` vs `restaurants`) before concluding no skill exists. +- Each result shows a recommended method — `api`, `fetch`, `browser`, or `hybrid` — indicating how the skill drives the site. Install counts signal which skills are proven. +- Many slugs end in a generated suffix (`-2ikb22`), so never guess or construct them. Install only with an exact slug copied from `list` or `find` output: `browse skills add yelp.com/extract-reviews-2ikb22`. The installed skill becomes available to the agent as a regular skill; a new agent session may be needed to pick it up. + +### Output Formats + +Output is a table in a terminal and JSON when piped, so agents get structured JSON by default. Force a format with `--format table|json` or `--json`. + +```bash +browse skills find food --format table --limit 10 +browse skills find food --json | jq -r '.skills[] | "\(.slug)\t\(.title)"' +``` + +JSON output includes every match with full descriptions and ignores `--limit`; `browse skills list --json` returns the entire catalog, which is large. Prefer `browse skills find ` to narrow first, or `--format table --limit ` for a compact view. + +## Best Practices + +1. Run the real command and inspect its output instead of guessing. +2. Use `browse snapshot` before interacting so you have current refs. +3. Re-run `browse snapshot` after navigation or DOM-changing actions because refs can change. +4. Prefer refs from snapshots for clicks and uploads; use selectors or XPath when refs are unavailable. +5. Use `--local` for localhost and repeatable development; use `--remote` for protected sites or Browserbase-specific behavior. +6. Use a distinct `--session ` for each parallel or long-running task; commands without the flag share the `default` session. +7. Use `--auto-connect` only when attaching to an existing debuggable local Chrome session is intended. +8. Use `browse doctor` when session startup, browser discovery, CDP attach, or Browserbase auth looks wrong. +9. Never retry a failing command unchanged. If the same command fails twice with the same error, stop — run `browse doctor --json`, then change approach (fix the key, switch `--local`/`--remote`, or `browse stop --force` and start fresh). Repeating an identical failing command will keep failing. +10. Use `browse stop` (or `browse stop --session `) when finished to clean up daemon state. +11. For unfamiliar command details, run `browse --help` and follow the exact dash-case flags. + +## Troubleshooting + +- "No active page": run `browse status --session `, then `browse open --session ` or `browse tab new --session `; use `browse stop --force` if the daemon is stale. +- Chrome not found: use `--remote` with Browserbase credentials, install Chrome, or attach with `--cdp`. +- Action fails: run `browse snapshot` and use a visible ref from the current page state. +- 401 Unauthorized on `open`, `get`, or other driver commands: a set `BROWSERBASE_API_KEY` makes `browse` default to remote mode. Fix the key at https://browserbase.com/settings, unset it, or pass `--local` to run a managed local browser (no key needed). +- Same command fails twice with the same error: stop retrying — never retry a failing command unchanged. Init failures are cached for several seconds, so instant retries return identical errors. Run `browse doctor --json`, then change approach: fix the key, switch `--local`/`--remote`, or `browse stop --force` and start fresh. +- Remote command fails: verify `BROWSERBASE_API_KEY` and inspect `browse cloud projects list`. +- Session setup is unclear: run `browse doctor` or `browse doctor --json`. +- Protected site blocks local mode: retry with `--remote`. +- `browse skills find` returns nothing: broaden the query — bare domain, then site name, then a task keyword or synonym. +- `browse skills add` fails on `npx`: install Node.js from https://nodejs.org, then rerun. +- Newly added skill is not available: it installs for future sessions; list installed skills or start a new agent session. diff --git a/README.md b/README.md index 686cbe4..e28e92e 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,49 @@ -# Browserbase for Cursor +# browse plugin marketplace -Browser automation plugins for the Cursor IDE Marketplace. Control a real Chrome browser through natural language -- navigate, click, type, extract data, and take screenshots. +Install the [`browse`](https://github.com/browserbase/stagehand/tree/main/packages/cli) CLI as a native plugin in Claude Code, Cursor, Codex, Grok, and Gemini CLI. -## Plugins +This repo has no application code. It's a set of static JSON manifests that let each agent marketplace install and SHA-pin the `browse` plugin, plus the skill that teaches the agent to drive `browse` from the shell. -| Plugin | Description | -|--------|-------------| -| [browse](plugins/browse/) | Automate browser interactions via MCP tools. Navigate pages, fill forms, extract data, take screenshots. No API key needed for local mode. | -| [functions](plugins/functions/) | Deploy serverless browser automation to Browserbase cloud using the `bb` CLI. | +## What's inside + +| Path | Marketplace format | +|------|--------------------| +| `.claude-plugin/marketplace.json` | Claude Code marketplace | +| `.codex-plugin/plugin.json` | Codex plugin | +| `.cursor-plugin/marketplace.json` | Cursor marketplace | +| `.agents/plugins/marketplace.json` | Generic `.agents` marketplace | +| `.grok-plugin/plugin.json` | Grok plugin | +| `gemini-extension.json` | Gemini CLI extension (`GEMINI.md` context file) | + +See [`docs/add-a-plugin.md`](docs/add-a-plugin.md) for the full repo layout and how to update the plugin. ## Quick start -### Browse plugin +- **Claude Code**: add this repo as a plugin marketplace, then install the `browse` plugin. +- **Cursor**: add the marketplace, then install `browse`. +- **Codex / Grok**: add the repo as a plugin marketplace and install `browse`. +- **Gemini CLI**: install this repo as an extension (`gemini-extension.json` + `GEMINI.md`). -```bash -cd plugins/browse -npm install # Installs dependencies and auto-builds TypeScript -``` +Then just ask your agent: -The MCP server starts automatically when the plugin is active in Cursor. Just ask: +- *"Go to Hacker News and get the top 5 stories."* +- *"Fill out the signup form on example.com."* +- *"Take a screenshot of localhost:3000."* -- *"Go to Hacker News and get the top 5 stories"* -- *"Fill out the signup form on example.com"* -- *"Take a screenshot of localhost:3000"* +No local server, no build step, no API key required for local browsing. ### Browserbase cloud (optional) -For stealth browsing, proxies, and CAPTCHA solving: +Remote stealth sessions, proxies, and CAPTCHA solving use a Browserbase API key: ```bash export BROWSERBASE_API_KEY="your-api-key" -export BROWSERBASE_PROJECT_ID="your-project-id" ``` -Get credentials at [browserbase.com/settings](https://browserbase.com/settings). - -## Architecture - -The browse plugin runs an MCP server over stdio that wraps Playwright: - -``` -Cursor Model ──MCP tool call──▶ MCP Server ──Playwright──▶ Chrome - ▲ │ - │ │ - └──── screenshot file path ──────┘ - └──── interactive elements ──────┘ -``` - -- **No API key** needed for local Chrome automation -- **Headed browser** -- you can watch the automation happen -- **10 MCP tools**: navigate, click, type, snapshot, screenshot, scroll, evaluate, select, wait, close -- **Persistent sessions** -- cookies and login state carry over via Chrome profile - -## Validation - -```bash -node scripts/validate-template.mjs -``` - -## Troubleshooting - -### Chrome not found - -- **macOS/Windows**: Install from [google.com/chrome](https://www.google.com/chrome/) -- **Linux**: `sudo apt install google-chrome-stable` - -### Profile refresh - -```bash -rm -rf plugins/browse/.chrome-profile -``` +Get a key at [browserbase.com/settings](https://browserbase.com/settings). Local mode uses Chrome/Chromium on your machine and needs no key. ## Resources -- [Playwright Documentation](https://playwright.dev) +- [browse CLI](https://github.com/browserbase/stagehand/tree/main/packages/cli) +- [Adding or updating the plugin](docs/add-a-plugin.md) - [Browserbase](https://browserbase.com) -- [MCP Specification](https://modelcontextprotocol.io) -- [Cursor Marketplace](https://cursor.com) diff --git a/plugins/browse/assets/logo.svg b/assets/logo.svg similarity index 100% rename from plugins/browse/assets/logo.svg rename to assets/logo.svg diff --git a/docs/add-a-plugin.md b/docs/add-a-plugin.md index b7b3414..7e966f8 100644 --- a/docs/add-a-plugin.md +++ b/docs/add-a-plugin.md @@ -1,75 +1,67 @@ -# Add a plugin +# Repo layout -Add a new plugin under `plugins/` and register it in `.cursor-plugin/marketplace.json`. +This repo is a **static, single-plugin catalog** — repo root **is** the `browse` plugin, one plugin per repo. No application code, no build step. The plugin is CLI-only (a `SKILL.md` that shells out to a real CLI) — no MCP config, so there's no API key to embed in this public, git-pinned repo. -## 1. Create plugin directory +If Browserbase ever needs a second distinct plugin, it belongs in its own dedicated repo — not nested inside this one. A nested `plugins//` layout breaks third-party tools (e.g. Hermes Agent's skill "tap") that scan for `skills/` at repo root by convention. -Create a new folder: +## Layout ```text -plugins/my-new-plugin/ +. +├── .claude-plugin/marketplace.json # Claude Code marketplace (source: ".") +├── .claude-plugin/plugin.json # Claude Code plugin manifest +├── .codex-plugin/plugin.json # Codex plugin +├── .cursor-plugin/marketplace.json # Cursor marketplace (source: ".", repo validator reads this) +├── .cursor-plugin/plugin.json # Cursor plugin manifest +├── .agents/plugins/marketplace.json # Generic .agents marketplace (path: ".") +├── .grok-plugin/plugin.json # Grok plugin +├── gemini-extension.json # Gemini CLI extension +├── GEMINI.md # Gemini context file (CLI-only, no mcpServers) +├── plugin.json # Open Plugin spec manifest (vendor-neutral, e.g. `npx plugins add`) +├── assets/logo.svg +├── skills/browse/SKILL.md # YAML frontmatter: name + description +└── scripts/ + ├── validate-template.mjs # run by CI and locally, see "Validate" below + ├── gemini-sync.mjs # shared logic: derive GEMINI.md's expected content from SKILL.md + ├── sync-gemini.mjs # regenerates GEMINI.md; --check fails without writing + ├── version-sync.mjs # shared logic: reads the version every manifest must match + └── sync-version.mjs # propagates plugin.json's version to the others; --check fails without writing ``` -Add the required manifest: +Every per-format `plugin.json`'s `"skills"` and `"logo"` fields are relative to repo root (`./skills/`, `assets/logo.svg`), and every root marketplace file's `"source"`/`"path"` is `"."`. The root `plugin.json` is a separate, vendor-neutral manifest ([Open Plugin spec](https://github.com/vercel-labs/open-plugin-spec) v1.0.0); it doesn't replace or override any per-client manifest and only needs updating when the plugin's name, version, or metadata changes. -```text -plugins/my-new-plugin/.cursor-plugin/plugin.json -``` - -Example manifest: - -```json -{ - "name": "my-new-plugin", - "displayName": "My New Plugin", - "version": "0.1.0", - "description": "Describe what this plugin does", - "author": { - "name": "Your Org" - }, - "logo": "assets/logo.svg" -} -``` - -## 2. Add plugin components +## Updating the skill -Add only the components you need: +- `skills/browse/SKILL.md` — YAML frontmatter must include `name` and `description`; `allowed-tools: Bash` and instructions for shelling out to `browse`. This is a manual copy of the canonical `stagehand/packages/cli/skills/browse/SKILL.md` — edit there and re-copy here. Automated copy-on-release sync is tracked in [stagehand#2330](https://github.com/browserbase/stagehand/pull/2330). +- `GEMINI.md` — Gemini's extension format has no way to reference an external skill file, so it carries the same instructions as `SKILL.md`'s body, verbatim. After editing `skills/browse/SKILL.md`, run `node scripts/sync-gemini.mjs` to regenerate `GEMINI.md`. CI fails if the two drift out of sync. +- `assets/logo.svg` — the marketplace display logo. -- `rules/` with `.mdc` files (YAML frontmatter required) -- `skills//SKILL.md` (YAML frontmatter required) -- `agents/*.md` (YAML frontmatter required) -- `commands/*.(md|mdc|markdown|txt)` (frontmatter recommended) -- `hooks/hooks.json` and `scripts/*` for automation hooks -- `mcp.json` for MCP server definitions -- `assets/logo.svg` for marketplace display +## Bumping the version -## 3. Register in marketplace manifest +`plugin.json`'s `version` tracks this repo's own release tags (`v0.1.0`, `v0.2.0`, ...), the same as the git tags already used for GitHub Releases. The published Cursor marketplace listing is built by `release.yml`, which only runs on a tag push — merging to `main` alone doesn't update it. `plugin.json` is the single source of truth for the other four: `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.grok-plugin/plugin.json`, and `gemini-extension.json` must all match it exactly. -Edit `.cursor-plugin/marketplace.json` and append a new entry: +To cut a release, bump `plugin.json`'s `version` and run: -```json -{ - "name": "my-new-plugin", - "source": "./plugins/my-new-plugin", - "description": "Describe your plugin" -} +```bash +node scripts/sync-version.mjs ``` -`source` is the relative path from the repository root to the plugin folder. +CI fails if any of the five drift out of sync. Once that PR merges to `main`, a second CI job (`tag-release`, in `.github/workflows/validate.yml`) detects the version change and pushes the matching `vX.Y.Z` tag automatically — no one runs `git tag` by hand. That tag push is what triggers `release.yml` to actually validate, package, and publish the release. If `plugin.json`'s version didn't change on a given push to `main`, or a tag for that version already exists, `tag-release` is a no-op. -## 4. Validate +## Validate ```bash node scripts/validate-template.mjs ``` -Fix all reported errors before committing. +Fix all reported errors before committing. This also runs in CI on every pull request and on pushes to `main` (`.github/workflows/validate.yml`). -## 5. Common pitfalls +## Common pitfalls -- Plugin `name` not kebab-case. -- `source` path in marketplace manifest does not match folder name. -- Missing `.cursor-plugin/plugin.json` in plugin folder. -- Missing frontmatter keys (`name`, `description`) in skills, agents, or commands. -- Rule files missing frontmatter `description`. -- Broken relative paths for `logo`, `hooks`, or `mcpServers` in manifest files. +- Plugin `name` not kebab-case, or not matching a marketplace entry name. +- Missing `.cursor-plugin/plugin.json` at repo root. +- Missing frontmatter keys (`name`, `description`) in `SKILL.md`. +- Broken relative paths for `logo` or `skills` in a manifest. +- A marketplace `source`/`path` pointing at anything other than `"."` — this repo has no nested plugin folder anymore. +- Editing `skills/browse/SKILL.md` without running `node scripts/sync-gemini.mjs` afterward — CI fails if `GEMINI.md` drifts out of sync. +- Bumping `plugin.json`'s `version` without running `node scripts/sync-version.mjs` afterward — CI fails if the other manifests drift out of sync. diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..e8ed957 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,5 @@ +{ + "name": "browserbase", + "version": "0.3.0", + "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI." +} diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..bfda2da --- /dev/null +++ b/plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://open-plugins.com/schemas/1.0.0/plugin.schema.json", + "name": "browse", + "version": "0.3.0", + "description": "Browser automation for AI agents: navigate, extract, screenshot, and interact with real web pages via a single CLI.", + "author": { "name": "Browserbase", "url": "https://browserbase.com" }, + "homepage": "https://browserbase.com", + "repository": "https://github.com/browserbase/stagehand", + "license": "MIT", + "keywords": ["browser", "automation", "browserbase", "stagehand"] +} diff --git a/plugins/browse/.cursor-plugin/plugin.json b/plugins/browse/.cursor-plugin/plugin.json deleted file mode 100644 index 5e8491d..0000000 --- a/plugins/browse/.cursor-plugin/plugin.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "browse", - "displayName": "Browserbase Browse", - "version": "2.0.0", - "description": "Browser automation for Cursor. Navigate, click, fill forms, extract data, capture network, manage tabs, and take screenshots -- powered by browse-cli and controlled via MCP.", - "author": { - "name": "Browserbase", - "email": "support@browserbase.com" - }, - "license": "MIT", - "keywords": ["browser", "automation", "web-scraping", "browse-cli", "screenshots", "mcp", "stagehand"], - "logo": "assets/logo.svg" -} diff --git a/plugins/browse/README.md b/plugins/browse/README.md deleted file mode 100644 index ad082b6..0000000 --- a/plugins/browse/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Browserbase Browse - -Browser automation plugin for Cursor. Control a real Chrome browser using MCP tools -- navigate pages, click elements, fill forms, extract data, and take screenshots. - -**Zero API keys required** for local mode. Just install and go. - -## How it works - -The plugin runs an MCP server that wraps Playwright. Cursor's model sees the page through screenshots and interactive element snapshots, then issues precise tool calls (`browser_click`, `browser_type`, etc.) to control the browser. - -## Setup - -```bash -cd plugins/browse -npm install # Dependencies install + TypeScript auto-builds -``` - -The MCP server starts automatically when Cursor activates the plugin. - -## Usage - -Once installed, ask Cursor to browse: - -- *"Go to Hacker News and summarize the top 5 posts"* -- *"Fill out the contact form on example.com"* -- *"Take a screenshot of my dashboard at localhost:3000"* -- *"Extract all product prices from this page"* - -## Tools - -| Tool | Description | -|------|-------------| -| `browser_navigate` | Go to a URL | -| `browser_snapshot` | List interactive elements with CSS selectors | -| `browser_click` | Click an element | -| `browser_type` | Type into an input field | -| `browser_screenshot` | Capture the current page | -| `browser_scroll` | Scroll the page | -| `browser_select` | Pick a dropdown option | -| `browser_evaluate` | Run JavaScript | -| `browser_wait` | Wait for elements or time | -| `browser_close` | Close the browser | - -## Browserbase Cloud (optional) - -For stealth browsing, proxy support, and CAPTCHA solving, set environment variables: - -```bash -export BROWSERBASE_API_KEY="your-api-key" -export BROWSERBASE_PROJECT_ID="your-project-id" -``` - -Get credentials at [browserbase.com/settings](https://browserbase.com/settings). - -## Troubleshooting - -### Chrome not found - -Install Chrome for your platform: -- **macOS/Windows**: [google.com/chrome](https://www.google.com/chrome/) -- **Linux**: `sudo apt install google-chrome-stable` - -### Profile refresh - -To re-copy cookies from your main Chrome profile: - -```bash -rm -rf .chrome-profile -``` - -## Resources - -- [Playwright Documentation](https://playwright.dev) -- [Browserbase](https://browserbase.com) -- [MCP Specification](https://modelcontextprotocol.io) diff --git a/plugins/browse/agent/browser_screenshots/.gitkeep b/plugins/browse/agent/browser_screenshots/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/browse/agent/downloads/.gitkeep b/plugins/browse/agent/downloads/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/browse/mcp.json b/plugins/browse/mcp.json deleted file mode 100644 index fd8341f..0000000 --- a/plugins/browse/mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mcpServers": { - "browser": { - "command": "node", - "args": ["${CURSOR_PLUGIN_ROOT}/dist/src/mcp-server.js"], - "cwd": "${CURSOR_PLUGIN_ROOT}" - } - } -} diff --git a/plugins/browse/package-lock.json b/plugins/browse/package-lock.json deleted file mode 100644 index a57d8ce..0000000 --- a/plugins/browse/package-lock.json +++ /dev/null @@ -1,1565 +0,0 @@ -{ - "name": "browserbase-browse", - "version": "2.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "browserbase-browse", - "version": "2.0.0", - "hasInstallScript": true, - "dependencies": { - "@browserbasehq/browse-cli": "^0.1.4", - "@modelcontextprotocol/sdk": "^1.12.1", - "dotenv": "^17.2.4", - "playwright": "^1.52.0", - "zod": "^3.25.0" - }, - "bin": { - "browser": "dist/src/cli.js" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } - }, - "node_modules/@browserbasehq/browse-cli": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@browserbasehq/browse-cli/-/browse-cli-0.1.4.tgz", - "integrity": "sha512-JfviY4e/WEeLyoHo9ZIf0qWHeF6Sm4odHFOefJiVLdj0c0OlIkBqHTWDyiL0D6Eo4i/76gygWioAIAIPFsXV2A==", - "license": "MIT", - "dependencies": { - "commander": "^12.0.0", - "dotenv": "^16.4.5", - "pino": "^9.6.0", - "pino-pretty": "^13.0.0", - "ws": "^8.18.0" - }, - "bin": { - "browse": "dist/index.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "playwright": "^1.50.0" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": true - } - } - }, - "node_modules/@browserbasehq/browse-cli/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", - "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/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "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.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "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/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "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/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "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/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "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/dotenv": { - "version": "17.2.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", - "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "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.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "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.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", - "license": "MIT", - "dependencies": { - "ip-address": "10.0.1" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-copy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", - "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", - "license": "MIT" - }, - "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-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "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/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "license": "MIT" - }, - "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", - "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.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "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.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "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.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", - "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^4.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^4.0.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^5.0.2" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-pretty/node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "license": "MIT" - }, - "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/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "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/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "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/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "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/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "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.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "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/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "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/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "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.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "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/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - } - } -} diff --git a/plugins/browse/package.json b/plugins/browse/package.json deleted file mode 100644 index ce387bb..0000000 --- a/plugins/browse/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "browserbase-browse", - "version": "2.0.0", - "type": "module", - "private": true, - "bin": { - "browser": "./dist/src/cli.js" - }, - "scripts": { - "build": "tsc", - "postinstall": "npm run build" - }, - "dependencies": { - "@browserbasehq/browse-cli": "^0.1.4", - "@modelcontextprotocol/sdk": "^1.12.1", - "dotenv": "^17.2.4", - "playwright": "^1.52.0", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/node": "^22.15.0", - "typescript": "^5.8.0" - } -} diff --git a/plugins/browse/rules/browser-best-practices.mdc b/plugins/browse/rules/browser-best-practices.mdc deleted file mode 100644 index 14bac72..0000000 --- a/plugins/browse/rules/browser-best-practices.mdc +++ /dev/null @@ -1,21 +0,0 @@ ---- -description: Best practices for browser automation tasks -alwaysApply: false -globs: - - "**/*" ---- - -browser-automation-practices: - -- Always call browser_navigate before any other browser tool. -- Call browser_snapshot to get the accessibility tree with element refs before clicking, filling, or selecting. -- Use element refs (e.g., @0-5) from browser_snapshot to target elements -- never guess selectors. -- After each snapshot, refs are fresh. Always re-snapshot after page changes before acting again. -- Take a screenshot after important actions to confirm the expected result. -- Use browser_fill for form inputs (fills by ref and presses Enter by default). Use browser_type for raw text to the focused element. -- Use browser_press for keyboard shortcuts (Enter, Tab, Escape, Cmd+A, etc.). -- Use browser_wait after actions that trigger page loads or dynamic content before interacting again. -- Use browser_get for quick page info (url, title, text) instead of a full snapshot when you only need metadata. -- Use browser_evaluate with a single JavaScript expression for bulk data extraction over multiple individual tool calls. -- For multi-tab workflows, use browser_pages to list tabs and browser_switch_tab to change context. -- Close the browser with browser_close when the automation task is complete. diff --git a/plugins/browse/scripts/scrape-browserbase-usecases.ts b/plugins/browse/scripts/scrape-browserbase-usecases.ts deleted file mode 100644 index c9f3cf1..0000000 --- a/plugins/browse/scripts/scrape-browserbase-usecases.ts +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env npx tsx - -/** - * Scrapes all use cases from browserbase.com. - * - * Usage: - * npx tsx scripts/scrape-browserbase-usecases.ts - * - * Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in .env for cloud mode, - * otherwise falls back to local Chrome. - */ - -import { chromium, type Browser, type Page } from 'playwright-core'; -import { resolve, dirname, join } from 'path'; -import { fileURLToPath } from 'url'; -import { existsSync } from 'fs'; -import dotenv from 'dotenv'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const PLUGIN_ROOT = resolve(__dirname, '..'); - -dotenv.config({ path: join(PLUGIN_ROOT, '.env') }); - -// ── helpers ────────────────────────────────────────────────────────── - -function log(msg: string) { - console.error(`[scrape] ${msg}`); -} - -async function connectBrowser(): Promise<{ browser: Browser; page: Page }> { - const bbApiKey = process.env.BROWSERBASE_API_KEY; - const bbProjectId = process.env.BROWSERBASE_PROJECT_ID; - - if (bbApiKey && bbProjectId) { - log('Creating Browserbase session…'); - const res = await fetch('https://api.browserbase.com/v1/sessions', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'x-bb-api-key': bbApiKey }, - body: JSON.stringify({ projectId: bbProjectId }), - }); - if (!res.ok) throw new Error(`Browserbase error (${res.status}): ${await res.text()}`); - const session = (await res.json()) as { id: string; connectUrl: string }; - log(`Connected to session ${session.id}`); - - const browser = await chromium.connectOverCDP(session.connectUrl); - const context = browser.contexts()[0] ?? (await browser.newContext()); - const page = context.pages()[0] ?? (await context.newPage()); - return { browser, page }; - } - - // Local fallback – try CDP first, then launch - log('Using local Chrome…'); - let wsUrl: string | undefined; - try { - const r = await fetch('http://127.0.0.1:9222/json/version'); - wsUrl = ((await r.json()) as { webSocketDebuggerUrl: string }).webSocketDebuggerUrl; - } catch { /* not running */ } - - if (!wsUrl) { - const paths = [ - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - '/usr/bin/google-chrome-stable', - '/usr/bin/google-chrome', - ]; - const chromePath = paths.find((p) => existsSync(p)); - if (!chromePath) throw new Error('Chrome not found and no Browserbase credentials set.'); - const browser = await chromium.launch({ executablePath: chromePath, headless: false }); - const page = await browser.newPage(); - return { browser, page }; - } - - const browser = await chromium.connectOverCDP(wsUrl); - const context = browser.contexts()[0] ?? (await browser.newContext()); - const page = context.pages()[0] ?? (await context.newPage()); - return { browser, page }; -} - -// ── scraping logic ─────────────────────────────────────────────────── - -interface UseCaseTab { - name: string; - bullets: string[]; - caseStudies: { title: string; href: string }[]; -} - -interface UseCasePage { - name: string; - url: string; - headline: string; - subtitle: string; - problem: string; - solution: string; - features: string[]; - beforeAfter?: { before: string[]; after: string[] }; - stats?: string[]; -} - -async function scrapeHomepageTabs(page: Page): Promise { - log('Scraping homepage use-case tabs…'); - await page.goto('https://www.browserbase.com', { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(2000); - - const tabNames: string[] = await page.evaluate( - `Array.from(document.querySelectorAll('.UseCases_tab__pArYf button')).map(function(b){ return b.textContent.trim() })`, - ); - - const results: UseCaseTab[] = []; - - for (let i = 0; i < tabNames.length; i++) { - // Click the tab - await page.evaluate(`document.querySelectorAll('.UseCases_tab__pArYf button')[${i}].click()`); - await page.waitForTimeout(800); - - const data = await page.evaluate(`(() => { - var items = document.querySelectorAll('.UseCases_item__Nnse8'); - var links = document.querySelectorAll('.UseCases_link__wY_VS a'); - return { - bullets: Array.from(items).map(function(el){ return el.textContent.trim() }), - caseStudies: Array.from(links).map(function(a){ return { title: a.textContent.trim(), href: a.href } }) - }; - })()` - ) as { bullets: string[]; caseStudies: { title: string; href: string }[] }; - - results.push({ name: tabNames[i], ...data }); - log(` ✓ ${tabNames[i]} (${data.bullets.length} bullets, ${data.caseStudies.length} case studies)`); - } - - return results; -} - -async function scrapeUseCasePage(page: Page, url: string): Promise { - log(`Scraping ${url}…`); - await page.goto(url, { waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(1500); - - // Use a string-based evaluate to avoid tsx/esbuild injecting __name into browser context - const data = await page.evaluate(`(() => { - var mainText = (document.querySelector('main') || document.body).innerText || ''; - - // headline — use innerText of the first h1 child to avoid SSR duplication - var h1 = document.querySelector('h1'); - var headline = ''; - if (h1) { - var firstChild = h1.querySelector('span, div, p'); - headline = firstChild ? firstChild.innerText.trim() : h1.innerText.trim(); - // If still duplicated, take the shorter unique prefix - var lines = headline.split('\\n').filter(Boolean); - if (lines.length >= 2) headline = lines.slice(0, Math.ceil(lines.length / 2)).join(' '); - } - - // subtitle - var subEl = document.querySelector('[class*="hero"] p') || document.querySelector('[class*="Hero"] p'); - var subtitle = subEl ? subEl.textContent.trim() : ''; - - // problem / solution - var sections = mainText.split('\\n').filter(Boolean); - var problem = ''; - var solution = ''; - var inProblem = false; - var inSolution = false; - - for (var i = 0; i < sections.length; i++) { - var line = sections[i]; - if (/the problem/i.test(line)) { inProblem = true; inSolution = false; continue; } - if (/the solution|how browserbase/i.test(line)) { inSolution = true; inProblem = false; continue; } - if (/proof|real impact|before|what will you build/i.test(line)) { inProblem = false; inSolution = false; continue; } - if (inProblem) problem += (problem ? ' ' : '') + line; - if (inSolution) solution += (solution ? ' ' : '') + line; - } - - // features - var features = []; - var bullets = mainText.match(/Before/); - var featureSection = mainText.split(/Before|What will you build/)[0] || ''; - var featureLines = featureSection.split('\\n').filter(Boolean); - var captureFeat = false; - for (var j = 0; j < featureLines.length; j++) { - var fl = featureLines[j].trim(); - if (/your .* feature set|why browserbase/i.test(fl)) { captureFeat = true; continue; } - if (captureFeat && fl.length > 0) features.push(fl); - } - - // before / after — interleaved pairs (Before line, After line, Before line, After line…) - var beforeItems = []; - var afterItems = []; - var baMatch = mainText.match(/Before\\n([\\s\\S]*?)(?:\\n\\n|What will)/); - if (baMatch) { - var baLines = baMatch[1].split('\\n').map(function(s){return s.trim()}).filter(function(s){return s && s !== 'After'}); - for (var bi = 0; bi < baLines.length; bi += 2) { - if (baLines[bi]) beforeItems.push(baLines[bi]); - if (baLines[bi+1]) afterItems.push(baLines[bi+1]); - } - } - - return { - headline: headline, - subtitle: subtitle, - problem: problem, - solution: solution, - features: features, - beforeAfter: { before: beforeItems, after: afterItems } - }; - })()`); - - const name = url.split('/').pop() ?? ''; - return { name, url, ...(data as Omit) }; -} - -// ── main ───────────────────────────────────────────────────────────── - -async function main() { - const { browser, page } = await connectBrowser(); - - try { - // 1. Scrape the homepage tabs - const tabs = await scrapeHomepageTabs(page); - - // 2. Scrape each dedicated use-case page - const useCaseUrls = [ - 'https://www.browserbase.com/use-case/agents', - 'https://www.browserbase.com/use-case/workflow-automation', - 'https://www.browserbase.com/use-case/web-scraping', - ]; - - const pages: UseCasePage[] = []; - for (const url of useCaseUrls) { - pages.push(await scrapeUseCasePage(page, url)); - } - - // 3. Output - const output = { scrapedAt: new Date().toISOString(), homepageTabs: tabs, useCasePages: pages }; - console.log(JSON.stringify(output, null, 2)); - - log('Done!'); - } finally { - await browser.close(); - } -} - -main().catch((err) => { - console.error('Fatal:', err); - process.exit(1); -}); diff --git a/plugins/browse/skills/browser-automation/SKILL.md b/plugins/browse/skills/browser-automation/SKILL.md deleted file mode 100644 index 2fe3c69..0000000 --- a/plugins/browse/skills/browser-automation/SKILL.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: browser-automation -description: Automate web browser interactions using MCP tools. Use when the user asks to browse websites, navigate web pages, extract data from websites, take screenshots, fill forms, click buttons, or interact with web applications. ---- - -# Browser Automation - -Control a real Chrome browser through MCP tools. The browser launches automatically on first use via a background daemon -- no API keys required for local mode. - -## Available Tools - -| Tool | Purpose | -|------|---------| -| `browser_navigate` | Go to a URL (auto-starts browser daemon) | -| `browser_snapshot` | Get accessibility tree with element refs | -| `browser_click` | Click an element by ref (e.g., `@0-5`) | -| `browser_type` | Type text (goes to focused element) | -| `browser_fill` | Fill an input field by ref | -| `browser_press` | Press a key (Enter, Tab, Escape, Cmd+A, etc.) | -| `browser_select` | Pick a dropdown option by ref | -| `browser_screenshot` | Capture the current page | -| `browser_scroll` | Scroll at coordinates | -| `browser_hover` | Hover at coordinates | -| `browser_drag` | Drag from one point to another | -| `browser_click_xy` | Click at exact coordinates | -| `browser_evaluate` | Run JavaScript in the page | -| `browser_get` | Get page info (url, title, text, html, value, box) | -| `browser_wait` | Wait for load state, selector, or timeout | -| `browser_back` | Go back in history | -| `browser_forward` | Go forward in history | -| `browser_reload` | Reload current page | -| `browser_pages` | List all open tabs | -| `browser_new_tab` | Open a new tab | -| `browser_switch_tab` | Switch to a tab by index | -| `browser_close_tab` | Close a tab by index | -| `browser_network` | Capture/inspect HTTP requests | -| `browser_close` | Stop the browser daemon | - -## Core Workflow - -Every browser task follows this loop: - -1. **Navigate** to the target URL with `browser_navigate` -2. **Snapshot** the page with `browser_snapshot` to get the accessibility tree with element refs -3. **Act** using `browser_click`, `browser_fill`, `browser_press`, `browser_select` with the **refs** from the snapshot -4. **Verify** with `browser_screenshot` to confirm the action worked -5. **Repeat** steps 2-4 until the task is complete -6. **Close** the browser with `browser_close` when done - -## How Element Refs Work - -Call `browser_snapshot` -- it returns an accessibility tree where every interactive element has a **ref** like `[0-5]`. Use that ref to target the element. - -``` -RootWebArea "Example" url="https://example.com" - [0-0] link "Home" - [0-1] link "About" - [0-2] button "Sign In" - [0-5] textbox "Search" -``` - -Then act on elements using their ref: - -``` -browser_click({ ref: "@0-2" }) → clicks "Sign In" -browser_fill({ ref: "@0-5", value: "my query" }) → fills the search box -``` - -Refs are more reliable than CSS selectors because they're tied to the accessibility tree and work across iframes. - -### Ref formats (all equivalent) - -- `@0-5` -- @ prefix -- `0-5` -- plain ref -- `ref=0-5` -- explicit prefix - -### Snapshot also provides maps - -The full snapshot output includes: -- **xpathMap**: Cross-frame XPath selectors -- **cssMap**: CSS selectors when available -- **urlMap**: URLs extracted from links - -Use the compact flag (`-c`) for a concise tree without the maps. - -## Common Patterns - -### Fill out a form - -``` -1. browser_navigate({ url: "https://example.com/signup" }) -2. browser_snapshot() - → Find refs: [0-3] textbox "Email", [0-4] textbox "Password", [0-7] button "Submit" -3. browser_fill({ ref: "@0-3", value: "user@example.com" }) -4. browser_fill({ ref: "@0-4", value: "securepassword" }) -5. browser_click({ ref: "@0-7" }) -6. browser_screenshot() → Verify success -``` - -### Extract data from a page - -``` -1. browser_navigate({ url: "https://example.com/products" }) -2. browser_evaluate({ script: "JSON.stringify(Array.from(document.querySelectorAll('.product')).map(el => ({ name: el.querySelector('h2')?.textContent, price: el.querySelector('.price')?.textContent })))" }) -``` - -### Handle pagination - -``` -1. browser_navigate({ url: "https://example.com/results" }) -2. browser_evaluate({ script: "..." }) → Extract data from page 1 -3. browser_snapshot() → Find the "Next" button ref -4. browser_click({ ref: "@0-12" }) -5. browser_wait({ type: "load" }) -6. browser_evaluate({ script: "..." }) → Extract data from page 2 -``` - -### Deal with elements not in view - -``` -1. browser_snapshot() → Target element not found -2. browser_scroll({ x: 0, y: 0, deltaX: 0, deltaY: 500 }) -3. browser_snapshot() → Now the element appears -4. browser_click({ ref: "@0-8" }) -``` - -### Wait for dynamic content - -``` -1. browser_click({ ref: "@0-3" }) -2. browser_wait({ type: "selector", value: ".new-content", timeout: 10000 }) -3. browser_snapshot() → New elements available -``` - -### Multi-tab workflow - -``` -1. browser_navigate({ url: "https://example.com" }) -2. browser_new_tab({ url: "https://other.com" }) -3. browser_pages() → See tab indices -4. browser_switch_tab({ index: 0 }) → Back to first tab -5. browser_close_tab({ index: 1 }) → Close second tab -``` - -### Keyboard shortcuts - -``` -1. browser_press({ key: "Cmd+A" }) → Select all -2. browser_press({ key: "Cmd+C" }) → Copy -3. browser_press({ key: "Tab" }) → Move to next field -4. browser_press({ key: "Enter" }) → Submit -5. browser_press({ key: "Escape" }) → Close modal -``` - -### Network inspection - -``` -1. browser_network({ action: "on" }) → Start capturing -2. browser_navigate({ url: "https://api.example.com" }) -3. browser_network({ action: "path" }) → Get capture directory -4. browser_network({ action: "off" }) → Stop capturing -``` - -## Error Recovery - -- **Element not found**: Call `browser_snapshot` again -- the page may have changed. Refs update on each snapshot. -- **Page not loaded**: Use `browser_wait({ type: "load" })` before interacting. -- **Click intercepted**: Another element is covering the target. Scroll or close modals first. -- **Timeout**: Increase the timeout parameter or check if the page requires interaction first. -- **Stale page**: After navigation or form submission, always snapshot again before acting. -- **Daemon crashed**: Commands self-heal -- the daemon auto-restarts on the next command. - -## Tips - -- **Always navigate first** before any other interaction. -- **Always snapshot before acting** to get current refs -- refs change when the page changes. -- **Take screenshots after key actions** to verify state. -- **Use refs, not CSS selectors** -- they are more reliable across iframes and dynamic pages. -- **Use `browser_fill` for form inputs** -- it's designed for input fields and presses Enter by default. -- **Use `browser_type` for raw text** -- goes to the currently focused element. -- **Use `browser_press` for keyboard shortcuts** -- Enter, Tab, Escape, Cmd+A, etc. -- **Close the browser when done** to free system resources. -- **Use `browser_evaluate` for bulk extraction** -- it's faster than scraping element by element. -- **Use `browser_get` for quick info** -- url, title, text, html, value, box without a full snapshot. - -## Environment Modes - -The browser tools automatically detect which mode to use. No code changes or extra configuration from the model is needed -- just call the tools normally. The same tools work identically in both modes. - -- **Local** (default): Uses the user's installed Chrome. No API keys needed. A background daemon manages the browser lifecycle. -- **Browserbase cloud**: Activates automatically when a `.env` file in the plugin root contains `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID`. Provides stealth browsing, proxy support, and CAPTCHA solving. The CLI wrapper creates a cloud session and connects the daemon via WebSocket. - -The model should NEVER try to configure credentials or environment variables. If the user wants to switch modes, they edit the `.env` file themselves. diff --git a/plugins/browse/src/bb-browser.ts b/plugins/browse/src/bb-browser.ts deleted file mode 100644 index dd0f7d0..0000000 --- a/plugins/browse/src/bb-browser.ts +++ /dev/null @@ -1,594 +0,0 @@ -/** - * In-process Playwright backend for Browserbase cloud mode. - * - * Keeps the CDP WebSocket connection alive for the lifetime of the - * MCP server process. This is critical because Browserbase sessions - * die as soon as every client disconnects -- so we can't use the - * browse-cli's per-command `--ws` mode (which opens and closes a - * connection per invocation). - * - * Provides the same tool interface as the browse-cli based backend - * (refs from the accessibility tree, JSON results). - */ - -import { chromium, type Browser, type Page, type BrowserContext } from 'playwright'; -import { getOrCreateSession, clearCachedSession } from './browserbase.js'; - -// ---------- Persistent state ---------- - -let _browser: Browser | null = null; -let _context: BrowserContext | null = null; -let _page: Page | null = null; - -/** Map from ref string (e.g. "0-5") to AX node info for element resolution. */ -interface RefEntry { - role: string; - name: string; - /** For disambiguation when multiple nodes share role+name. */ - index: number; -} - -const _refMap = new Map(); - -// ---------- Connection management ---------- - -async function getPage(): Promise { - if (_page && !_page.isClosed()) { - return _page; - } - - // Reset stale references - _browser = null; - _context = null; - _page = null; - - const session = await getOrCreateSession(); - console.error(`[bb-browser] Connecting to Browserbase session ${session.id}`); - - _browser = await chromium.connectOverCDP(session.connectUrl); - _context = _browser.contexts()[0] ?? (await _browser.newContext()); - _page = _context.pages()[0] ?? (await _context.newPage()); - - // Wait for page to be ready - let retries = 0; - while (retries < 30) { - try { - await _page.evaluate('document.readyState'); - break; - } catch { - await new Promise((r) => setTimeout(r, 100)); - retries++; - } - } - - return _page; -} - -// ---------- Accessibility tree / ref mapping ---------- - -interface AXNode { - role: string; - name: string; - value?: string; - description?: string; - checked?: boolean | 'mixed'; - disabled?: boolean; - focused?: boolean; - children?: AXNode[]; -} - -/** - * Walk the accessibility tree, assign refs, build the ref map, - * and return a formatted string similar to browse-cli's compact snapshot. - */ -function buildRefTree(root: AXNode): string { - _refMap.clear(); - const lines: string[] = []; - /** Track role+name combos for disambiguation. */ - const roleCounts = new Map(); - let refIdx = 0; - - function walk(node: AXNode, depth: number) { - const indent = ' '.repeat(depth); - const ref = `0-${refIdx}`; - const roleKey = `${node.role}::${node.name}`; - const count = roleCounts.get(roleKey) ?? 0; - roleCounts.set(roleKey, count + 1); - - _refMap.set(ref, { role: node.role, name: node.name, index: count }); - refIdx++; - - // Build display line - const parts: string[] = []; - if (isInteractive(node.role)) { - parts.push(`[${ref}]`); - } - parts.push(node.role); - if (node.name) { - parts.push(`"${node.name}"`); - } - if (node.value) { - parts.push(`value="${node.value}"`); - } - if (node.checked !== undefined) { - parts.push(`checked=${String(node.checked)}`); - } - if (node.disabled) { - parts.push('disabled'); - } - if (node.focused) { - parts.push('focused'); - } - - lines.push(`${indent}${parts.join(' ')}`); - - if (node.children) { - for (const child of node.children) { - walk(child, depth + 1); - } - } - } - - walk(root, 0); - return lines.join('\n'); -} - -const INTERACTIVE_ROLES = new Set([ - 'link', - 'button', - 'textbox', - 'checkbox', - 'radio', - 'combobox', - 'listbox', - 'menuitem', - 'menuitemcheckbox', - 'menuitemradio', - 'option', - 'searchbox', - 'slider', - 'spinbutton', - 'switch', - 'tab', - 'treeitem', -]); - -function isInteractive(role: string): boolean { - return INTERACTIVE_ROLES.has(role); -} - -/** - * Resolve a ref (e.g. "@0-5" or "0-5") to a Playwright locator and click/fill/etc. - */ -function resolveRef(ref: string): RefEntry { - const clean = ref.replace(/^[@]|^ref=/i, ''); - const entry = _refMap.get(clean); - if (!entry) { - throw new Error( - `Unknown ref "${ref}". Run browser_snapshot first to get current refs.`, - ); - } - return entry; -} - -async function clickByRef(page: Page, ref: string): Promise { - const entry = resolveRef(ref); - const locator = page.getByRole(entry.role as Parameters[0], { - name: entry.name, - exact: false, - }); - const count = await locator.count(); - if (count === 0) { - throw new Error(`No element found for ref ${ref} (role=${entry.role}, name="${entry.name}")`); - } - if (entry.index > 0 && count > entry.index) { - await locator.nth(entry.index).click({ timeout: 10_000 }); - } else { - await locator.first().click({ timeout: 10_000 }); - } -} - -async function fillByRef(page: Page, ref: string, value: string, pressEnter = true): Promise { - const entry = resolveRef(ref); - const locator = page.getByRole(entry.role as Parameters[0], { - name: entry.name, - exact: false, - }); - const target = entry.index > 0 ? locator.nth(entry.index) : locator.first(); - await target.click({ timeout: 10_000 }); - await target.fill(value); - if (pressEnter) { - await page.keyboard.press('Enter'); - } -} - -async function selectByRef(page: Page, ref: string, values: string[]): Promise { - const entry = resolveRef(ref); - const locator = page.getByRole(entry.role as Parameters[0], { - name: entry.name, - exact: false, - }); - const target = entry.index > 0 ? locator.nth(entry.index) : locator.first(); - await target.selectOption(values, { timeout: 10_000 }); -} - -// ---------- CDP AX tree conversion ---------- - -interface CDPAXNode { - nodeId: string; - role: { type: string; value: string }; - name?: { type: string; value: string }; - value?: { type: string; value: string }; - description?: { type: string; value: string }; - properties?: Array<{ name: string; value: { type: string; value: unknown } }>; - childIds?: string[]; - parentId?: string; - backendDOMNodeId?: number; - ignored?: boolean; -} - -/** - * Convert flat CDP AX node array into a nested AXNode tree. - * When compact=true, filters to only include nodes with meaningful roles. - */ -function cdpNodesToTree(cdpNodes: CDPAXNode[], compact: boolean): AXNode | null { - if (cdpNodes.length === 0) return null; - - const SKIP_ROLES = new Set([ - 'none', - 'generic', - 'InlineTextBox', - 'LineBreak', - 'StaticText', - ]); - - // Build a map from nodeId to CDPAXNode - const nodeMap = new Map(); - for (const node of cdpNodes) { - nodeMap.set(node.nodeId, node); - } - - // Recursive converter - function convert(cdpNode: CDPAXNode): AXNode | null { - if (cdpNode.ignored) return null; - - const role = cdpNode.role?.value ?? 'unknown'; - const name = cdpNode.name?.value ?? ''; - const value = cdpNode.value?.value; - - // Build children - const children: AXNode[] = []; - if (cdpNode.childIds) { - for (const childId of cdpNode.childIds) { - const childCdp = nodeMap.get(childId); - if (childCdp) { - const childNode = convert(childCdp); - if (childNode) children.push(childNode); - } - } - } - - // In compact mode, skip non-meaningful nodes but keep their children - if (compact && SKIP_ROLES.has(role) && !name) { - if (children.length === 1) return children[0]; - if (children.length === 0) return null; - // Flatten: return a generic container with the children - return { role: 'group', name: '', children }; - } - - // Extract properties - let checked: boolean | 'mixed' | undefined; - let disabled: boolean | undefined; - let focused: boolean | undefined; - - if (cdpNode.properties) { - for (const prop of cdpNode.properties) { - switch (prop.name) { - case 'checked': - checked = prop.value.value === 'mixed' ? 'mixed' : Boolean(prop.value.value); - break; - case 'disabled': - disabled = Boolean(prop.value.value); - break; - case 'focused': - focused = Boolean(prop.value.value); - break; - } - } - } - - const node: AXNode = { role, name }; - if (value) node.value = value; - if (checked !== undefined) node.checked = checked; - if (disabled) node.disabled = disabled; - if (focused) node.focused = focused; - if (children.length > 0) node.children = children; - - return node; - } - - return convert(cdpNodes[0]); -} - -// ---------- Tool implementations ---------- - -export async function bbNavigate(url: string): Promise> { - const page = await getPage(); - await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); - await page.waitForTimeout(1000); - const title = await page.title(); - return { title, url: page.url() }; -} - -export async function bbClick(ref: string): Promise> { - const page = await getPage(); - await clickByRef(page, ref); - await page.waitForTimeout(500); - return { clicked: ref }; -} - -export async function bbClickXY(x: number, y: number): Promise> { - const page = await getPage(); - await page.mouse.click(x, y); - await page.waitForTimeout(300); - return { clicked: { x, y } }; -} - -export async function bbType(text: string, delay?: number): Promise> { - const page = await getPage(); - await page.keyboard.type(text, delay ? { delay } : undefined); - return { typed: text }; -} - -export async function bbFill( - ref: string, - value: string, - pressEnter = true, -): Promise> { - const page = await getPage(); - await fillByRef(page, ref, value, pressEnter); - return { filled: ref, value }; -} - -export async function bbPress(key: string): Promise> { - const page = await getPage(); - await page.keyboard.press(key); - return { pressed: key }; -} - -export async function bbSelect(ref: string, values: string[]): Promise> { - const page = await getPage(); - await selectByRef(page, ref, values); - return { selected: ref, values }; -} - -export async function bbSnapshot(compact = true): Promise> { - const page = await getPage(); - - // Use CDP to get the full accessibility tree (page.accessibility was removed) - const cdp = await page.context().newCDPSession(page); - try { - const { nodes } = (await cdp.send('Accessibility.getFullAXTree')) as { - nodes: CDPAXNode[]; - }; - const tree = cdpNodesToTree(nodes, compact); - if (!tree) { - return { snapshot: '(empty page)' }; - } - const formatted = buildRefTree(tree); - return { snapshot: formatted }; - } finally { - await cdp.detach().catch(() => {}); - } -} - -export async function bbScreenshot(path?: string, fullPage?: boolean): Promise> { - const page = await getPage(); - const opts: { path?: string; fullPage?: boolean; type: 'png' } = { type: 'png' }; - if (path) opts.path = path; - if (fullPage) opts.fullPage = true; - const buffer = await page.screenshot(opts); - - if (!path) { - // Save to temp location - const { mkdirSync, writeFileSync, existsSync } = await import('fs'); - const { join, resolve, dirname } = await import('path'); - const { fileURLToPath } = await import('url'); - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const pluginRoot = resolve(__dirname, '..', '..'); - const screenshotDir = join(pluginRoot, 'agent', 'browser_screenshots'); - if (!existsSync(screenshotDir)) mkdirSync(screenshotDir, { recursive: true }); - const ts = new Date().toISOString().replace(/[:.]/g, '-'); - path = join(screenshotDir, `screenshot-${ts}.png`); - writeFileSync(path, buffer); - } - - return { screenshot: path }; -} - -export async function bbScroll( - x: number, - y: number, - deltaX: number, - deltaY: number, -): Promise> { - const page = await getPage(); - await page.mouse.move(x, y); - await page.mouse.wheel(deltaX, deltaY); - await page.waitForTimeout(300); - return { scrolled: { x, y, deltaX, deltaY } }; -} - -export async function bbHover(x: number, y: number): Promise> { - const page = await getPage(); - await page.mouse.move(x, y); - return { hovered: { x, y } }; -} - -export async function bbDrag( - fromX: number, - fromY: number, - toX: number, - toY: number, -): Promise> { - const page = await getPage(); - await page.mouse.move(fromX, fromY); - await page.mouse.down(); - await page.mouse.move(toX, toY, { steps: 10 }); - await page.mouse.up(); - return { dragged: { fromX, fromY, toX, toY } }; -} - -export async function bbEvaluate(script: string): Promise> { - const page = await getPage(); - const result = await page.evaluate(script); - return { result: result ?? null }; -} - -export async function bbGet(what: string, selector?: string): Promise> { - const page = await getPage(); - switch (what) { - case 'url': - return { url: page.url() }; - case 'title': - return { title: await page.title() }; - case 'text': { - if (!selector) throw new Error('CSS selector required for "text"'); - const text = await page.locator(selector).innerText({ timeout: 10_000 }); - return { text }; - } - case 'html': { - if (!selector) throw new Error('CSS selector required for "html"'); - const html = await page.locator(selector).innerHTML({ timeout: 10_000 }); - return { html }; - } - case 'value': { - if (!selector) throw new Error('CSS selector required for "value"'); - const value = await page.locator(selector).inputValue({ timeout: 10_000 }); - return { value }; - } - case 'box': { - if (!selector) throw new Error('CSS selector required for "box"'); - const box = await page.locator(selector).boundingBox({ timeout: 10_000 }); - return { box }; - } - default: - throw new Error(`Unknown "what": ${what}. Use: url, title, text, html, value, box`); - } -} - -export async function bbWait( - type: string, - value?: string, - timeout?: number, -): Promise> { - const page = await getPage(); - switch (type) { - case 'load': - await page.waitForLoadState( - (value as 'load' | 'domcontentloaded' | 'networkidle') ?? 'load', - { timeout: timeout ?? 30_000 }, - ); - return { waited: 'load', state: value ?? 'load' }; - case 'selector': - if (!value) throw new Error('CSS selector required for wait type "selector"'); - await page.waitForSelector(value, { timeout: timeout ?? 30_000 }); - return { waited: 'selector', selector: value }; - case 'timeout': { - const ms = value ? parseInt(value, 10) : timeout ?? 5000; - await page.waitForTimeout(ms); - return { waited: 'timeout', ms }; - } - default: - throw new Error(`Unknown wait type: ${type}. Use: load, selector, timeout`); - } -} - -export async function bbBack(): Promise> { - const page = await getPage(); - await page.goBack({ waitUntil: 'domcontentloaded' }); - return { url: page.url() }; -} - -export async function bbForward(): Promise> { - const page = await getPage(); - await page.goForward({ waitUntil: 'domcontentloaded' }); - return { url: page.url() }; -} - -export async function bbReload(): Promise> { - const page = await getPage(); - await page.reload({ waitUntil: 'domcontentloaded' }); - return { url: page.url() }; -} - -export async function bbPages(): Promise> { - const page = await getPage(); - const context = page.context(); - const pages = context.pages().map((p, i) => ({ - index: i, - url: p.url(), - title: '', - })); - // Fill titles - for (const entry of pages) { - try { - entry.title = await context.pages()[entry.index].title(); - } catch { - // ignore - } - } - return { pages }; -} - -export async function bbNewTab(url?: string): Promise> { - const page = await getPage(); - const newPage = await page.context().newPage(); - if (url) { - await newPage.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); - } - _page = newPage; - return { url: newPage.url() }; -} - -export async function bbSwitchTab(index: number): Promise> { - const page = await getPage(); - const pages = page.context().pages(); - if (index < 0 || index >= pages.length) { - throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`); - } - _page = pages[index]; - return { switched: index, url: _page.url() }; -} - -export async function bbCloseTab(index?: number): Promise> { - const page = await getPage(); - const pages = page.context().pages(); - const target = index !== undefined ? index : pages.length - 1; - if (target < 0 || target >= pages.length) { - throw new Error(`Tab index ${target} out of range (0-${pages.length - 1})`); - } - const closing = pages[target]; - await closing.close(); - // Switch to the last remaining page - const remaining = page.context().pages(); - if (remaining.length > 0) { - _page = remaining[remaining.length - 1]; - } - return { closed: target }; -} - -export async function bbClose(): Promise> { - try { - if (_context) await _context.close().catch(() => {}); - if (_browser) await _browser.close().catch(() => {}); - } catch { - // already closed - } - _page = null; - _context = null; - _browser = null; - clearCachedSession(); - return { message: 'Browser closed' }; -} diff --git a/plugins/browse/src/browserbase.ts b/plugins/browse/src/browserbase.ts deleted file mode 100644 index 5f28027..0000000 --- a/plugins/browse/src/browserbase.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Browserbase cloud session management. - * - * Creates and caches a Browserbase session so the browse-cli daemon can - * connect to a remote Chrome instance via `--ws `. - * - * Shared by both the CLI wrapper and the MCP server. - */ - -export interface BrowserbaseSession { - id: string; - connectUrl: string; -} - -let _cachedSession: BrowserbaseSession | null = null; - -/** - * Returns true when BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are set. - */ -export function isBrowserbaseMode(): boolean { - return !!(process.env.BROWSERBASE_API_KEY && process.env.BROWSERBASE_PROJECT_ID); -} - -/** - * Create a Browserbase cloud browser session. - * Returns the session id and CDP WebSocket connect URL. - * - * The result is cached for the lifetime of this process so that - * multiple browse commands share the same remote browser. - */ -export async function getOrCreateSession(): Promise { - if (_cachedSession) { - return _cachedSession; - } - - const apiKey = process.env.BROWSERBASE_API_KEY; - const projectId = process.env.BROWSERBASE_PROJECT_ID; - - if (!apiKey || !projectId) { - throw new Error( - 'Browserbase cloud mode requires BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID environment variables.', - ); - } - - const res = await fetch('https://api.browserbase.com/v1/sessions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-bb-api-key': apiKey, - }, - body: JSON.stringify({ projectId }), - }); - - if (!res.ok) { - const errText = await res.text(); - throw new Error(`Browserbase session creation failed (${res.status}): ${errText}`); - } - - const session = (await res.json()) as BrowserbaseSession; - _cachedSession = session; - - return session; -} - -/** - * Clear the cached session (e.g. after `browse stop`). - */ -export function clearCachedSession(): void { - _cachedSession = null; -} diff --git a/plugins/browse/src/cli.ts b/plugins/browse/src/cli.ts deleted file mode 100644 index 721de60..0000000 --- a/plugins/browse/src/cli.ts +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -/** - * Thin CLI wrapper around @browserbasehq/browse-cli. - * - * - If BROWSERBASE_API_KEY + BROWSERBASE_PROJECT_ID are set, creates a - * Browserbase cloud session and injects `--ws ` so the - * browse daemon connects to the remote browser. - * - Otherwise, forwards all arguments directly to `browse` which manages - * a local Chrome instance via its daemon. - * - * Usage: - * browser open https://example.com - * browser snapshot -c - * browser click @0-5 - * browser stop - */ - -import { spawn } from 'child_process'; -import { join, resolve, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import dotenv from 'dotenv'; -import { isBrowserbaseMode, getOrCreateSession, clearCachedSession } from './browserbase.js'; - -// Resolve plugin root directory -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const PLUGIN_ROOT = resolve(__dirname, '..', '..'); - -// Load .env from the plugin root -dotenv.config({ path: join(PLUGIN_ROOT, '.env') }); - -/** - * Resolve the path to the browse binary from node_modules. - */ -function getBrowseBin(): string { - return join(PLUGIN_ROOT, 'node_modules', '.bin', 'browse'); -} - -async function main() { - const userArgs = process.argv.slice(2); - const browseBin = getBrowseBin(); - - // Build the argument list - const args: string[] = []; - - // If Browserbase cloud mode, create a session and inject --ws - if (isBrowserbaseMode()) { - try { - const session = await getOrCreateSession(); - console.error(`[browser] Using Browserbase cloud session ${session.id}`); - args.push('--ws', session.connectUrl); - } catch (err) { - console.error( - `[browser] Failed to create Browserbase session: ${err instanceof Error ? err.message : err}`, - ); - process.exit(1); - } - } - - // Append the user's arguments - args.push(...userArgs); - - // Check if this is a stop command -- clear cached session after - const isStop = userArgs[0] === 'stop'; - - // Spawn browse with inherited stdio so output flows through - const child = spawn(browseBin, args, { - stdio: 'inherit', - env: process.env, - }); - - child.on('error', (err) => { - console.error(`[browser] Failed to run browse: ${err.message}`); - console.error(`[browser] Make sure @browserbasehq/browse-cli is installed (npm install)`); - process.exit(1); - }); - - child.on('exit', (code) => { - if (isStop) { - clearCachedSession(); - } - process.exit(code ?? 0); - }); -} - -main().catch((err) => { - console.error('[browser] Fatal error:', err); - process.exit(1); -}); diff --git a/plugins/browse/src/mcp-server.ts b/plugins/browse/src/mcp-server.ts deleted file mode 100644 index e9a988c..0000000 --- a/plugins/browse/src/mcp-server.ts +++ /dev/null @@ -1,552 +0,0 @@ -#!/usr/bin/env node - -/** - * MCP server for browser automation. - * - * Two backends depending on environment: - * - * - **Local mode** (default): shells out to @browserbasehq/browse-cli. - * The browse daemon manages Chrome lifecycle and keeps state between calls. - * - * - **Browserbase cloud mode** (BROWSERBASE_API_KEY + BROWSERBASE_PROJECT_ID): - * keeps a Playwright CDP connection alive in-process. This is required - * because BB sessions die when the last client disconnects, so the - * per-command `--ws` approach doesn't work. - */ - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { z } from 'zod'; -import { execFile } from 'child_process'; -import { promisify } from 'util'; -import { join, resolve, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import dotenv from 'dotenv'; -import { isBrowserbaseMode, clearCachedSession } from './browserbase.js'; -import * as bb from './bb-browser.js'; - -// ---------- Resolve paths & load env ---------- - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const PLUGIN_ROOT = resolve(__dirname, '..', '..'); - -dotenv.config({ path: join(PLUGIN_ROOT, '.env') }); - -const BROWSE_BIN = join(PLUGIN_ROOT, 'node_modules', '.bin', 'browse'); -const BB_MODE = isBrowserbaseMode(); - -const execFileAsync = promisify(execFile); - -if (BB_MODE) { - console.error('[mcp] Browserbase cloud mode — using in-process Playwright connection'); -} else { - console.error('[mcp] Local mode — using browse-cli daemon'); -} - -// ---------- Local mode: browse-cli runner ---------- - -async function runBrowse(...args: string[]): Promise> { - const { stdout, stderr } = await execFileAsync(BROWSE_BIN, ['--json', ...args], { - timeout: 60_000, - env: process.env, - }); - - if (stderr) { - console.error('[browse]', stderr.trim()); - } - - try { - return JSON.parse(stdout) as Record; - } catch { - return { output: stdout.trim() }; - } -} - -// ---------- Helper ---------- - -function json(obj: Record, isError = false) { - return { - content: [{ type: 'text' as const, text: JSON.stringify(obj, null, 2) }], - ...(isError ? { isError: true } : {}), - }; -} - -async function safeTool(fn: () => Promise>) { - try { - return json(await fn()); - } catch (err) { - return json( - { success: false, error: err instanceof Error ? err.message : String(err) }, - true, - ); - } -} - -// ---------- MCP Server ---------- - -const server = new McpServer({ name: 'browser', version: '2.0.0' }); - -// --- browser_navigate --- -server.registerTool( - 'browser_navigate', - { - description: - 'Navigate to a URL in the browser. Launches Chrome automatically on first call. Returns page title and URL.', - inputSchema: { - url: z.string().describe('The URL to navigate to (include https://)'), - }, - }, - async ({ url }) => - safeTool(async () => (BB_MODE ? bb.bbNavigate(url) : runBrowse('open', url))), -); - -// --- browser_click --- -server.registerTool( - 'browser_click', - { - description: - 'Click an element on the page by ref. Use browser_snapshot first to get element refs. Refs look like @0-5.', - inputSchema: { - ref: z.string().describe('Element ref from snapshot (e.g., @0-5)'), - }, - }, - async ({ ref }) => - safeTool(async () => (BB_MODE ? bb.bbClick(ref) : runBrowse('click', ref))), -); - -// --- browser_click_xy --- -server.registerTool( - 'browser_click_xy', - { - description: 'Click at exact page coordinates.', - inputSchema: { - x: z.number().describe('X coordinate'), - y: z.number().describe('Y coordinate'), - }, - }, - async ({ x, y }) => - safeTool(async () => - BB_MODE ? bb.bbClickXY(x, y) : runBrowse('click_xy', String(x), String(y)), - ), -); - -// --- browser_type --- -server.registerTool( - 'browser_type', - { - description: - 'Type text into the currently focused element. Use browser_click or browser_fill to focus an element first.', - inputSchema: { - text: z.string().describe('Text to type'), - delay: z.number().optional().describe('Delay between keystrokes in ms'), - }, - }, - async ({ text, delay }) => - safeTool(async () => { - if (BB_MODE) return bb.bbType(text, delay); - const args = ['type', text]; - if (delay !== undefined) args.push('-d', String(delay)); - return runBrowse(...args); - }), -); - -// --- browser_fill --- -server.registerTool( - 'browser_fill', - { - description: - 'Fill an input field by ref. Clears the field first. Presses Enter after by default.', - inputSchema: { - ref: z.string().describe('Element ref from snapshot (e.g., @0-5)'), - value: z.string().describe('Value to fill'), - pressEnter: z - .boolean() - .optional() - .describe('Whether to press Enter after filling (default: true)'), - }, - }, - async ({ ref, value, pressEnter }) => - safeTool(async () => { - if (BB_MODE) return bb.bbFill(ref, value, pressEnter ?? true); - const args = ['fill', ref, value]; - if (pressEnter === false) args.push('--no-press-enter'); - return runBrowse(...args); - }), -); - -// --- browser_press --- -server.registerTool( - 'browser_press', - { - description: - 'Press a key or key combination. Examples: Enter, Tab, Escape, Cmd+A, Shift+Tab.', - inputSchema: { - key: z.string().describe('Key to press (e.g., Enter, Tab, Cmd+A)'), - }, - }, - async ({ key }) => - safeTool(async () => (BB_MODE ? bb.bbPress(key) : runBrowse('press', key))), -); - -// --- browser_select --- -server.registerTool( - 'browser_select', - { - description: 'Select option(s) in a