From c58b2a93da894282e825f218037625b0ff1fa771 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 4 Sep 2025 06:40:19 -0700 Subject: [PATCH] chore: use mcp implementation in Playwright (#992) --- .github/workflows/ci.yml | 50 +- .github/workflows/copilot-setup-steps.yml | 44 - .npmignore | 1 - Dockerfile | 5 - cli.js | 2 +- eslint.config.js | 230 -- examples/generate-test.md | 10 - extension/README.md | 48 - extension/icons/icon-128.png | Bin 6352 -> 0 bytes extension/icons/icon-16.png | Bin 571 -> 0 bytes extension/icons/icon-32.png | Bin 1258 -> 0 bytes extension/icons/icon-48.png | Bin 2043 -> 0 bytes extension/manifest.json | 35 - extension/package-lock.json | 1884 ---------- extension/package.json | 35 - extension/playwright.config.ts | 31 - extension/src/background.ts | 222 -- extension/src/relayConnection.ts | 178 - extension/src/ui/connect.css | 206 -- extension/src/ui/connect.html | 29 - extension/src/ui/connect.tsx | 233 -- extension/src/ui/status.html | 13 - extension/src/ui/status.tsx | 110 - extension/src/ui/tabItem.tsx | 67 - extension/src/ui/tsconfig.json | 4 - extension/tests/extension.spec.ts | 306 -- extension/tsconfig.json | 22 - extension/tsconfig.ui.json | 19 - extension/vite.config.mts | 54 - extension/vite.sw.config.mts | 31 - index.js | 2 +- package-lock.json | 3733 +------------------- package.json | 46 +- playwright.config.ts | 4 - src/DEPS.list | 5 - src/browser/DEPS.list | 5 - src/browser/actions.d.ts | 172 - src/browser/browserContextFactory.ts | 253 -- src/browser/browserServerBackend.ts | 88 - src/browser/codegen.ts | 53 - src/browser/config.ts | 327 -- src/browser/context.ts | 276 -- src/browser/response.ts | 201 -- src/browser/sessionLog.ts | 176 - src/browser/tab.ts | 313 -- src/browser/tools.ts | 60 - src/browser/tools/DEPS.list | 3 - src/browser/tools/common.ts | 63 - src/browser/tools/console.ts | 36 - src/browser/tools/dialogs.ts | 55 - src/browser/tools/evaluate.ts | 61 - src/browser/tools/files.ts | 52 - src/browser/tools/form.ts | 60 - src/browser/tools/install.ts | 56 - src/browser/tools/keyboard.ts | 88 - src/browser/tools/mouse.ts | 113 - src/browser/tools/navigate.ts | 62 - src/browser/tools/network.ts | 49 - src/browser/tools/pdf.ts | 46 - src/browser/tools/screenshot.ts | 91 - src/browser/tools/snapshot.ts | 165 - src/browser/tools/tabs.ts | 64 - src/browser/tools/tool.ts | 70 - src/browser/tools/utils.ts | 85 - src/browser/tools/verify.ts | 148 - src/browser/tools/wait.ts | 65 - src/extension/DEPS.list | 4 - src/extension/cdpRelay.ts | 422 --- src/extension/extensionContextFactory.ts | 65 - src/extension/protocol.ts | 42 - src/index.ts | 51 - src/log.ts | 25 - src/package.ts | 20 - src/program.ts | 139 - src/sdk/DEPS.list | 1 - src/sdk/README.md | 1 - src/sdk/bundle.ts | 48 - src/sdk/http.ts | 156 - src/sdk/inProcessTransport.ts | 92 - src/sdk/manualPromise.ts | 127 - src/sdk/mdb.ts | 236 -- src/sdk/proxyBackend.ts | 127 - src/sdk/server.ts | 157 - src/sdk/tool.ts | 46 - src/vscode/DEPS.list | 7 - src/vscode/host.ts | 204 -- src/vscode/main.ts | 78 - tests/cdp.spec.ts | 93 - tests/click.spec.ts | 59 - tests/config.spec.ts | 80 - tests/console.spec.ts | 100 - tests/core.spec.ts | 158 - tests/device.spec.ts | 45 - tests/dialogs.spec.ts | 255 -- tests/evaluate.spec.ts | 99 - tests/files.spec.ts | 151 - tests/form.spec.ts | 123 - tests/headed.spec.ts | 49 - tests/http.spec.ts | 255 -- tests/iframes.spec.ts | 46 - tests/install.spec.ts | 26 - tests/launch.spec.ts | 169 - tests/mdb.spec.ts | 217 -- tests/network.spec.ts | 47 - tests/pdf.spec.ts | 88 - tests/request-blocking.spec.ts | 82 - tests/roots.spec.ts | 83 - tests/screenshot.spec.ts | 327 -- tests/session-log.spec.ts | 275 -- tests/sse.spec.ts | 232 -- tests/tabs.spec.ts | 155 - tests/trace.spec.ts | 37 - tests/type.spec.ts | 138 - tests/verify.spec.ts | 522 --- tests/vscode.spec.ts | 119 - tests/wait.spec.ts | 107 - tests/webdriver.spec.ts | 40 - tsconfig.all.json | 4 - tsconfig.json | 14 - utils/update-readme.js => update-readme.js | 8 +- utils/check-deps.js | 230 -- utils/copyright.js | 15 - utils/generate-links.js | 6 - utils/set-version.js | 66 - 124 files changed, 49 insertions(+), 17534 deletions(-) delete mode 100644 .github/workflows/copilot-setup-steps.yml delete mode 100644 eslint.config.js delete mode 100644 examples/generate-test.md delete mode 100644 extension/README.md delete mode 100644 extension/icons/icon-128.png delete mode 100644 extension/icons/icon-16.png delete mode 100644 extension/icons/icon-32.png delete mode 100644 extension/icons/icon-48.png delete mode 100644 extension/manifest.json delete mode 100644 extension/package-lock.json delete mode 100644 extension/package.json delete mode 100644 extension/playwright.config.ts delete mode 100644 extension/src/background.ts delete mode 100644 extension/src/relayConnection.ts delete mode 100644 extension/src/ui/connect.css delete mode 100644 extension/src/ui/connect.html delete mode 100644 extension/src/ui/connect.tsx delete mode 100644 extension/src/ui/status.html delete mode 100644 extension/src/ui/status.tsx delete mode 100644 extension/src/ui/tabItem.tsx delete mode 100644 extension/src/ui/tsconfig.json delete mode 100644 extension/tests/extension.spec.ts delete mode 100644 extension/tsconfig.json delete mode 100644 extension/tsconfig.ui.json delete mode 100644 extension/vite.config.mts delete mode 100644 extension/vite.sw.config.mts delete mode 100644 src/DEPS.list delete mode 100644 src/browser/DEPS.list delete mode 100644 src/browser/actions.d.ts delete mode 100644 src/browser/browserContextFactory.ts delete mode 100644 src/browser/browserServerBackend.ts delete mode 100644 src/browser/codegen.ts delete mode 100644 src/browser/config.ts delete mode 100644 src/browser/context.ts delete mode 100644 src/browser/response.ts delete mode 100644 src/browser/sessionLog.ts delete mode 100644 src/browser/tab.ts delete mode 100644 src/browser/tools.ts delete mode 100644 src/browser/tools/DEPS.list delete mode 100644 src/browser/tools/common.ts delete mode 100644 src/browser/tools/console.ts delete mode 100644 src/browser/tools/dialogs.ts delete mode 100644 src/browser/tools/evaluate.ts delete mode 100644 src/browser/tools/files.ts delete mode 100644 src/browser/tools/form.ts delete mode 100644 src/browser/tools/install.ts delete mode 100644 src/browser/tools/keyboard.ts delete mode 100644 src/browser/tools/mouse.ts delete mode 100644 src/browser/tools/navigate.ts delete mode 100644 src/browser/tools/network.ts delete mode 100644 src/browser/tools/pdf.ts delete mode 100644 src/browser/tools/screenshot.ts delete mode 100644 src/browser/tools/snapshot.ts delete mode 100644 src/browser/tools/tabs.ts delete mode 100644 src/browser/tools/tool.ts delete mode 100644 src/browser/tools/utils.ts delete mode 100644 src/browser/tools/verify.ts delete mode 100644 src/browser/tools/wait.ts delete mode 100644 src/extension/DEPS.list delete mode 100644 src/extension/cdpRelay.ts delete mode 100644 src/extension/extensionContextFactory.ts delete mode 100644 src/extension/protocol.ts delete mode 100644 src/index.ts delete mode 100644 src/log.ts delete mode 100644 src/package.ts delete mode 100644 src/program.ts delete mode 100644 src/sdk/DEPS.list delete mode 100644 src/sdk/README.md delete mode 100644 src/sdk/bundle.ts delete mode 100644 src/sdk/http.ts delete mode 100644 src/sdk/inProcessTransport.ts delete mode 100644 src/sdk/manualPromise.ts delete mode 100644 src/sdk/mdb.ts delete mode 100644 src/sdk/proxyBackend.ts delete mode 100644 src/sdk/server.ts delete mode 100644 src/sdk/tool.ts delete mode 100644 src/vscode/DEPS.list delete mode 100644 src/vscode/host.ts delete mode 100644 src/vscode/main.ts delete mode 100644 tests/cdp.spec.ts delete mode 100644 tests/config.spec.ts delete mode 100644 tests/console.spec.ts delete mode 100644 tests/device.spec.ts delete mode 100644 tests/dialogs.spec.ts delete mode 100644 tests/evaluate.spec.ts delete mode 100644 tests/files.spec.ts delete mode 100644 tests/form.spec.ts delete mode 100644 tests/headed.spec.ts delete mode 100644 tests/http.spec.ts delete mode 100644 tests/iframes.spec.ts delete mode 100644 tests/install.spec.ts delete mode 100644 tests/launch.spec.ts delete mode 100644 tests/mdb.spec.ts delete mode 100644 tests/network.spec.ts delete mode 100644 tests/pdf.spec.ts delete mode 100644 tests/request-blocking.spec.ts delete mode 100644 tests/roots.spec.ts delete mode 100644 tests/screenshot.spec.ts delete mode 100644 tests/session-log.spec.ts delete mode 100644 tests/sse.spec.ts delete mode 100644 tests/tabs.spec.ts delete mode 100644 tests/trace.spec.ts delete mode 100644 tests/type.spec.ts delete mode 100644 tests/verify.spec.ts delete mode 100644 tests/vscode.spec.ts delete mode 100644 tests/wait.spec.ts delete mode 100644 tests/webdriver.spec.ts delete mode 100644 tsconfig.all.json delete mode 100644 tsconfig.json rename utils/update-readme.js => update-readme.js (95%) delete mode 100644 utils/check-deps.js delete mode 100644 utils/copyright.js delete mode 100644 utils/generate-links.js delete mode 100644 utils/set-version.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b695863..6526d0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: cache: 'npm' - name: Install dependencies run: npm ci - - run: npm run build - name: Run ESLint run: npm run lint - name: Ensure no changes @@ -41,14 +40,8 @@ jobs: run: npm ci - name: Playwright install run: npx playwright install --with-deps - - name: Install MS Edge - # MS Edge is not preinstalled on macOS runners. - if: ${{ matrix.os == 'macos-latest' }} - run: npx playwright install msedge - - name: Build - run: npm run build - name: Run tests - run: npm test + run: npm run test test_docker: runs-on: ubuntu-latest @@ -63,8 +56,6 @@ jobs: run: npm ci - name: Playwright install run: npx playwright install --with-deps chromium - - name: Build - run: npm run build - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push @@ -82,42 +73,3 @@ jobs: npm run test -- --project=chromium-docker env: MCP_IN_DOCKER: 1 - - test_extension: - strategy: - fail-fast: false - runs-on: macos-latest - defaults: - run: - working-directory: ./extension - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: '20' # crypto.randomUUID(); stalls in v18.20.8 - cache: 'npm' - - name: Install dependencies - run: npm ci - - name: Build extension - run: npm run build - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: extension - path: ./extension/dist - retention-days: 7 - - name: Install and build MCP server - run: | - cd .. - npm ci - npm run build - npx playwright install chromium - - name: Run tests - run: | - if [[ "$(uname)" == "Linux" ]]; then - xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test - else - npm run test - fi - shell: bash diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml deleted file mode 100644 index 3288368..0000000 --- a/.github/workflows/copilot-setup-steps.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "Copilot Setup Steps" - -# Automatically run the setup steps when they are changed to allow for easy validation, and -# allow manual testing through the repository's "Actions" tab -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -jobs: - # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. - copilot-setup-steps: - runs-on: ubuntu-latest - - # Set the permissions to the lowest permissions possible needed for your steps. - # Copilot will be given its own token for its operations. - permissions: - # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. - contents: read - - # You can define any steps you want, and they will run before the agent starts. - # If you do not check out your code, Copilot will do this for you. - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "18.19" - cache: "npm" - - - name: Install JavaScript dependencies - run: npm ci - - - name: Playwright install - run: npx playwright install --with-deps - - - name: Build - run: npm run build \ No newline at end of file diff --git a/.npmignore b/.npmignore index a2846ba..92fff4a 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,6 @@ **/* README.md LICENSE -!lib/**/*.js !cli.js !index.* !config.d.ts diff --git a/Dockerfile b/Dockerfile index 556ea6a..d56c3ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,10 +32,6 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \ # Copy the rest of the app COPY *.json *.js *.ts . -COPY src src/ - -# Build the app -RUN npm run build # ------------------------------ # Browser @@ -63,7 +59,6 @@ USER ${USERNAME} COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH} COPY --chown=${USERNAME}:${USERNAME} cli.js package.json ./ -COPY --from=builder --chown=${USERNAME}:${USERNAME} /app/lib /app/lib # Run in headless and only with chromium (other browsers need more dependencies not included in this image) ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"] diff --git a/cli.js b/cli.js index 95d4dbd..9f9991e 100755 --- a/cli.js +++ b/cli.js @@ -15,4 +15,4 @@ * limitations under the License. */ -require('./lib/program'); +require('playwright/lib/mcp/program'); diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index c4b8a02..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const typescriptEslint = require("@typescript-eslint/eslint-plugin"); -const tsParser = require("@typescript-eslint/parser"); -const notice = require("eslint-plugin-notice"); -const path = require("path"); -const stylistic = require("@stylistic/eslint-plugin"); -const importRules = require("eslint-plugin-import"); - -const plugins = { - "@stylistic": stylistic, - "@typescript-eslint": typescriptEslint, - notice, - import: importRules, -}; - -const baseRules = { - "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-unused-vars": [ - 2, - { args: "none", caughtErrors: "none" }, - ], - - /** - * Enforced rules - */ - // syntax preferences - "object-curly-spacing": ["error", "always"], - quotes: [ - 2, - "single", - { - avoidEscape: true, - allowTemplateLiterals: true, - }, - ], - "jsx-quotes": [2, "prefer-single"], - "no-extra-semi": 2, - "@stylistic/semi": [2], - "comma-style": [2, "last"], - "wrap-iife": [2, "inside"], - "spaced-comment": [ - 2, - "always", - { - markers: ["*"], - }, - ], - eqeqeq: [2], - "accessor-pairs": [ - 2, - { - getWithoutSet: false, - setWithoutGet: false, - }, - ], - "brace-style": [2, "1tbs", { allowSingleLine: true }], - curly: [2, "multi-or-nest", "consistent"], - "new-parens": 2, - "arrow-parens": [2, "as-needed"], - "prefer-const": 2, - "quote-props": [2, "consistent"], - "nonblock-statement-body-position": [2, "below"], - - // anti-patterns - "no-var": 2, - "no-with": 2, - "no-multi-str": 2, - "no-caller": 2, - "no-implied-eval": 2, - "no-labels": 2, - "no-new-object": 2, - "no-octal-escape": 2, - "no-self-compare": 2, - "no-shadow-restricted-names": 2, - "no-cond-assign": 2, - "no-debugger": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-empty-character-class": 2, - "no-unreachable": 2, - "no-unsafe-negation": 2, - radix: 2, - "valid-typeof": 2, - "no-implicit-globals": [2], - "no-unused-expressions": [ - 2, - { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true }, - ], - "no-proto": 2, - - // es2015 features - "require-yield": 2, - "template-curly-spacing": [2, "never"], - - // spacing details - "space-infix-ops": 2, - "space-in-parens": [2, "never"], - "array-bracket-spacing": [2, "never"], - "comma-spacing": [2, { before: false, after: true }], - "keyword-spacing": [2, "always"], - "space-before-function-paren": [ - 2, - { - anonymous: "never", - named: "never", - asyncArrow: "always", - }, - ], - "no-whitespace-before-property": 2, - "keyword-spacing": [ - 2, - { - overrides: { - if: { after: true }, - else: { after: true }, - for: { after: true }, - while: { after: true }, - do: { after: true }, - switch: { after: true }, - return: { after: true }, - }, - }, - ], - "arrow-spacing": [ - 2, - { - after: true, - before: true, - }, - ], - "@stylistic/func-call-spacing": 2, - "@stylistic/type-annotation-spacing": 2, - - // file whitespace - "no-multiple-empty-lines": [2, { max: 2, maxEOF: 0 }], - "no-mixed-spaces-and-tabs": 2, - "no-trailing-spaces": 2, - "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"], - indent: [ - 2, - 2, - { SwitchCase: 1, CallExpression: { arguments: 2 }, MemberExpression: 2 }, - ], - "key-spacing": [ - 2, - { - beforeColon: false, - }, - ], - "eol-last": 2, - - // copyright - "notice/notice": [ - 2, - { - mustMatch: "Copyright", - templateFile: path.join(__dirname, "utils", "copyright.js"), - }, - ], - - // react - "react/react-in-jsx-scope": 0, - "no-console": 2, -}; - -const languageOptions = { - parser: tsParser, - ecmaVersion: 9, - sourceType: "module", - parserOptions: { - project: path.join(__filename, "..", "tsconfig.all.json"), - } -}; - -const importOrderRules = { - "import/order": [ - 2, - { - groups: [ - "builtin", - "external", - "internal", - ["parent", "sibling"], - "index", - "type", - ], - }, - ], - "import/consistent-type-specifier-style": [2, "prefer-top-level"], -}; - -const noFloatingPromisesRules = { - "@typescript-eslint/no-floating-promises": "error", -}; - -const noBooleanCompareRules = { - "@typescript-eslint/no-unnecessary-boolean-literal-compare": 2, -}; - -module.exports = [ - { - ignores: ["**/*.js"], - }, - { - files: ["**/*.ts", "**/*.tsx"], - plugins, - languageOptions, - rules: { - ...baseRules, - ...importOrderRules, - ...noFloatingPromisesRules, - ...noBooleanCompareRules, - }, - }, -]; diff --git a/examples/generate-test.md b/examples/generate-test.md deleted file mode 100644 index b6d8dd8..0000000 --- a/examples/generate-test.md +++ /dev/null @@ -1,10 +0,0 @@ -Use Playwright tools to generate test for scenario: - -## GitHub PR Checks Navigation Checklist - -1. Open the [Microsoft Playwright GitHub repository](https://github.com/microsoft/playwright). -2. Click on the **Pull requests** tab. -3. Find and open the pull request titled **"chore: make noWaitAfter a default"**. -4. Switch to the **Checks** tab for that pull request. -5. Expand the **infra** check suite to view its jobs. -6. Click on the **docs & lint** job to view its details. diff --git a/extension/README.md b/extension/README.md deleted file mode 100644 index 6421798..0000000 --- a/extension/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Playwright MCP Chrome Extension - -## Introduction - -The Playwright MCP Chrome Extension allows you to connect to pages in your existing browser and leverage the state of your default user profile. This means the AI assistant can interact with websites where you're already logged in, using your existing cookies, sessions, and browser state, providing a seamless experience without requiring separate authentication or setup. - -## Prerequisites - -- Chrome/Edge/Chromium browser - -## Installation Steps - -### Download the Extension - -Download the latest Chrome extension from GitHub: -- **Download link**: https://github.com/microsoft/playwright-mcp/releases - -### Load Chrome Extension - -1. Open Chrome and navigate to `chrome://extensions/` -2. Enable "Developer mode" (toggle in the top right corner) -3. Click "Load unpacked" and select the extension directory - -### Configure Playwright MCP server - -Configure Playwright MCP server to connect to the browser using the extension by passing the `--extension` option when running the MCP server: - -```json -{ - "mcpServers": { - "playwright-extension": { - "command": "npx", - "args": [ - "@playwright/mcp@latest", - "--extension" - ] - } - } -} -``` - -## Usage - -### Browser Tab Selection - -When the LLM interacts with the browser for the first time, it will load a page where you can select which browser tab the LLM will connect to. This allows you to control which specific page the AI assistant will interact with during the session. - - diff --git a/extension/icons/icon-128.png b/extension/icons/icon-128.png deleted file mode 100644 index c4bc8b02508b40cec8a0d73465324a9612744172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6352 zcmb7JWmnXV)Bf$U^a7HCASFn5m#lQBba#i+y|jdYAX2h~l%#Y>xrB5}r*tFT`}h6^ z&ogso=FPmAGv~})6EPZU@_0DUZ~y?nQ&f=A`e)()2^Pjb-Sx_1@Snl3RF;VMe&-7<*EP~0QQ-e zyPEv64$;N8t5@Htw|oavKfM1-X3m~l`|-KlSi0S_&s^fcK)*57v>O(C>Pr#51Gt?1w1oSgyZ zedR~@)(h_;^n;ic3U<4En>p%V?e!pfb?7B!W!m_c2ES%)$S$j#Pis^j{P`2Tp_~;* zJ9e4N%F*$2Bcv`M{4dqIXz8}>yTYjn(FDN1yxDxgJ3u_h7@Q%Qo^qaqx`dNq53CY4js>CT{=bOAlkx~C2O^GnfL8-s^NA-@d2 z12s8#X<0Fe$-|GX9H(I01GP_nmHN1}UYpcFW&S_{f+y&@Gf5UJYZCc5Hd)(Yw3Txg zjxL#l>bhz8gZD2!Cy;JM%hhlB^VQgyr=IJ2DWle=0DTGPWA58bO#V+f2@_JmB8$^7MeThKdvSR7&RO$-;f)BFiBerbg$JfxA)!{6<- z|4wHBDEGT)u**VQnK z=e+{3&E7%)cmZZsvMwnqU^W_l@7CnkY?4tc2Qrjww-wezTV;tjta{IL`(x!G0tq{z9$P5qgosqxaD6STSl7GuM3(OSF4wsxobSb#Rx??j z@4I$ZRMM})hnVoEDh1Ikl4$*5JC;$5Sau*7Z#3ki5$|bCQWrZDPYMCVEnPL#;whse z)GdtXj}wjT$(I{RZaT}WXM9prXE~bW-ybY{KegnyT>iA{>Rkz$7C0TYh1HYaz=trc z@Bw{I9*VfHqDO0}?)P)tXvq{|a!2dzwh@I8;D80En@D2a-ZiLIR+s?yCU2>R=L2)- z@p00IstLoLJ`V{416RCrX5de#f)$NG96KDSpXH*A)|F;aQ2o-)Z583wH9fZA<_I?% zM(~H^VPSCuP!U-S5ICoO@JfOwyzW$$;q|IMptE9z?R-I-T^mIFE5uT!faPGo?y_=W z!KTq&r9WdFA54w~?o%}2r{pFlBOhS2GQoJ^S&5Q7;v;fRBe6_1&NfsZp%~9rrZ3o; zcv?MVDXC&+9_BzES$&_PZ#{V8h9#P=uq#|TC=Zf)-+zo@Cj$aHlcBQ4<-kYg?Nx=~ zS#Kj5NLog~)Wu$TKZ$NEh=hQK7?|41djl?`U8HQ^_2s5VV~iOnYp&@BpDfEL{W@wQ z#{{j`6~xOK%blXppGt9gPC0Qw%1e}l-9{G$uLeD~HS}Y4hggFu>5r9d7)6Si8k1-j z8esM~5t$MXlEh4(2FSF%3QL1&3nKlc*D^SbJa8Q>3T_@0rm%8{Nmnp61YA0QxCEyo z=heOkW}qjq5WSAG&!=U#UB&1Ayq?@t+Te34Zl*V)7_vF;z%FuRov}6Y@{HiSOc4F% zy6I~EUM6v*l$tNE(5LaWTzSo0nI1IJFggf3t`<7=qx}H|EH74J#0d9MJ$Q^J<>$etp7f`<$7MI_~2UO=rJHdt1+{eH$ZV7MWTYQ>vpZ&YCo zo$l=@8#?D92K~Dow+^vKxC|DKoGvxXIJJEiVVUtHTNVL-d+W`*ObMF1rcm)wmbkk1 z5|17pA@ksE$bss{8k#o=44UzYTgEL}swd*TH6(u@%Epu*(zt<+o@Aabe!dzY@t3qS zUjJq-^yCGa3EBs~4sB84?HF{o)K@x>kp`${q=eQ@luH%@ZOsbc#PRd@86l~5E*L{s z=r8>gD@0WBL=O~Eumre|X~_u`#=xMT8AhnAE>3);D~b0<{L7)f}( z{Da@1cD86ZODn&jX-F9V`mH%;rMB~hTPt9W9|Y;-7pdbLS9)8u8qQ&KA6M#hDQHj^ zA0>71c3V7#dFt2HMMGW$0@sl-I5!La9B@gSm>sVR8oX)u+Kv5D+5Gw`=g-rcq*^~U zP&xCku$2|W{OMqwICsS+@YyH!q|r8{qg=wCdVbwMpx0r1of88nyvgius`rS9Q(=B| zdk33~BUpNvstV(<&+khTXZaI3S`H-PWBv$6wRF!2&bNT+{j)RD&}m!5lTzflFxN*h z#2?qm6v1t1C*BPkY^q`iNnXR}OPl^DafqM0FJ#x)KLg#ek8m@}uO)TXywY25h_|Lr zYQi<%FqRuus#R3b%Eb2BD4R~1lyZ{t50u-GfJBpP<5pN(@IlqkCTu zu)>g6p>gugJs6N+i46Y+5|~uGTCU`R1M_3*{0>eh^f0S=tyUhX)MLWc`oyQ*yC;?u zJ#icVk%ebFVS<{0ue^jd$*f;R9LdX(7TI}zG6irWS=KL6gyY?mMI_Z`Z5*~D()~Y2 zkcpOVrUX?^RZNNX{1ko$rw!J7b2t{w##{J6XijAPE{TU64KO%o!+F;A^{S%zg>H3t zeHmL$Yp+KiUP5!xie-BV?unCCD^~Z*>Zp)F-Iabxgsxn0eQHJr_G+2TIXV0oG#kl` z=8nka1xa!mgmKPf#S9?hgX3Oc&!7=0;F`VBsilYr2>DENppzbVFr^83PMmATJ@mXN zHK30p>`Zm60vd?4ba1lyAO*HVemrXiLV`WS=H(PM`blKACpzR?0zfuTgAHqzYNluo zLTtKX2$gc*sSfiy{5gZ%6T;&va?HbD`ITb3W_0??EYE8E*lve#t|501$QWLaqQO#F zS`MeH8Af>v?me_)U2;3CpPZpG*-`UHU6?>Wk?s^Ov81rrWeK&{xt@F<$0;sNuwC81 zo{wiFl;pC6vgAMCnk4P0s?6r8I~F16kslEv$v$^u)7VYel(I`uo7 zm|ffpj)d2{&mWiWy(}^hQz_Hh^_q8Es)DT7FN9_uYFxWNPKG|uO2C|2Rf0E|dYZ#B zkQK{5F0sU9)-Njn>^AjO9Raa)_l1y6^nf=f@2%FWbH%7ca+pt3jpoPm65XCA2?=nG zLsvoErdEHRV$Cb#)&Jl$lov)F{Rr~c2%_vno=I-?ar*CAgW$r;FWnqUmORwykuoV7 z4B^-)yw!$7!g`-nS8ElJ@Bnrzzb3ZhcmIZu0MM-fp&JMLdE42% zi_tx9*>R8tY|3hul^`}oyyT#W%E>^*j~1bPq!guKPcK%25rR*9l9D7C&Fk>tYYULW z)9!KXfQ2H0?*os@wykpu`^UzuZ+E$Rw$<75z*b$dj7DvL?|}{nbfIE;6{*t2fNOVO zEr**xjL(ZD{nO$#LeCUZ;eJ1{%X6Md-3cdv7yT-C1mL@!Xy$aGN3SD0!YcPAWwoV0 zaLWqam&R>j6m^%}IcqPqfB5!Ae2tBeNS%8v1;xA5vK7$Y*(_^lg`2$(!Gge}km2jy z>BS+qh^XW-epYOdbaVPwB!l*H1;ZNU+{r+Z4{)$&y>XW}YYI=oVIXm(C1`iv_{zdm zjgT)v))Cg!FOZRNd>!l%|N7iq%EM}SxlvoyHsU8du(woI*yd3UP07Q ztA9T!DcWl*m{kXU*VsLxpakR&b0?xK0p16*JR%N9^=OdANz3t7oZ)A0d6`(=-=oOg zB`@6uEANcAC)E!`yh`WT5k{uC=G1_D``g}=n?tc2C{fCg?xywQjYS#xRO{Z~FsX+z znZ$jC>cXEv`Xqg((*;?DR9+u*8vAgG)@foHPJAe-eNly;b*Xi#9v3RUf?87#Gs8u^ zKb{W6(ChP@9UsubM{=TC8}9@6d&!)G9+9B@hBf!xiM0z$N6W(~oIBe+#~5sFWA8Qw zMI9rY!Ku$uO(6Hz8!8*~mDIH*9i5*((tlV6mJ}&KTM$5BSWN6s#BT8M?9OM8MkO#7 zTNI?tqjV)t*Yg!I#5O^odft+#5@p+*$abS!mB|J!)w`gL4ET(ne*Npt@#>Mm?>ae{ z<;pm~0%!|_wo7;Jh-?uRP;sPh`r8~)Jv7%41=~jTSE8iOzc{3LK~@GBmr9Q3$8FmQPBNnF)=1brfo!)xBJ zY-WGd(-2hOe2w3v`Ok|fW>tqQJra)ktbg!Az!L6DAiANx0`PZ6cx+e}wz(8D_}4rl z`ckaPx8ARz^ia)Z{U3MsqLiv};4ce}K9cZns3b~<46dOjHpi%O+F(()b!7rP)bRdH zS@biX5Gkx1MPame47<*_x3wr12REvd~$A(rq|e zmRp`E0hjH{Nlx|JH=P!r@Jssm245>U_jl-07>EJ9GH62cTYQj81bpKLw zDwKggRT`|HrXz$v>_$ZfE@pq_inz@l1)0M8}h=8Ts%JH zETDpMhBY=zGZ~YsW68^aMF=e1=yGEIl58VOsN2b0hWvvpao`MN%ho8T1A$ZOiYcGA zjs}4_k~MAamO{^3x5XC9b&3Yu54w3-g=7lGF@SWJZUZJ%Lp%?z@LN+H18^m4OBIa8 zuGP~iS`wLe4_K68i`T)2M@!!Z_?}r2&6Kx(y~MCx#;4sTrvAF=I{f{Rq!;OyzI}xa zfltT0eDu~d63u)?K$zQ9hp*KmcgccE-|<0bLzoGg=ub-nLC-oDKUsr9tjrD(RYenW z_a5?~|Fte~-)~^^NE84%lK=&9;k!q=-5-ZqurSgU*bx&AIzm)P4nq zi1&JWf89+T&VuQqk7Z;>@jeWOBWjbG3xm!i272g23QXifMFc17)*ElrXa>8xFnN;< ze>b^IQfkE-k=u(nXpW=VNkj&4REQiA)oQwzvE~y8>Vrc9J9Yc$EP5G15r4~~G9^|< zrgnLGYk&WqUleFQzqJFb!0A_n<&C+KXhrbb10xoe2L`wdOk@2c7h*uNlp41X2mT!0OeSS8zC-%fYrFVRM zrM=r?n$qU_$9T3*p_G0k5d$kCN#m{PA0B+ttHQ&7S1t@awbr)5Nu{UkrORiXNAVj5 z_eL(kk*@NdGcw+O=^bb#M608lDq~@Wts#rSuxKP-Se#bZ#@eXd! z^Djm%G^L{c3D_z1nM;Z;CrPa9Ije$0w-=V480 z(ERSuk?fo(ze;+2tTXeWh=M1Dy5l3g2Dij+;zuSVEpDlk@!j(Dwd}8`15G%?w-VDk z4)DXq*3#I_H2L@HH_M@Jxuu1;hFSuiEhSJ%N)+Ve>2?XSxCPcrZ#muoq{*o8vEQTU* zIK}cZ<$O;S%!>On($OTCRLy|e9foC2@!{ciS4MUkUsV0}Bw`|!RWBP1{ST<@UY{S6 zZ)yt}Q|(}Jh>#{Q=k1PX!N^obFszGLSa$FmfRDoSlq;y%Z==+sfci0 z=k@u{9hY0(19s!z)lyx!1({w&n=F11xuwk_vIUyY;2W=ul(5v2K|0;$*5F49fHTGaT6w)FOK_rd&O{f<{wIdjUXE6nKRwtbq=a*V$ceu4^P)5vgd!sD#D!20RJ7_s!G%Fhg;rAR15BY}BoejF>z;e>85fmMp>Zn?%x31} z$H&YE{P#tj7Rl+cCoIAN$C8=iRI%DgCw3ZMA3vEE+Ryqeb+A_V%E_Cf_tUr2gF8CN zWHQFBJ_o|mGAadCh=nnOfOz_2TK6p1kr-q>?xSq{lH8ih)!d$F;0ZL`BV8O07w9+ zloo5Lk#uWVa+bh}!xdxi!^BVc((6YL&pY2@*RW={u+KX{1Ya8ZO!;1Uvkd@%XCIf} z#D+#oNvTs2E%duS6;uu75HM^F(OK&Eju62F=-m9o`$GGTodPnJo0wF`OP3J$;!ez# zP;v!zg?>s-}L{cegPiHz@Uarj;R0u002ov JPDHLkV1gAA4qyNP diff --git a/extension/icons/icon-32.png b/extension/icons/icon-32.png deleted file mode 100644 index 1f9a8ccb89bdbccd6bdc1c7f945f72eed9b94017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1258 zcmVvS_*I>m_KT&x^fBtGyo{vygB`pAML~lkiZCl zFf-D?$W*Ys3K9gyUC;dcrg?P$@B=7epk(@p9G}{ohz9_lo3_~Duup;mPzcN{s+i$0 zqdPq|IjJTrO_?Dj?R3X>5;^)fz?|lSZ%zYPWQA^zz&8{G2a_f+!%_+z8RG!Dq22SJ zDgKZgu>{}`qMN!0#$jWj0)0f4%?I;Y2#2`WLP9MJtimggcQyB%i4 zT!lV!9Kg1m?2*cruSa&Y?ul->e!~1UFDNu}r7j?}iM?hHlS1Fj?gVb=-qM|rGI0xz zNA!#F;|ha*))l>wrSrJ}RK8+{%Zy>2CslD>!x^=n9#fHn!{x1+tXOEC3HgWg)9Ra* z$JJLWhxy0sb}&NmJcg*73X}l2q2#d`i2(rcv8rI9I+de7Q_KPw-vb4eR8wXKE0FG) zkUwbq&kduHn!MFqR8oXDy{ z&C&tD^;-e0EBAfR)!e_#>)DI-2Wes9YS-X*24HXyz|8yDXPDUA|82iL4R9vW+6}Kh ze>5079)MZ_02-);iC0G)*X;DiaPXNsiCE3h!osI-yRonJ!kY5!1ws>*%&26E60!AO z6LQDaPd1`=ukEa_nWfMy%>X=Gv2%Y-VDyj&oE!HkW(?RC_Lts9RH$hcG>>*GeQEl~ zil~=ZJqATPA^GLWApWJP`ARts&E8@;(*Xc|H`*I6EnCx9tYdYS;3WVA5y4dqQ)sF& zI64iSq{uV_#w!Hg4Z?N^6is#ZDArLw* zE2FMO23bPs)EpYbtiU#)fkwy0I&2yVgZN-jKFdudbhrD*teZ!yQjj}`*z%h)<+kD5{ zfGbNrZLY1oIbW@Z0sw&O>T22XK&@w}3PGYkfpr4fq4usnWC$RR~H#Xhj^UO2PJoEe?L#Y&7d1z%`*vs9* zl-F{EXH&!X8vLo$PlQO7&|{l+WuxEy7iB4r#Ki|su+n#NePvg?>GGwEp_8Xz2%9gR9SlS?843zufP{cCp&U>Om+}L1Uc8V3!kF_V7;L_F zVJJo?@fer%VcQWUztm#A4)%4_|?47h(`$hI0ej4T_;MGJHuHW z(eq8`+;@P9&L%YgfIkRR(pEyCz$nYkI@?hX0E&UIW{kRGb`(Jxd(K;RY4GN^2G04a z+cuBw>)cCBe{%Ktk zX>K^Q;x#@{c6dGFTYNyNP~)@)G~?qz7SAstRSW7#c^pv{ z!oWw-{Xas?J~mf8Qul7q^hHgA1-)z34zy|Dvc+5(eZJBRo2ECA2-7A1{euv|*M`IV z!mW$uGd(XmZs)BA@F%{S`2@UXOuZB6Xd69Qb z8McrZKPA?+;RWzoFV(*#zkll-GVRO!S$1aBw6N*mzs_s`fM)$#gC%=svls#90SDr5 z!_&0lWZj1dnE+L5=-<=6-bZPNHzP{#NJCYFH}M*#aP{QTmf!juT||+JP0T37CqMLM z9(v0WZ#Hc_uRXo@p&L$0yCqE-iXNMF{}(*@GQ_N3OL|(OgMP-(Pg_P;vPlZ!C+m;A z|Fn;AT8^Z-rj7@*?i88V@^8A2uPn$oR_4t5Y}K&Kzo6YT9A3jI@6OJ_B#lkg7e7fh zC=(qV?rQ0~S-7@hk4rsF;FtwU5~tT3dH=fr@Z-{w9Xmt(jdO)}DI9e%vu3H+-s$yp z=aebiI7>kzX!K;Qa9qz{ipCrhea;qP@-=wWI3zE?H18$E{QThC{T)filMR$_s>)m9 zmY@F06DrZnlL1UAf31~%iV0=5)-G=-jKih(@b`uBiFLTmGaW;1n{f~}0F-U5E+$|* z3v7qxEg)pTsYhtlcq^V5Sp`CF^n51NHtlJc_5~>64gf%Z_jSK(S!p|n*OyA28!@Yy zH!!e-0fUGTQX07-11A=b#%U;97v`kr9VnaZV^W>|x zEBFyD3<*?>EGLtvegLopQ_bMpeFJgAQOx-0R<>>Tb2Se2a*Z=w!U@hFw`>Ho!iyXG zF=QrxFOa4TXFQzsCb7Q4fFDFlhKV%|M?v z)~z>H8i1HKA_c*3E362*aH?P|sgB{4(&=%m184_ndyVZQI_&j3OZfL8B9zyI9gmogLKU8{4w99d9D@p-f2UjQQ%+ z0^6?cZb;g79@s!K9@(~g3o{+pIiXSC#&)-|Y|ms6A&^~|HN8NX>Q#UGv&}Eo-k3|= zlztm6+giO9#D`fTEA(V{cgS7k58m3Wsn{TB1xz&AAJ6ykMj`{-fKQ$1c zbbC#S(0U)h+rcOUA@XqMW8fkI$1Kfnw0?N>_J3*(DW#;cGP8VZ%?ua9JoC&m&piKS Z{soKhKGhw*_GbV9002ovPDHLkV1gda&R_rl diff --git a/extension/manifest.json b/extension/manifest.json deleted file mode 100644 index ffc9e9d..0000000 --- a/extension/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "manifest_version": 3, - "name": "Playwright MCP Bridge", - "version": "0.0.36", - "description": "Share browser tabs with Playwright MCP server", - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB", - "permissions": [ - "debugger", - "activeTab", - "tabs", - "storage" - ], - "host_permissions": [ - "" - ], - "background": { - "service_worker": "lib/background.mjs", - "type": "module" - }, - "action": { - "default_title": "Playwright MCP Bridge", - "default_icon": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } - }, - "icons": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } -} diff --git a/extension/package-lock.json b/extension/package-lock.json deleted file mode 100644 index df8cf14..0000000 --- a/extension/package-lock.json +++ /dev/null @@ -1,1884 +0,0 @@ -{ - "name": "@playwright/mcp-extension", - "version": "0.0.36", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@playwright/mcp-extension", - "version": "0.0.36", - "license": "Apache-2.0", - "devDependencies": { - "@types/chrome": "^0.0.315", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.8.2", - "vite": "^5.0.0", - "vite-plugin-static-copy": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.1.tgz", - "integrity": "sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.1.tgz", - "integrity": "sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.1.tgz", - "integrity": "sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.1.tgz", - "integrity": "sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.1.tgz", - "integrity": "sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.1.tgz", - "integrity": "sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.1.tgz", - "integrity": "sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.1.tgz", - "integrity": "sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.1.tgz", - "integrity": "sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.1.tgz", - "integrity": "sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.1.tgz", - "integrity": "sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.1.tgz", - "integrity": "sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.1.tgz", - "integrity": "sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.1.tgz", - "integrity": "sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.1.tgz", - "integrity": "sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.1.tgz", - "integrity": "sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.1.tgz", - "integrity": "sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.1.tgz", - "integrity": "sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.1.tgz", - "integrity": "sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.1.tgz", - "integrity": "sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/chrome": { - "version": "0.0.315", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.315.tgz", - "integrity": "sha512-Oy1dYWkr6BCmgwBtOngLByCHstQ3whltZg7/7lubgIZEYvKobDneqplgc6LKERNRBwckFviV4UU5AZZNUFrJ4A==", - "dev": true, - "dependencies": { - "@types/filesystem": "*", - "@types/har-format": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "node_modules/@types/filesystem": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", - "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", - "dev": true, - "dependencies": { - "@types/filewriter": "*" - } - }, - "node_modules/@types/filewriter": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", - "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", - "dev": true - }, - "node_modules/@types/har-format": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", - "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.192", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", - "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "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==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rollup": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.1.tgz", - "integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.1", - "@rollup/rollup-android-arm64": "4.46.1", - "@rollup/rollup-darwin-arm64": "4.46.1", - "@rollup/rollup-darwin-x64": "4.46.1", - "@rollup/rollup-freebsd-arm64": "4.46.1", - "@rollup/rollup-freebsd-x64": "4.46.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.1", - "@rollup/rollup-linux-arm-musleabihf": "4.46.1", - "@rollup/rollup-linux-arm64-gnu": "4.46.1", - "@rollup/rollup-linux-arm64-musl": "4.46.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.1", - "@rollup/rollup-linux-ppc64-gnu": "4.46.1", - "@rollup/rollup-linux-riscv64-gnu": "4.46.1", - "@rollup/rollup-linux-riscv64-musl": "4.46.1", - "@rollup/rollup-linux-s390x-gnu": "4.46.1", - "@rollup/rollup-linux-x64-gnu": "4.46.1", - "@rollup/rollup-linux-x64-musl": "4.46.1", - "@rollup/rollup-win32-arm64-msvc": "4.46.1", - "@rollup/rollup-win32-ia32-msvc": "4.46.1", - "@rollup/rollup-win32-x64-msvc": "4.46.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-plugin-static-copy": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.1.tgz", - "integrity": "sha512-oR53SkL5cX4KT1t18E/xU50vJDo0N8oaHza4EMk0Fm+2/u6nQivxavOfrDk3udWj+dizRizB/QnBvJOOQrTTAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.6.0", - "fs-extra": "^11.3.0", - "p-map": "^7.0.3", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.14" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } -} diff --git a/extension/package.json b/extension/package.json deleted file mode 100644 index 89b75eb..0000000 --- a/extension/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@playwright/mcp-extension", - "version": "0.0.36", - "description": "Playwright MCP Browser Extension", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/microsoft/playwright-mcp.git" - }, - "homepage": "https://playwright.dev", - "engines": { - "node": ">=18" - }, - "author": { - "name": "Microsoft Corporation" - }, - "license": "Apache-2.0", - "scripts": { - "build": "tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts", - "watch": "tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts", - "test": "playwright test", - "clean": "rm -rf dist" - }, - "devDependencies": { - "@types/chrome": "^0.0.315", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.8.2", - "vite": "^5.0.0", - "vite-plugin-static-copy": "^3.1.1" - } -} diff --git a/extension/playwright.config.ts b/extension/playwright.config.ts deleted file mode 100644 index 7bf4f0e..0000000 --- a/extension/playwright.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { defineConfig } from '@playwright/test'; - -import type { TestOptions } from '../tests/fixtures'; - -export default defineConfig({ - testDir: './tests', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'list', - projects: [ - { name: 'chromium', use: { mcpBrowser: 'chromium' } }, - ], -}); diff --git a/extension/src/background.ts b/extension/src/background.ts deleted file mode 100644 index e74ebb8..0000000 --- a/extension/src/background.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RelayConnection, debugLog } from './relayConnection'; - -type PageMessage = { - type: 'connectToMCPRelay'; - mcpRelayUrl: string; -} | { - type: 'getTabs'; -} | { - type: 'connectToTab'; - tabId?: number; - windowId?: number; - mcpRelayUrl: string; -} | { - type: 'getConnectionStatus'; -} | { - type: 'disconnect'; -}; - -class TabShareExtension { - private _activeConnection: RelayConnection | undefined; - private _connectedTabId: number | null = null; - private _pendingTabSelection = new Map(); - - constructor() { - chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this)); - chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this)); - chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this)); - chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); - chrome.action.onClicked.addListener(this._onActionClicked.bind(this)); - } - - // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031 - private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) { - switch (message.type) { - case 'connectToMCPRelay': - this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then( - () => sendResponse({ success: true }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; - case 'getTabs': - this._getTabs().then( - tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; - case 'connectToTab': - const tabId = message.tabId || sender.tab?.id!; - const windowId = message.windowId || sender.tab?.windowId!; - this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then( - () => sendResponse({ success: true }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; // Return true to indicate that the response will be sent asynchronously - case 'getConnectionStatus': - sendResponse({ - connectedTabId: this._connectedTabId - }); - return false; - case 'disconnect': - this._disconnect().then( - () => sendResponse({ success: true }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; - } - return false; - } - - private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise { - try { - debugLog(`Connecting to relay at ${mcpRelayUrl}`); - const socket = new WebSocket(mcpRelayUrl); - await new Promise((resolve, reject) => { - socket.onopen = () => resolve(); - socket.onerror = () => reject(new Error('WebSocket error')); - setTimeout(() => reject(new Error('Connection timeout')), 5000); - }); - - const connection = new RelayConnection(socket); - connection.onclose = () => { - debugLog('Connection closed'); - this._pendingTabSelection.delete(selectorTabId); - // TODO: show error in the selector tab? - }; - this._pendingTabSelection.set(selectorTabId, { connection }); - debugLog(`Connected to MCP relay`); - } catch (error: any) { - const message = `Failed to connect to MCP relay: ${error.message}`; - debugLog(message); - throw new Error(message); - } - } - - private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise { - try { - debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`); - try { - this._activeConnection?.close('Another connection is requested'); - } catch (error: any) { - debugLog(`Error closing active connection:`, error); - } - await this._setConnectedTabId(null); - - this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection; - if (!this._activeConnection) - throw new Error('No active MCP relay connection'); - this._pendingTabSelection.delete(selectorTabId); - - this._activeConnection.setTabId(tabId); - this._activeConnection.onclose = () => { - debugLog('MCP connection closed'); - this._activeConnection = undefined; - void this._setConnectedTabId(null); - }; - - await Promise.all([ - this._setConnectedTabId(tabId), - chrome.tabs.update(tabId, { active: true }), - chrome.windows.update(windowId, { focused: true }), - ]); - debugLog(`Connected to MCP bridge`); - } catch (error: any) { - await this._setConnectedTabId(null); - debugLog(`Failed to connect tab ${tabId}:`, error.message); - throw error; - } - } - - private async _setConnectedTabId(tabId: number | null): Promise { - const oldTabId = this._connectedTabId; - this._connectedTabId = tabId; - if (oldTabId && oldTabId !== tabId) - await this._updateBadge(oldTabId, { text: '' }); - if (tabId) - await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' }); - } - - private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise { - try { - await chrome.action.setBadgeText({ tabId, text }); - await chrome.action.setTitle({ tabId, title: title || '' }); - if (color) - await chrome.action.setBadgeBackgroundColor({ tabId, color }); - } catch (error: any) { - // Ignore errors as the tab may be closed already. - } - } - - private async _onTabRemoved(tabId: number): Promise { - const pendingConnection = this._pendingTabSelection.get(tabId)?.connection; - if (pendingConnection) { - this._pendingTabSelection.delete(tabId); - pendingConnection.close('Browser tab closed'); - return; - } - if (this._connectedTabId !== tabId) - return; - this._activeConnection?.close('Browser tab closed'); - this._activeConnection = undefined; - this._connectedTabId = null; - } - - private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) { - for (const [tabId, pending] of this._pendingTabSelection) { - if (tabId === activeInfo.tabId) { - if (pending.timerId) { - clearTimeout(pending.timerId); - pending.timerId = undefined; - } - continue; - } - if (!pending.timerId) { - pending.timerId = setTimeout(() => { - const existed = this._pendingTabSelection.delete(tabId); - if (existed) { - pending.connection.close('Tab has been inactive for 5 seconds'); - chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' }); - } - }, 5000); - return; - } - } - } - - private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) { - if (this._connectedTabId === tabId) - void this._setConnectedTabId(tabId); - } - - private async _getTabs(): Promise { - const tabs = await chrome.tabs.query({}); - return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme))); - } - - private async _onActionClicked(): Promise { - await chrome.tabs.create({ - url: chrome.runtime.getURL('status.html'), - active: true - }); - } - - private async _disconnect(): Promise { - this._activeConnection?.close('User disconnected'); - this._activeConnection = undefined; - await this._setConnectedTabId(null); - } -} - -new TabShareExtension(); diff --git a/extension/src/relayConnection.ts b/extension/src/relayConnection.ts deleted file mode 100644 index b203af4..0000000 --- a/extension/src/relayConnection.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function debugLog(...args: unknown[]): void { - const enabled = true; - if (enabled) { - // eslint-disable-next-line no-console - console.log('[Extension]', ...args); - } -} - -type ProtocolCommand = { - id: number; - method: string; - params?: any; -}; - -type ProtocolResponse = { - id?: number; - method?: string; - params?: any; - result?: any; - error?: string; -}; - -export class RelayConnection { - private _debuggee: chrome.debugger.Debuggee; - private _ws: WebSocket; - private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; - private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; - private _tabPromise: Promise; - private _tabPromiseResolve!: () => void; - private _closed = false; - - onclose?: () => void; - - constructor(ws: WebSocket) { - this._debuggee = { }; - this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve); - this._ws = ws; - this._ws.onmessage = this._onMessage.bind(this); - this._ws.onclose = () => this._onClose(); - // Store listeners for cleanup - this._eventListener = this._onDebuggerEvent.bind(this); - this._detachListener = this._onDebuggerDetach.bind(this); - chrome.debugger.onEvent.addListener(this._eventListener); - chrome.debugger.onDetach.addListener(this._detachListener); - } - - // Either setTabId or close is called after creating the connection. - setTabId(tabId: number): void { - this._debuggee = { tabId }; - this._tabPromiseResolve(); - } - - close(message: string): void { - this._ws.close(1000, message); - // ws.onclose is called asynchronously, so we call it here to avoid forwarding - // CDP events to the closed connection. - this._onClose(); - } - - private _onClose() { - if (this._closed) - return; - this._closed = true; - chrome.debugger.onEvent.removeListener(this._eventListener); - chrome.debugger.onDetach.removeListener(this._detachListener); - chrome.debugger.detach(this._debuggee).catch(() => {}); - this.onclose?.(); - } - - private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { - if (source.tabId !== this._debuggee.tabId) - return; - debugLog('Forwarding CDP event:', method, params); - const sessionId = source.sessionId; - this._sendMessage({ - method: 'forwardCDPEvent', - params: { - sessionId, - method, - params, - }, - }); - } - - private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { - if (source.tabId !== this._debuggee.tabId) - return; - this.close(`Debugger detached: ${reason}`); - this._debuggee = { }; - } - - private _onMessage(event: MessageEvent): void { - this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e)); - } - - private async _onMessageAsync(event: MessageEvent): Promise { - let message: ProtocolCommand; - try { - message = JSON.parse(event.data); - } catch (error: any) { - debugLog('Error parsing message:', error); - this._sendError(-32700, `Error parsing message: ${error.message}`); - return; - } - - debugLog('Received message:', message); - - const response: ProtocolResponse = { - id: message.id, - }; - try { - response.result = await this._handleCommand(message); - } catch (error: any) { - debugLog('Error handling command:', error); - response.error = error.message; - } - debugLog('Sending response:', response); - this._sendMessage(response); - } - - private async _handleCommand(message: ProtocolCommand): Promise { - if (message.method === 'attachToTab') { - await this._tabPromise; - debugLog('Attaching debugger to tab:', this._debuggee); - await chrome.debugger.attach(this._debuggee, '1.3'); - const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo'); - return { - targetInfo: result?.targetInfo, - }; - } - if (!this._debuggee.tabId) - throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.'); - if (message.method === 'forwardCDPCommand') { - const { sessionId, method, params } = message.params; - debugLog('CDP command:', method, params); - const debuggerSession: chrome.debugger.DebuggerSession = { - ...this._debuggee, - sessionId, - }; - // Forward CDP command to chrome.debugger - return await chrome.debugger.sendCommand( - debuggerSession, - method, - params - ); - } - } - - private _sendError(code: number, message: string): void { - this._sendMessage({ - error: { - code, - message, - }, - }); - } - - private _sendMessage(message: any): void { - if (this._ws.readyState === WebSocket.OPEN) - this._ws.send(JSON.stringify(message)); - } -} diff --git a/extension/src/ui/connect.css b/extension/src/ui/connect.css deleted file mode 100644 index 60945d4..0000000 --- a/extension/src/ui/connect.css +++ /dev/null @@ -1,206 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -body { - margin: 0; - padding: 0; -} - -/* Base styles */ -.app-container { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; - background-color: #ffffff; - color: #1f2328; - margin: 0; - padding: 16px; - min-height: 100vh; - font-size: 14px; -} - -.content-wrapper { - max-width: 600px; - margin: 0 auto; -} - -/* Status Banner */ -.status-container { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; - padding-right: 12px; -} - -.status-banner { - padding: 12px; - font-size: 14px; - font-weight: 500; - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} - -.status-banner.connected { - color: #1f2328; -} - -.status-banner.connected::before { - content: "\2705"; - margin-right: 8px; -} - -.status-banner.error { - color: #1f2328; -} - -.status-banner.error::before { - content: "\274C"; - margin-right: 8px; -} - -/* Buttons */ -.button-container { - margin-bottom: 16px; - display: flex; - justify-content: flex-end; - padding-right: 12px; -} - -.button { - padding: 8px 16px; - border-radius: 6px; - border: none; - font-size: 14px; - font-weight: 500; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - text-decoration: none; - margin-right: 8px; - min-width: 90px; -} - -.button.primary { - background-color: #f8f9fa; - color: #3c4043; - border: 1px solid #dadce0; -} - -.button.primary:hover { - background-color: #f1f3f4; - border-color: #dadce0; - box-shadow: 0 1px 2px 0 rgba(60,64,67,.1); -} - -.button.default { - background-color: #f6f8fa; - color: #24292f; -} - -.button.default:hover { - background-color: #f3f4f6; -} - -.button.reject { - background-color: #da3633; - color: #ffffff; - border: 1px solid #da3633; -} - -.button.reject:hover { - background-color: #c73836; - border-color: #c73836; -} - -/* Tab selection */ -.tab-section-title { - padding-left: 12px; - font-size: 12px; - font-weight: 400; - margin-bottom: 12px; - color: #656d76; -} - -.tab-item { - display: flex; - align-items: center; - padding: 12px; - margin-bottom: 8px; - background-color: #ffffff; - cursor: pointer; - border-radius: 6px; - transition: background-color 0.2s ease; -} - -.tab-item:hover { - background-color: #f8f9fa; -} - -.tab-item.selected { - background-color: #f6f8fa; -} - -.tab-item.disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.tab-radio { - margin-right: 12px; - flex-shrink: 0; -} - -.tab-favicon { - width: 16px; - height: 16px; - margin-right: 8px; - flex-shrink: 0; -} - -.tab-content { - flex: 1; - min-width: 0; -} - -.tab-title { - font-weight: 500; - color: #1f2328; - margin-bottom: 2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.tab-url { - font-size: 12px; - color: #656d76; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Link-style button */ -.link-button { - background: none; - border: none; - color: #0066cc; - text-decoration: underline; - cursor: pointer; - padding: 0; - font: inherit; -} \ No newline at end of file diff --git a/extension/src/ui/connect.html b/extension/src/ui/connect.html deleted file mode 100644 index 3f20e4b..0000000 --- a/extension/src/ui/connect.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Playwright MCP extension - - - - - - -
- - - \ No newline at end of file diff --git a/extension/src/ui/connect.tsx b/extension/src/ui/connect.tsx deleted file mode 100644 index 153017b..0000000 --- a/extension/src/ui/connect.tsx +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState, useEffect, useCallback } from 'react'; -import { createRoot } from 'react-dom/client'; -import { Button, TabItem } from './tabItem'; -import type { TabInfo } from './tabItem'; - -type Status = - | { type: 'connecting'; message: string } - | { type: 'connected'; message: string } - | { type: 'error'; message: string } - | { type: 'error'; versionMismatch: { extensionVersion: string; } }; - -const SUPPORTED_PROTOCOL_VERSION = 1; - -const ConnectApp: React.FC = () => { - const [tabs, setTabs] = useState([]); - const [status, setStatus] = useState(null); - const [showButtons, setShowButtons] = useState(true); - const [showTabList, setShowTabList] = useState(true); - const [clientInfo, setClientInfo] = useState('unknown'); - const [mcpRelayUrl, setMcpRelayUrl] = useState(''); - const [newTab, setNewTab] = useState(false); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const relayUrl = params.get('mcpRelayUrl'); - - if (!relayUrl) { - setShowButtons(false); - setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' }); - return; - } - - setMcpRelayUrl(relayUrl); - - try { - const client = JSON.parse(params.get('client') || '{}'); - const info = `${client.name}/${client.version}`; - setClientInfo(info); - setStatus({ - type: 'connecting', - message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?` - }); - } catch (e) { - setStatus({ type: 'error', message: 'Failed to parse client version.' }); - return; - } - - const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10); - const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion; - if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) { - const extensionVersion = chrome.runtime.getManifest().version; - setShowButtons(false); - setShowTabList(false); - setStatus({ - type: 'error', - versionMismatch: { - extensionVersion, - } - }); - return; - } - - void connectToMCPRelay(relayUrl); - - // If this is a browser_navigate command, hide the tab list and show simple allow/reject - if (params.get('newTab') === 'true') { - setNewTab(true); - setShowTabList(false); - } else { - void loadTabs(); - } - }, []); - - const handleReject = useCallback((message: string) => { - setShowButtons(false); - setShowTabList(false); - setStatus({ type: 'error', message }); - }, []); - - const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => { - - const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl }); - if (!response.success) - handleReject(response.error); - }, [handleReject]); - - const loadTabs = useCallback(async () => { - const response = await chrome.runtime.sendMessage({ type: 'getTabs' }); - if (response.success) - setTabs(response.tabs); - else - setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error }); - }, []); - - const handleConnectToTab = useCallback(async (tab?: TabInfo) => { - setShowButtons(false); - setShowTabList(false); - - try { - const response = await chrome.runtime.sendMessage({ - type: 'connectToTab', - mcpRelayUrl, - tabId: tab?.id, - windowId: tab?.windowId, - }); - - if (response?.success) { - setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` }); - } else { - setStatus({ - type: 'error', - message: response?.error || `MCP client "${clientInfo}" failed to connect.` - }); - } - } catch (e) { - setStatus({ - type: 'error', - message: `MCP client "${clientInfo}" failed to connect: ${e}` - }); - } - }, [clientInfo, mcpRelayUrl]); - - useEffect(() => { - const listener = (message: any) => { - if (message.type === 'connectionTimeout') - handleReject('Connection timed out.'); - }; - chrome.runtime.onMessage.addListener(listener); - return () => { - chrome.runtime.onMessage.removeListener(listener); - }; - }, [handleReject]); - - return ( -
-
- {status && ( -
- - {showButtons && ( -
- {newTab ? ( - <> - - - - ) : ( - - )} -
- )} -
- )} - - {showTabList && ( -
-
- Select page to expose to MCP server: -
-
- {tabs.map(tab => ( - handleConnectToTab(tab)}> - Connect - - } - /> - ))} -
-
- )} -
-
- ); -}; - -const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => { - const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md'; - const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest'; - return ( -
- Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '} - Click here to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '} - See installation instructions for more details. -
- ); -}; - -const StatusBanner: React.FC<{ status: Status }> = ({ status }) => { - return ( -
- {'versionMismatch' in status ? ( - - ) : ( - status.message - )} -
- ); -}; - -// Initialize the React app -const container = document.getElementById('root'); -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/extension/src/ui/status.html b/extension/src/ui/status.html deleted file mode 100644 index ccc1f04..0000000 --- a/extension/src/ui/status.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Playwright MCP Bridge Status - - - -
- - - \ No newline at end of file diff --git a/extension/src/ui/status.tsx b/extension/src/ui/status.tsx deleted file mode 100644 index f8de788..0000000 --- a/extension/src/ui/status.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState, useEffect } from 'react'; -import { createRoot } from 'react-dom/client'; -import { Button, TabItem } from './tabItem'; - -import type { TabInfo } from './tabItem'; - -interface ConnectionStatus { - isConnected: boolean; - connectedTabId: number | null; - connectedTab?: TabInfo; -} - -const StatusApp: React.FC = () => { - const [status, setStatus] = useState({ - isConnected: false, - connectedTabId: null - }); - - useEffect(() => { - void loadStatus(); - }, []); - - const loadStatus = async () => { - // Get current connection status from background script - const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' }); - if (connectedTabId) { - const tab = await chrome.tabs.get(connectedTabId); - setStatus({ - isConnected: true, - connectedTabId, - connectedTab: { - id: tab.id!, - windowId: tab.windowId!, - title: tab.title!, - url: tab.url!, - favIconUrl: tab.favIconUrl - } - }); - } else { - setStatus({ - isConnected: false, - connectedTabId: null - }); - } - }; - - const openConnectedTab = async () => { - if (!status.connectedTabId) - return; - await chrome.tabs.update(status.connectedTabId, { active: true }); - window.close(); - }; - - const disconnect = async () => { - await chrome.runtime.sendMessage({ type: 'disconnect' }); - window.close(); - }; - - return ( -
-
- {status.isConnected && status.connectedTab ? ( -
-
- Page with connected MCP client: -
-
- - Disconnect - - } - onClick={openConnectedTab} - /> -
-
- ) : ( -
- No MCP clients are currently connected. -
- )} -
-
- ); -}; - -// Initialize the React app -const container = document.getElementById('root'); -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/extension/src/ui/tabItem.tsx b/extension/src/ui/tabItem.tsx deleted file mode 100644 index 1483742..0000000 --- a/extension/src/ui/tabItem.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; - -export interface TabInfo { - id: number; - windowId: number; - title: string; - url: string; - favIconUrl?: string; -} - -export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({ - variant, - onClick, - children -}) => { - return ( - - ); -}; - - -export interface TabItemProps { - tab: TabInfo; - onClick?: () => void; - button?: React.ReactNode; -} - -export const TabItem: React.FC = ({ - tab, - onClick, - button -}) => { - return ( -
- '} - alt='' - className='tab-favicon' - /> -
-
- {tab.title || 'Untitled'} -
-
{tab.url}
-
- {button} -
- ); -}; diff --git a/extension/src/ui/tsconfig.json b/extension/src/ui/tsconfig.json deleted file mode 100644 index 77839b6..0000000 --- a/extension/src/ui/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -// Help VSCode to find right tsconfig file. -{ - "extends": "../../tsconfig.ui.json" -} diff --git a/extension/tests/extension.spec.ts b/extension/tests/extension.spec.ts deleted file mode 100644 index 91dbed1..0000000 --- a/extension/tests/extension.spec.ts +++ /dev/null @@ -1,306 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; -import { chromium } from 'playwright'; -import { test as base, expect } from '../../tests/fixtures'; - -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import type { BrowserContext } from 'playwright'; -import type { StartClient } from '../../tests/fixtures'; - -type BrowserWithExtension = { - userDataDir: string; - launch: (mode?: 'disable-extension') => Promise; -}; - -type TestFixtures = { - browserWithExtension: BrowserWithExtension, - pathToExtension: string, - useShortConnectionTimeout: (timeoutMs: number) => void - overrideProtocolVersion: (version: number) => void -}; - -const test = base.extend({ - pathToExtension: async ({}, use) => { - await use(path.resolve(__dirname, '../dist')); - }, - - browserWithExtension: async ({ mcpBrowser, pathToExtension }, use, testInfo) => { - // The flags no longer work in Chrome since - // https://chromium.googlesource.com/chromium/src/+/290ed8046692651ce76088914750cb659b65fb17%5E%21/chrome/browser/extensions/extension_service.cc?pli=1# - test.skip('chromium' !== mcpBrowser, '--load-extension is not supported for official builds of Chromium'); - - let browserContext: BrowserContext | undefined; - const userDataDir = testInfo.outputPath('extension-user-data-dir'); - await use({ - userDataDir, - launch: async (mode?: 'disable-extension') => { - browserContext = await chromium.launchPersistentContext(userDataDir, { - channel: mcpBrowser, - // Opening the browser singleton only works in headed. - headless: false, - // Automation disables singleton browser process behavior, which is necessary for the extension. - ignoreDefaultArgs: ['--enable-automation'], - args: mode === 'disable-extension' ? [] : [ - `--disable-extensions-except=${pathToExtension}`, - `--load-extension=${pathToExtension}`, - ], - }); - - // for manifest v3: - let [serviceWorker] = browserContext.serviceWorkers(); - if (!serviceWorker) - serviceWorker = await browserContext.waitForEvent('serviceworker'); - - return browserContext; - } - }); - await browserContext?.close(); - }, - - useShortConnectionTimeout: async ({}, use) => { - await use((timeoutMs: number) => { - process.env.PWMCP_TEST_CONNECTION_TIMEOUT = timeoutMs.toString(); - }); - process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined; - }, - - overrideProtocolVersion: async ({}, use) => { - await use((version: number) => { - process.env.PWMCP_TEST_PROTOCOL_VERSION = version.toString(); - }); - process.env.PWMCP_TEST_PROTOCOL_VERSION = undefined; - } -}); - -async function startAndCallConnectTool(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { - const { client } = await startClient({ - args: [`--connect-tool`], - config: { - browser: { - userDataDir: browserWithExtension.userDataDir, - } - }, - }); - - expect(await client.callTool({ - name: 'browser_connect', - arguments: { - name: 'extension' - } - })).toHaveResponse({ - result: 'Successfully changed connection method.', - }); - - return client; -} - -async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { - const { client } = await startClient({ - args: [`--extension`], - config: { - browser: { - userDataDir: browserWithExtension.userDataDir, - } - }, - }); - return client; -} - -const testWithOldExtensionVersion = test.extend({ - pathToExtension: async ({}, use, testInfo) => { - const extensionDir = testInfo.outputPath('extension'); - const oldPath = path.resolve(__dirname, '../dist'); - - await fs.promises.cp(oldPath, extensionDir, { recursive: true }); - const manifestPath = path.join(extensionDir, 'manifest.json'); - const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); - manifest.version = '0.0.1'; - await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); - - await use(extensionDir); - }, -}); - -for (const [mode, startClientMethod] of [ - ['connect-tool', startAndCallConnectTool], - ['extension-flag', startWithExtensionFlag], -] as const) { - - test(`navigate with extension (${mode})`, async ({ browserWithExtension, startClient, server }) => { - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const selectorPage = await confirmationPagePromise; - // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector - await selectorPage.getByRole('button', { name: 'Allow' }).click(); - - expect(await navigateResponse).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - }); - - test(`snapshot of an existing page (${mode})`, async ({ browserWithExtension, startClient, server }) => { - const browserContext = await browserWithExtension.launch(); - - const page = await browserContext.newPage(); - await page.goto(server.HELLO_WORLD); - - // Another empty page. - await browserContext.newPage(); - expect(browserContext.pages()).toHaveLength(3); - - const client = await startClientMethod(browserWithExtension, startClient); - expect(browserContext.pages()).toHaveLength(3); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_snapshot', - arguments: { }, - }); - - const selectorPage = await confirmationPagePromise; - expect(browserContext.pages()).toHaveLength(4); - - await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click(); - - expect(await navigateResponse).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - - expect(browserContext.pages()).toHaveLength(4); - }); - - test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { - useShortConnectionTimeout(100); - - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - result: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'), - isError: true, - }); - - await confirmationPagePromise; - }); - - testWithOldExtensionVersion(`works with old extension version (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { - useShortConnectionTimeout(500); - - // Prelaunch the browser, so that it is properly closed after the test. - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const selectorPage = await confirmationPagePromise; - // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector - await selectorPage.getByRole('button', { name: 'Allow' }).click(); - - expect(await navigateResponse).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - }); - - test(`extension needs update (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout, overrideProtocolVersion }) => { - useShortConnectionTimeout(500); - overrideProtocolVersion(1000); - - // Prelaunch the browser, so that it is properly closed after the test. - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const confirmationPage = await confirmationPagePromise; - await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`); - - expect(await navigateResponse).toHaveResponse({ - result: expect.stringContaining('Extension connection timeout.'), - isError: true, - }); - }); - -} - -test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => { - useShortConnectionTimeout(1000); - - const executablePath = test.info().outputPath('echo.sh'); - await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 }); - - const { client } = await startClient({ - args: [`--extension`], - config: { - browser: { - launchOptions: { - executablePath, - }, - } - }, - }); - - const navigateResponse = await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - timeout: 1000, - }); - expect(await navigateResponse).toHaveResponse({ - result: expect.stringContaining('Extension connection timeout.'), - isError: true, - }); - expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?'); -}); diff --git a/extension/tsconfig.json b/extension/tsconfig.json deleted file mode 100644 index 9c22b0b..0000000 --- a/extension/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "moduleResolution": "node", - "strict": true, - "module": "ESNext", - "rootDir": "src", - "outDir": "./dist/lib", - "resolveJsonModule": true, - "types": ["chrome"], - "jsx": "react-jsx", - "jsxImportSource": "react", - "noEmit": true - }, - "include": [ - "src", - ], - "exclude": [ - "src/ui", - ] -} diff --git a/extension/tsconfig.ui.json b/extension/tsconfig.ui.json deleted file mode 100644 index f62dd8a..0000000 --- a/extension/tsconfig.ui.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "moduleResolution": "node", - "strict": true, - "module": "ESNext", - "rootDir": "src", - "outDir": "./lib", - "resolveJsonModule": true, - "types": ["chrome"], - "jsx": "react-jsx", - "jsxImportSource": "react", - "noEmit": true, - }, - "include": [ - "src/ui", - ], -} diff --git a/extension/vite.config.mts b/extension/vite.config.mts deleted file mode 100644 index 89ec56c..0000000 --- a/extension/vite.config.mts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { viteStaticCopy } from 'vite-plugin-static-copy'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - react(), - viteStaticCopy({ - targets: [ - { - src: '../../icons/*', - dest: 'icons' - }, - { - src: '../../manifest.json', - dest: '.' - } - ] - }) - ], - root: resolve(__dirname, 'src/ui'), - build: { - outDir: resolve(__dirname, 'dist/'), - emptyOutDir: false, - minify: false, - rollupOptions: { - input: ['src/ui/connect.html', 'src/ui/status.html'], - output: { - manualChunks: undefined, - entryFileNames: 'lib/ui/[name].js', - chunkFileNames: 'lib/ui/[name].js', - assetFileNames: 'lib/ui/[name].[ext]' - } - } - } -}); diff --git a/extension/vite.sw.config.mts b/extension/vite.sw.config.mts deleted file mode 100644 index a383e4b..0000000 --- a/extension/vite.sw.config.mts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - build: { - lib: { - entry: resolve(__dirname, 'src/background.ts'), - fileName: 'lib/background', - formats: ['es'] - }, - outDir: 'dist', - emptyOutDir: false, - minify: false - } -}); diff --git a/index.js b/index.js index b153a9f..5df2c29 100755 --- a/index.js +++ b/index.js @@ -15,5 +15,5 @@ * limitations under the License. */ -const { createConnection } = require('./lib/index.js'); +const { createConnection } = require('playwright/lib/mcp/index'); module.exports = { createConnection }; diff --git a/package-lock.json b/package-lock.json index d0a05c9..0a45439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,642 +9,25 @@ "version": "0.0.36", "license": "Apache-2.0", "dependencies": { - "commander": "^13.1.0", - "debug": "^4.4.1", - "dotenv": "^17.2.0", - "mime": "^4.0.7", - "playwright": "1.56.0-alpha-1756505518000", - "playwright-core": "1.56.0-alpha-1756505518000", - "ws": "^8.18.1" + "playwright": "1.56.0-alpha-1756945786000", + "zod-to-json-schema": "^3.24.6" }, "bin": { "mcp-server-playwright": "cli.js" }, "devDependencies": { - "@anthropic-ai/sdk": "^0.57.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.19.0", - "@modelcontextprotocol/sdk": "^1.16.0", - "@playwright/test": "1.56.0-alpha-1756505518000", - "@stylistic/eslint-plugin": "^3.0.1", - "@types/debug": "^4.1.12", - "@types/node": "^22.13.10", - "@types/ws": "^8.18.1", - "@typescript-eslint/eslint-plugin": "^8.26.1", - "@typescript-eslint/parser": "^8.26.1", - "@typescript-eslint/utils": "^8.26.1", - "esbuild": "^0.20.1", - "eslint": "^9.19.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-notice": "^1.0.0", - "openai": "^5.10.2", - "typescript": "^5.8.2", - "zod": "^3.24.1", - "zod-to-json-schema": "^3.24.4" + "@modelcontextprotocol/sdk": "^1.17.5", + "@playwright/test": "1.56.0-alpha-1756945786000", + "@types/node": "^24.3.0" }, "engines": { "node": ">=18" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz", - "integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==", - "dev": true, - "license": "MIT", - "bin": { - "anthropic-ai-sdk": "bin/cli" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.4.tgz", - "integrity": "sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==", + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", "dev": true, "license": "MIT", "dependencies": { @@ -665,52 +48,14 @@ "node": ">=18" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@playwright/test": { - "version": "1.56.0-alpha-1756505518000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-1756505518000.tgz", - "integrity": "sha512-BLTEYook8jXHONKqmOgcG/q6SLZIyyJClgc+YJGg/G3w3dg1pE2dtdO/gECFnM8FX9UY4DOa9c6eJVU1feHk/w==", + "version": "1.56.0-alpha-1756945786000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-1756945786000.tgz", + "integrity": "sha512-I4+J2BnzDO2KLMhafHmVw8gPzWA0R4niu+tDHwD+YaJzaxj4lH/nxSvqSRZJ5lCEBrt/zbDbEyzNpBdqBy4/Wg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-1756505518000" + "playwright": "1.56.0-alpha-1756945786000" }, "bin": { "playwright": "cli.js" @@ -719,334 +64,14 @@ "node": ">=18" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stylistic/eslint-plugin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz", - "integrity": "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.13.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "estraverse": "^5.3.0", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, - "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", - "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/type-utils": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", - "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", - "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", - "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.27.0", - "@typescript-eslint/utils": "8.27.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", - "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", - "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/visitor-keys": "8.27.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.27.0", - "@typescript-eslint/types": "8.27.0", - "@typescript-eslint/typescript-estree": "8.27.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", - "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.27.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "undici-types": "~7.10.0" } }, "node_modules/accepts": { @@ -1063,29 +88,6 @@ "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1103,182 +105,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -1300,30 +126,6 @@ "node": ">=18" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1334,25 +136,6 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "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", @@ -1384,69 +167,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -1519,64 +239,11 @@ "node": ">= 8" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1590,49 +257,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1643,31 +267,6 @@ "node": ">= 0.8" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", - "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", @@ -1700,72 +299,6 @@ "node": ">= 0.8" } }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1799,92 +332,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1892,319 +339,6 @@ "dev": true, "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-notice": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-notice/-/eslint-plugin-notice-1.0.0.tgz", - "integrity": "sha512-M3VLQMZzZpvfTZ/vy9FmClIKq5rLBbQpM0KgfLZPJPrVXpmJYeobmmb+lfJzHWdNm8PWwvw8KlafQWo2N9xx1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-root": "^1.1.0", - "lodash": "^4.17.21", - "metric-lcs": "^0.1.2" - }, - "peerDependencies": { - "eslint": ">=3.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2304,36 +438,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2341,49 +445,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -2402,67 +463,6 @@ "node": ">= 0.8" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true, - "license": "MIT" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2507,37 +507,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "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", @@ -2577,67 +546,6 @@ "node": ">= 0.4" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2651,65 +559,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "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", @@ -2723,22 +572,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "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", @@ -2792,43 +625,6 @@ "node": ">=0.10.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2836,21 +632,6 @@ "dev": true, "license": "ISC" }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2861,239 +642,6 @@ "node": ">= 0.10" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -3101,158 +649,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3260,26 +656,6 @@ "dev": true, "license": "ISC" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3287,80 +663,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3394,52 +696,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/metric-lcs": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/metric-lcs/-/metric-lcs-0.1.2.tgz", - "integrity": "sha512-+TZ5dUDPKPJaU/rscTzxyN8ZkX7eAVLAiQU/e+YINleXPv03SCmJShaMT1If1liTH8OcmWXZs0CmzCBRBLcMpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", - "integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "license": "MIT", - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -3463,39 +719,10 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, @@ -3532,90 +759,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3639,109 +782,6 @@ "wrappy": "1" } }, - "node_modules/openai": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.10.2.tgz", - "integrity": "sha512-n+vi74LzHtvlKcDPn9aApgELGiu5CwhaLG40zxLTlFQdoSJCLACORIPC2uVQ3JEYAbqapM+XyRKFy2Thej7bIw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3752,16 +792,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3772,34 +802,15 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/pkce-challenge": { @@ -3813,12 +824,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-alpha-1756505518000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-1756505518000.tgz", - "integrity": "sha512-aChIG1Hly/pxzVdwOMArmOMNz4Wo2VyWBxLaMvLJaGWRPPB9+Sl1N8PRm6oH1CbbpFGpPvIeXl83LomkibShRA==", + "version": "1.56.0-alpha-1756945786000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-1756945786000.tgz", + "integrity": "sha512-Y07i9m/hZOQ8lLb4XDYAO24V1+qy+OCgxsW66fSlJJ064p8N4KjwKL5OJpVZofhf3EP8vp0adGePjBOjSwhVeA==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-1756505518000" + "playwright-core": "1.56.0-alpha-1756945786000" }, "bin": { "playwright": "cli.js" @@ -3831,9 +842,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-1756505518000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-1756505518000.tgz", - "integrity": "sha512-qeM+G9jA+PkA3dSYZmqKrARnIgd53B+7Lm3e52wH3rPyZJ+IBhRvhW369iN8tVJunbmsr7fkU1+05K2c7q9y0g==", + "version": "1.56.0-alpha-1756945786000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-1756945786000.tgz", + "integrity": "sha512-692W77VRoV2hq1bNwrZXsRBVOY8z5DDjV5jacB5det4v8rUcvOAGTqNUhoV17acP4mfbItM34VOPYOMtlJxjXg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -3842,26 +853,6 @@ "node": ">=18" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3902,27 +893,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3949,92 +919,6 @@ "node": ">= 0.8" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -4052,50 +936,6 @@ "node": ">= 18" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4117,41 +957,6 @@ ], "license": "MIT" }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4159,19 +964,6 @@ "dev": true, "license": "MIT" }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -4211,55 +1003,6 @@ "node": ">= 18" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4376,127 +1119,6 @@ "node": ">= 0.8" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4507,45 +1129,6 @@ "node": ">=0.6" } }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -4561,121 +1144,10 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -4725,105 +1197,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4831,55 +1204,19 @@ "dev": true, "license": "ISC" }, - "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "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/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "3.24.2", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.4.tgz", - "integrity": "sha512-0uNlcvgabyrni9Ag8Vghj21drk7+7tp7VTwwR7KxxXXc/3pbXz2PHlDgj3cICahgF1kHm4dExBFj7BXrZJXzig==", - "dev": true, + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "license": "ISC", "peerDependencies": { "zod": "^3.24.1" diff --git a/package.json b/package.json index 2ff240d..3c54e95 100644 --- a/package.json +++ b/package.json @@ -15,18 +15,12 @@ }, "license": "Apache-2.0", "scripts": { - "build": "tsc", - "lint": "npm run update-readme && npm run check-deps && eslint . && tsc --noEmit", - "lint-fix": "eslint . --fix", - "check-deps": "node utils/check-deps.js", - "update-readme": "node utils/update-readme.js", - "watch": "tsc --watch", + "lint": "npm run update-readme", + "update-readme": "node update-readme.js", "test": "playwright test", "ctest": "playwright test --project=chrome", "ftest": "playwright test --project=firefox", "wtest": "playwright test --project=webkit", - "run-server": "node lib/browserServer.js", - "clean": "rm -rf lib", "npm-publish": "npm run clean && npm run build && npm run test && npm publish" }, "exports": { @@ -37,37 +31,15 @@ } }, "dependencies": { - "commander": "^13.1.0", - "debug": "^4.4.1", - "dotenv": "^17.2.0", - "mime": "^4.0.7", - "playwright": "1.56.0-alpha-1756505518000", - "playwright-core": "1.56.0-alpha-1756505518000", - "ws": "^8.18.1" - }, - "devDependencies": { - "@anthropic-ai/sdk": "^0.57.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.19.0", - "@modelcontextprotocol/sdk": "^1.16.0", - "@playwright/test": "1.56.0-alpha-1756505518000", - "@stylistic/eslint-plugin": "^3.0.1", - "@types/debug": "^4.1.12", - "@types/node": "^22.13.10", - "@types/ws": "^8.18.1", - "@typescript-eslint/eslint-plugin": "^8.26.1", - "@typescript-eslint/parser": "^8.26.1", - "@typescript-eslint/utils": "^8.26.1", - "esbuild": "^0.20.1", - "eslint": "^9.19.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-notice": "^1.0.0", - "openai": "^5.10.2", - "typescript": "^5.8.2", - "zod": "^3.24.1", - "zod-to-json-schema": "^3.24.4" + "playwright": "1.56.0-alpha-1756945786000", + "zod-to-json-schema": "^3.24.6" }, "bin": { "mcp-server-playwright": "cli.js" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.17.5", + "@playwright/test": "1.56.0-alpha-1756945786000", + "@types/node": "^24.3.0" } } diff --git a/playwright.config.ts b/playwright.config.ts index c69e8f0..2c3abfb 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -26,7 +26,6 @@ export default defineConfig({ reporter: 'list', projects: [ { name: 'chrome' }, - { name: 'chromium', use: { mcpBrowser: 'chromium' } }, ...process.env.MCP_IN_DOCKER ? [{ name: 'chromium-docker', grep: /browser_navigate|browser_click/, @@ -35,8 +34,5 @@ export default defineConfig({ mcpMode: 'docker' as const } }] : [], - { name: 'firefox', use: { mcpBrowser: 'firefox' } }, - { name: 'webkit', use: { mcpBrowser: 'webkit' } }, - ... process.platform === 'win32' ? [{ name: 'msedge', use: { mcpBrowser: 'msedge' } }] : [], ], }); diff --git a/src/DEPS.list b/src/DEPS.list deleted file mode 100644 index ea6f40c..0000000 --- a/src/DEPS.list +++ /dev/null @@ -1,5 +0,0 @@ -[program.ts] -*** - -[index.ts] -*** \ No newline at end of file diff --git a/src/browser/DEPS.list b/src/browser/DEPS.list deleted file mode 100644 index 936611f..0000000 --- a/src/browser/DEPS.list +++ /dev/null @@ -1,5 +0,0 @@ -[*] -./tools/ -../sdk/ -../log.ts -../package.ts diff --git a/src/browser/actions.d.ts b/src/browser/actions.d.ts deleted file mode 100644 index e52b420..0000000 --- a/src/browser/actions.d.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -type Point = { x: number, y: number }; - -export type ActionName = - 'check' | - 'click' | - 'closePage' | - 'fill' | - 'navigate' | - 'openPage' | - 'press' | - 'select' | - 'uncheck' | - 'setInputFiles' | - 'assertText' | - 'assertValue' | - 'assertChecked' | - 'assertVisible' | - 'assertSnapshot'; - -export type ActionBase = { - name: ActionName, - signals: Signal[], - ariaSnapshot?: string, -}; - -export type ActionWithSelector = ActionBase & { - selector: string, - ref?: string, -}; - -export type ClickAction = ActionWithSelector & { - name: 'click', - button: 'left' | 'middle' | 'right', - modifiers: number, - clickCount: number, - position?: Point, -}; - -export type CheckAction = ActionWithSelector & { - name: 'check', -}; - -export type UncheckAction = ActionWithSelector & { - name: 'uncheck', -}; - -export type FillAction = ActionWithSelector & { - name: 'fill', - text: string, -}; - -export type NavigateAction = ActionBase & { - name: 'navigate', - url: string, -}; - -export type OpenPageAction = ActionBase & { - name: 'openPage', - url: string, -}; - -export type ClosesPageAction = ActionBase & { - name: 'closePage', -}; - -export type PressAction = ActionWithSelector & { - name: 'press', - key: string, - modifiers: number, -}; - -export type SelectAction = ActionWithSelector & { - name: 'select', - options: string[], -}; - -export type SetInputFilesAction = ActionWithSelector & { - name: 'setInputFiles', - files: string[], -}; - -export type AssertTextAction = ActionWithSelector & { - name: 'assertText', - text: string, - substring: boolean, -}; - -export type AssertValueAction = ActionWithSelector & { - name: 'assertValue', - value: string, -}; - -export type AssertCheckedAction = ActionWithSelector & { - name: 'assertChecked', - checked: boolean, -}; - -export type AssertVisibleAction = ActionWithSelector & { - name: 'assertVisible', -}; - -export type AssertSnapshotAction = ActionWithSelector & { - name: 'assertSnapshot', - ariaSnapshot: string, -}; - -export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction; -export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction; -export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction; - -// Signals. - -export type BaseSignal = { -}; - -export type NavigationSignal = BaseSignal & { - name: 'navigation', - url: string, -}; - -export type PopupSignal = BaseSignal & { - name: 'popup', - popupAlias: string, -}; - -export type DownloadSignal = BaseSignal & { - name: 'download', - downloadAlias: string, -}; - -export type DialogSignal = BaseSignal & { - name: 'dialog', - dialogAlias: string, -}; - -export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; - -export type FrameDescription = { - pageGuid: string; - pageAlias: string; - framePath: string[]; -}; - -export type ActionInContext = { - frame: FrameDescription; - description?: string; - action: Action; - startTime: number; - endTime?: number; -}; - -export type SignalInContext = { - frame: FrameDescription; - signal: Signal; - timestamp: number; -}; diff --git a/src/browser/browserContextFactory.ts b/src/browser/browserContextFactory.ts deleted file mode 100644 index 097b77c..0000000 --- a/src/browser/browserContextFactory.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import crypto from 'crypto'; -import fs from 'fs'; -import net from 'net'; -import path from 'path'; - -import * as playwright from 'playwright'; -// @ts-ignore -import { registryDirectory } from 'playwright-core/lib/server/registry/index'; -// @ts-ignore -import { startTraceViewerServer } from 'playwright-core/lib/server'; -import { logUnhandledError, testDebug } from '../log'; -import { outputFile } from './config'; - -import type { FullConfig } from './config'; - -export function contextFactory(config: FullConfig): BrowserContextFactory { - if (config.browser.remoteEndpoint) - return new RemoteContextFactory(config); - if (config.browser.cdpEndpoint) - return new CdpContextFactory(config); - if (config.browser.isolated) - return new IsolatedContextFactory(config); - return new PersistentContextFactory(config); -} - -export type ClientInfo = { name?: string, version?: string, rootPath?: string }; - -export interface BrowserContextFactory { - createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }>; -} - -class BaseContextFactory implements BrowserContextFactory { - readonly config: FullConfig; - private _logName: string; - protected _browserPromise: Promise | undefined; - - constructor(name: string, config: FullConfig) { - this._logName = name; - this.config = config; - } - - protected async _obtainBrowser(clientInfo: ClientInfo): Promise { - if (this._browserPromise) - return this._browserPromise; - testDebug(`obtain browser (${this._logName})`); - this._browserPromise = this._doObtainBrowser(clientInfo); - void this._browserPromise.then(browser => { - browser.on('disconnected', () => { - this._browserPromise = undefined; - }); - }).catch(() => { - this._browserPromise = undefined; - }); - return this._browserPromise; - } - - protected async _doObtainBrowser(clientInfo: ClientInfo): Promise { - throw new Error('Not implemented'); - } - - async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { - testDebug(`create browser context (${this._logName})`); - const browser = await this._obtainBrowser(clientInfo); - const browserContext = await this._doCreateContext(browser); - return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; - } - - protected async _doCreateContext(browser: playwright.Browser): Promise { - throw new Error('Not implemented'); - } - - private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) { - testDebug(`close browser context (${this._logName})`); - if (browser.contexts().length === 1) - this._browserPromise = undefined; - await browserContext.close().catch(logUnhandledError); - if (browser.contexts().length === 0) { - testDebug(`close browser (${this._logName})`); - await browser.close().catch(logUnhandledError); - } - } -} - -class IsolatedContextFactory extends BaseContextFactory { - constructor(config: FullConfig) { - super('isolated', config); - } - - protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { - await injectCdpPort(this.config.browser); - const browserType = playwright[this.config.browser.browserName]; - return browserType.launch({ - tracesDir: await startTraceServer(this.config, clientInfo.rootPath), - ...this.config.browser.launchOptions, - handleSIGINT: false, - handleSIGTERM: false, - }).catch(error => { - if (error.message.includes('Executable doesn\'t exist')) - throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); - throw error; - }); - } - - protected override async _doCreateContext(browser: playwright.Browser): Promise { - return browser.newContext(this.config.browser.contextOptions); - } -} - -class CdpContextFactory extends BaseContextFactory { - constructor(config: FullConfig) { - super('cdp', config); - } - - protected override async _doObtainBrowser(): Promise { - return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!); - } - - protected override async _doCreateContext(browser: playwright.Browser): Promise { - return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0]; - } -} - -class RemoteContextFactory extends BaseContextFactory { - constructor(config: FullConfig) { - super('remote', config); - } - - protected override async _doObtainBrowser(): Promise { - const url = new URL(this.config.browser.remoteEndpoint!); - url.searchParams.set('browser', this.config.browser.browserName); - if (this.config.browser.launchOptions) - url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions)); - return playwright[this.config.browser.browserName].connect(String(url)); - } - - protected override async _doCreateContext(browser: playwright.Browser): Promise { - return browser.newContext(); - } -} - -class PersistentContextFactory implements BrowserContextFactory { - readonly config: FullConfig; - readonly name = 'persistent'; - readonly description = 'Create a new persistent browser context'; - - private _userDataDirs = new Set(); - - constructor(config: FullConfig) { - this.config = config; - } - - async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { - await injectCdpPort(this.config.browser); - testDebug('create browser context (persistent)'); - const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath); - const tracesDir = await startTraceServer(this.config, clientInfo.rootPath); - - this._userDataDirs.add(userDataDir); - testDebug('lock user data dir', userDataDir); - - const browserType = playwright[this.config.browser.browserName]; - for (let i = 0; i < 5; i++) { - try { - const browserContext = await browserType.launchPersistentContext(userDataDir, { - tracesDir, - ...this.config.browser.launchOptions, - ...this.config.browser.contextOptions, - handleSIGINT: false, - handleSIGTERM: false, - }); - const close = () => this._closeBrowserContext(browserContext, userDataDir); - return { browserContext, close }; - } catch (error: any) { - if (error.message.includes('Executable doesn\'t exist')) - throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); - if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { - // User data directory is already in use, try again. - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } - throw error; - } - } - throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); - } - - private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { - testDebug('close browser context (persistent)'); - testDebug('release user data dir', userDataDir); - await browserContext.close().catch(() => {}); - this._userDataDirs.delete(userDataDir); - testDebug('close browser context complete (persistent)'); - } - - private async _createUserDataDir(rootPath: string | undefined) { - const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory; - const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName; - // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead. - const rootPathToken = rootPath ? `-${createHash(rootPath)}` : ''; - const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`); - await fs.promises.mkdir(result, { recursive: true }); - return result; - } -} - -async function injectCdpPort(browserConfig: FullConfig['browser']) { - if (browserConfig.browserName === 'chromium') - (browserConfig.launchOptions as any).cdpPort = await findFreePort(); -} - -async function findFreePort(): Promise { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - server.close(() => resolve(port)); - }); - server.on('error', reject); - }); -} - -async function startTraceServer(config: FullConfig, rootPath: string | undefined): Promise { - if (!config.saveTrace) - return undefined; - - const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`); - const server = await startTraceViewerServer(); - const urlPrefix = server.urlPrefix('human-readable'); - const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json'; - // eslint-disable-next-line no-console - console.error('\nTrace viewer listening on ' + url); - return tracesDir; -} - -function createHash(data: string): string { - return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); -} diff --git a/src/browser/browserServerBackend.ts b/src/browser/browserServerBackend.ts deleted file mode 100644 index c773786..0000000 --- a/src/browser/browserServerBackend.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { fileURLToPath } from 'url'; -import { FullConfig } from './config'; -import { Context } from './context'; -import { logUnhandledError } from '../log'; -import { Response } from './response'; -import { SessionLog } from './sessionLog'; -import { filteredTools } from './tools'; -import { toMcpTool } from '../sdk/tool'; - -import type { Tool } from './tools/tool'; -import type { BrowserContextFactory } from './browserContextFactory'; -import type * as mcpServer from '../sdk/server'; -import type { ServerBackend } from '../sdk/server'; - -export class BrowserServerBackend implements ServerBackend { - private _tools: Tool[]; - private _context: Context | undefined; - private _sessionLog: SessionLog | undefined; - private _config: FullConfig; - private _browserContextFactory: BrowserContextFactory; - - constructor(config: FullConfig, factory: BrowserContextFactory) { - this._config = config; - this._browserContextFactory = factory; - this._tools = filteredTools(config); - } - - async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { - let rootPath: string | undefined; - if (roots.length > 0) { - const firstRootUri = roots[0]?.uri; - const url = firstRootUri ? new URL(firstRootUri) : undefined; - rootPath = url ? fileURLToPath(url) : undefined; - } - this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined; - this._context = new Context({ - tools: this._tools, - config: this._config, - browserContextFactory: this._browserContextFactory, - sessionLog: this._sessionLog, - clientInfo: { ...clientVersion, rootPath }, - }); - } - - async listTools(): Promise { - return this._tools.map(tool => toMcpTool(tool.schema)); - } - - async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) { - const tool = this._tools.find(tool => tool.schema.name === name)!; - if (!tool) - throw new Error(`Tool "${name}" not found`); - const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); - const context = this._context!; - const response = new Response(context, name, parsedArguments); - context.setRunningTool(name); - try { - await tool.handle(context, parsedArguments, response); - await response.finish(); - this._sessionLog?.logResponse(response); - } catch (error: any) { - response.addError(String(error)); - } finally { - context.setRunningTool(undefined); - } - return response.serialize(); - } - - serverClosed() { - void this._context?.dispose().catch(logUnhandledError); - } -} diff --git a/src/browser/codegen.ts b/src/browser/codegen.ts deleted file mode 100644 index a3bc8d0..0000000 --- a/src/browser/codegen.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// adapted from: -// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/utils/isomorphic/stringUtils.ts -// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/server/codegen/javascript.ts - -// NOTE: this function should not be used to escape any selectors. -export function escapeWithQuotes(text: string, char: string = '\'') { - const stringified = JSON.stringify(text); - const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"'); - if (char === '\'') - return char + escapedText.replace(/[']/g, '\\\'') + char; - if (char === '"') - return char + escapedText.replace(/["]/g, '\\"') + char; - if (char === '`') - return char + escapedText.replace(/[`]/g, '\\`') + char; - throw new Error('Invalid escape char'); -} - -export function quote(text: string) { - return escapeWithQuotes(text, '\''); -} - -export function formatObject(value: any, indent = ' '): string { - if (typeof value === 'string') - return quote(value); - if (Array.isArray(value)) - return `[${value.map(o => formatObject(o)).join(', ')}]`; - if (typeof value === 'object') { - const keys = Object.keys(value).filter(key => value[key] !== undefined).sort(); - if (!keys.length) - return '{}'; - const tokens: string[] = []; - for (const key of keys) - tokens.push(`${key}: ${formatObject(value[key])}`); - return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`; - } - return String(value); -} diff --git a/src/browser/config.ts b/src/browser/config.ts deleted file mode 100644 index 64c8e5b..0000000 --- a/src/browser/config.ts +++ /dev/null @@ -1,327 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import { devices } from 'playwright'; - -import type { Config, ToolCapability } from '../../config'; -import type { BrowserContextOptions, LaunchOptions } from 'playwright'; - -export type CLIOptions = { - allowedOrigins?: string[]; - blockedOrigins?: string[]; - blockServiceWorkers?: boolean; - browser?: string; - caps?: string[]; - cdpEndpoint?: string; - config?: string; - device?: string; - executablePath?: string; - headless?: boolean; - host?: string; - ignoreHttpsErrors?: boolean; - isolated?: boolean; - imageResponses?: 'allow' | 'omit'; - sandbox?: boolean; - outputDir?: string; - port?: number; - proxyBypass?: string; - proxyServer?: string; - saveSession?: boolean; - saveTrace?: boolean; - storageState?: string; - userAgent?: string; - userDataDir?: string; - viewportSize?: string; -}; - -const defaultConfig: FullConfig = { - browser: { - browserName: 'chromium', - launchOptions: { - channel: 'chrome', - headless: os.platform() === 'linux' && !process.env.DISPLAY, - chromiumSandbox: true, - }, - contextOptions: { - viewport: null, - }, - }, - network: { - allowedOrigins: undefined, - blockedOrigins: undefined, - }, - server: {}, - saveTrace: false, -}; - -type BrowserUserConfig = NonNullable; - -export type FullConfig = Config & { - browser: Omit & { - browserName: 'chromium' | 'firefox' | 'webkit'; - launchOptions: NonNullable; - contextOptions: NonNullable; - }, - network: NonNullable, - saveTrace: boolean; - server: NonNullable, -}; - -export async function resolveConfig(config: Config): Promise { - return mergeConfig(defaultConfig, config); -} - -export async function resolveCLIConfig(cliOptions: CLIOptions): Promise { - const configInFile = await loadConfig(cliOptions.config); - const envOverrides = configFromEnv(); - const cliOverrides = configFromCLIOptions(cliOptions); - let result = defaultConfig; - result = mergeConfig(result, configInFile); - result = mergeConfig(result, envOverrides); - result = mergeConfig(result, cliOverrides); - return result; -} - -export function configFromCLIOptions(cliOptions: CLIOptions): Config { - let browserName: 'chromium' | 'firefox' | 'webkit' | undefined; - let channel: string | undefined; - switch (cliOptions.browser) { - case 'chrome': - case 'chrome-beta': - case 'chrome-canary': - case 'chrome-dev': - case 'chromium': - case 'msedge': - case 'msedge-beta': - case 'msedge-canary': - case 'msedge-dev': - browserName = 'chromium'; - channel = cliOptions.browser; - break; - case 'firefox': - browserName = 'firefox'; - break; - case 'webkit': - browserName = 'webkit'; - break; - } - - // Launch options - const launchOptions: LaunchOptions = { - channel, - executablePath: cliOptions.executablePath, - headless: cliOptions.headless, - }; - - // --no-sandbox was passed, disable the sandbox - if (cliOptions.sandbox === false) - launchOptions.chromiumSandbox = false; - - if (cliOptions.proxyServer) { - launchOptions.proxy = { - server: cliOptions.proxyServer - }; - if (cliOptions.proxyBypass) - launchOptions.proxy.bypass = cliOptions.proxyBypass; - } - - if (cliOptions.device && cliOptions.cdpEndpoint) - throw new Error('Device emulation is not supported with cdpEndpoint.'); - - // Context options - const contextOptions: BrowserContextOptions = cliOptions.device ? devices[cliOptions.device] : {}; - if (cliOptions.storageState) - contextOptions.storageState = cliOptions.storageState; - - if (cliOptions.userAgent) - contextOptions.userAgent = cliOptions.userAgent; - - if (cliOptions.viewportSize) { - try { - const [width, height] = cliOptions.viewportSize.split(',').map(n => +n); - if (isNaN(width) || isNaN(height)) - throw new Error('bad values'); - contextOptions.viewport = { width, height }; - } catch (e) { - throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"'); - } - } - - if (cliOptions.ignoreHttpsErrors) - contextOptions.ignoreHTTPSErrors = true; - - if (cliOptions.blockServiceWorkers) - contextOptions.serviceWorkers = 'block'; - - const result: Config = { - browser: { - browserName, - isolated: cliOptions.isolated, - userDataDir: cliOptions.userDataDir, - launchOptions, - contextOptions, - cdpEndpoint: cliOptions.cdpEndpoint, - }, - server: { - port: cliOptions.port, - host: cliOptions.host, - }, - capabilities: cliOptions.caps as ToolCapability[], - network: { - allowedOrigins: cliOptions.allowedOrigins, - blockedOrigins: cliOptions.blockedOrigins, - }, - saveSession: cliOptions.saveSession, - saveTrace: cliOptions.saveTrace, - outputDir: cliOptions.outputDir, - imageResponses: cliOptions.imageResponses, - }; - - return result; -} - -function configFromEnv(): Config { - const options: CLIOptions = {}; - options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS); - options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS); - options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS); - options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER); - options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS); - options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT); - options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG); - options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE); - options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH); - options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS); - options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST); - options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS); - options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED); - if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit') - options.imageResponses = 'omit'; - options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX); - options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR); - options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT); - options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS); - options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER); - options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE); - options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE); - options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT); - options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR); - options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE); - return configFromCLIOptions(options); -} - -async function loadConfig(configFile: string | undefined): Promise { - if (!configFile) - return {}; - - try { - return JSON.parse(await fs.promises.readFile(configFile, 'utf8')); - } catch (error) { - throw new Error(`Failed to load config file: ${configFile}, ${error}`); - } -} - -export async function outputFile(config: FullConfig, rootPath: string | undefined, name: string): Promise { - const outputDir = config.outputDir - ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined) - ?? path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())); - - await fs.promises.mkdir(outputDir, { recursive: true }); - const fileName = sanitizeForFilePath(name); - return path.join(outputDir, fileName); -} - -function pickDefined(obj: T | undefined): Partial { - return Object.fromEntries( - Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined) - ) as Partial; -} - -function mergeConfig(base: FullConfig, overrides: Config): FullConfig { - const browser: FullConfig['browser'] = { - ...pickDefined(base.browser), - ...pickDefined(overrides.browser), - browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium', - isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false, - launchOptions: { - ...pickDefined(base.browser?.launchOptions), - ...pickDefined(overrides.browser?.launchOptions), - ...{ assistantMode: true }, - }, - contextOptions: { - ...pickDefined(base.browser?.contextOptions), - ...pickDefined(overrides.browser?.contextOptions), - }, - }; - - if (browser.browserName !== 'chromium' && browser.launchOptions) - delete browser.launchOptions.channel; - - return { - ...pickDefined(base), - ...pickDefined(overrides), - browser, - network: { - ...pickDefined(base.network), - ...pickDefined(overrides.network), - }, - server: { - ...pickDefined(base.server), - ...pickDefined(overrides.server), - }, - } as FullConfig; -} - -export function semicolonSeparatedList(value: string | undefined): string[] | undefined { - if (!value) - return undefined; - return value.split(';').map(v => v.trim()); -} - -export function commaSeparatedList(value: string | undefined): string[] | undefined { - if (!value) - return undefined; - return value.split(',').map(v => v.trim()); -} - -function envToNumber(value: string | undefined): number | undefined { - if (!value) - return undefined; - return +value; -} - -function envToBoolean(value: string | undefined): boolean | undefined { - if (value === 'true' || value === '1') - return true; - if (value === 'false' || value === '0') - return false; - return undefined; -} - -function envToString(value: string | undefined): string | undefined { - return value ? value.trim() : undefined; -} - -function sanitizeForFilePath(s: string) { - const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); - const separator = s.lastIndexOf('.'); - if (separator === -1) - return sanitize(s); - return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1)); -} diff --git a/src/browser/context.ts b/src/browser/context.ts deleted file mode 100644 index b209849..0000000 --- a/src/browser/context.ts +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; -import * as playwright from 'playwright'; - -import { logUnhandledError } from '../log'; -import { Tab } from './tab'; -import { outputFile } from './config'; - -import type { FullConfig } from './config'; -import type { Tool } from './tools/tool'; -import type { BrowserContextFactory, ClientInfo } from './browserContextFactory'; -import type * as actions from './actions'; -import type { SessionLog } from './sessionLog'; - -const testDebug = debug('pw:mcp:test'); - -type ContextOptions = { - tools: Tool[]; - config: FullConfig; - browserContextFactory: BrowserContextFactory; - sessionLog: SessionLog | undefined; - clientInfo: ClientInfo; -}; - -export class Context { - readonly tools: Tool[]; - readonly config: FullConfig; - readonly sessionLog: SessionLog | undefined; - readonly options: ContextOptions; - private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; - private _browserContextFactory: BrowserContextFactory; - private _tabs: Tab[] = []; - private _currentTab: Tab | undefined; - private _clientInfo: ClientInfo; - - private static _allContexts: Set = new Set(); - private _closeBrowserContextPromise: Promise | undefined; - private _runningToolName: string | undefined; - private _abortController = new AbortController(); - - constructor(options: ContextOptions) { - this.tools = options.tools; - this.config = options.config; - this.sessionLog = options.sessionLog; - this.options = options; - this._browserContextFactory = options.browserContextFactory; - this._clientInfo = options.clientInfo; - testDebug('create context'); - Context._allContexts.add(this); - } - - static async disposeAll() { - await Promise.all([...Context._allContexts].map(context => context.dispose())); - } - - tabs(): Tab[] { - return this._tabs; - } - - currentTab(): Tab | undefined { - return this._currentTab; - } - - currentTabOrDie(): Tab { - if (!this._currentTab) - throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.'); - return this._currentTab; - } - - async newTab(): Promise { - const { browserContext } = await this._ensureBrowserContext(); - const page = await browserContext.newPage(); - this._currentTab = this._tabs.find(t => t.page === page)!; - return this._currentTab; - } - - async selectTab(index: number) { - const tab = this._tabs[index]; - if (!tab) - throw new Error(`Tab ${index} not found`); - await tab.page.bringToFront(); - this._currentTab = tab; - return tab; - } - - async ensureTab(): Promise { - const { browserContext } = await this._ensureBrowserContext(); - if (!this._currentTab) - await browserContext.newPage(); - return this._currentTab!; - } - - async closeTab(index: number | undefined): Promise { - const tab = index === undefined ? this._currentTab : this._tabs[index]; - if (!tab) - throw new Error(`Tab ${index} not found`); - const url = tab.page.url(); - await tab.page.close(); - return url; - } - - async outputFile(name: string): Promise { - return outputFile(this.config, this._clientInfo.rootPath, name); - } - - private _onPageCreated(page: playwright.Page) { - const tab = new Tab(this, page, tab => this._onPageClosed(tab)); - this._tabs.push(tab); - if (!this._currentTab) - this._currentTab = tab; - } - - private _onPageClosed(tab: Tab) { - const index = this._tabs.indexOf(tab); - if (index === -1) - return; - this._tabs.splice(index, 1); - - if (this._currentTab === tab) - this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)]; - if (!this._tabs.length) - void this.closeBrowserContext(); - } - - async closeBrowserContext() { - if (!this._closeBrowserContextPromise) - this._closeBrowserContextPromise = this._closeBrowserContextImpl().catch(logUnhandledError); - await this._closeBrowserContextPromise; - this._closeBrowserContextPromise = undefined; - } - - isRunningTool() { - return this._runningToolName !== undefined; - } - - setRunningTool(name: string | undefined) { - this._runningToolName = name; - } - - private async _closeBrowserContextImpl() { - if (!this._browserContextPromise) - return; - - testDebug('close context'); - - const promise = this._browserContextPromise; - this._browserContextPromise = undefined; - - await promise.then(async ({ browserContext, close }) => { - if (this.config.saveTrace) - await browserContext.tracing.stop(); - await close(); - }); - } - - async dispose() { - this._abortController.abort('MCP context disposed'); - await this.closeBrowserContext(); - Context._allContexts.delete(this); - } - - private async _setupRequestInterception(context: playwright.BrowserContext) { - if (this.config.network?.allowedOrigins?.length) { - await context.route('**', route => route.abort('blockedbyclient')); - - for (const origin of this.config.network.allowedOrigins) - await context.route(`*://${origin}/**`, route => route.continue()); - } - - if (this.config.network?.blockedOrigins?.length) { - for (const origin of this.config.network.blockedOrigins) - await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient')); - } - } - - private _ensureBrowserContext() { - if (!this._browserContextPromise) { - this._browserContextPromise = this._setupBrowserContext(); - this._browserContextPromise.catch(() => { - this._browserContextPromise = undefined; - }); - } - return this._browserContextPromise; - } - - private async _setupBrowserContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { - if (this._closeBrowserContextPromise) - throw new Error('Another browser context is being closed.'); - // TODO: move to the browser context factory to make it based on isolation mode. - const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName); - const { browserContext } = result; - await this._setupRequestInterception(browserContext); - if (this.sessionLog) - await InputRecorder.create(this, browserContext); - for (const page of browserContext.pages()) - this._onPageCreated(page); - browserContext.on('page', page => this._onPageCreated(page)); - if (this.config.saveTrace) { - await browserContext.tracing.start({ - name: 'trace', - screenshots: false, - snapshots: true, - sources: false, - }); - } - return result; - } -} - -export class InputRecorder { - private _context: Context; - private _browserContext: playwright.BrowserContext; - - private constructor(context: Context, browserContext: playwright.BrowserContext) { - this._context = context; - this._browserContext = browserContext; - } - - static async create(context: Context, browserContext: playwright.BrowserContext) { - const recorder = new InputRecorder(context, browserContext); - await recorder._initialize(); - return recorder; - } - - private async _initialize() { - const sessionLog = this._context.sessionLog!; - await (this._browserContext as any)._enableRecorder({ - mode: 'recording', - recorderMode: 'api', - }, { - actionAdded: (page: playwright.Page, data: actions.ActionInContext, code: string) => { - if (this._context.isRunningTool()) - return; - const tab = Tab.forPage(page); - if (tab) - sessionLog.logUserAction(data.action, tab, code, false); - }, - actionUpdated: (page: playwright.Page, data: actions.ActionInContext, code: string) => { - if (this._context.isRunningTool()) - return; - const tab = Tab.forPage(page); - if (tab) - sessionLog.logUserAction(data.action, tab, code, true); - }, - signalAdded: (page: playwright.Page, data: actions.SignalInContext) => { - if (this._context.isRunningTool()) - return; - if (data.signal.name !== 'navigation') - return; - const tab = Tab.forPage(page); - const navigateAction: actions.Action = { - name: 'navigate', - url: data.signal.url, - signals: [], - }; - if (tab) - sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false); - }, - }); - } -} diff --git a/src/browser/response.ts b/src/browser/response.ts deleted file mode 100644 index 3dc95ab..0000000 --- a/src/browser/response.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { renderModalStates } from './tab'; - -import type { Tab, TabSnapshot } from './tab'; -import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; -import type { Context } from './context'; - -export class Response { - private _result: string[] = []; - private _code: string[] = []; - private _images: { contentType: string, data: Buffer }[] = []; - private _context: Context; - private _includeSnapshot = false; - private _includeTabs = false; - private _tabSnapshot: TabSnapshot | undefined; - - readonly toolName: string; - readonly toolArgs: Record; - private _isError: boolean | undefined; - - constructor(context: Context, toolName: string, toolArgs: Record) { - this._context = context; - this.toolName = toolName; - this.toolArgs = toolArgs; - } - - addResult(result: string) { - this._result.push(result); - } - - addError(error: string) { - this._result.push(error); - this._isError = true; - } - - isError() { - return this._isError; - } - - result() { - return this._result.join('\n'); - } - - addCode(code: string) { - this._code.push(code); - } - - code() { - return this._code.join('\n'); - } - - addImage(image: { contentType: string, data: Buffer }) { - this._images.push(image); - } - - images() { - return this._images; - } - - setIncludeSnapshot() { - this._includeSnapshot = true; - } - - setIncludeTabs() { - this._includeTabs = true; - } - - async finish() { - // All the async snapshotting post-action is happening here. - // Everything below should race against modal states. - if (this._includeSnapshot && this._context.currentTab()) - this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(); - for (const tab of this._context.tabs()) - await tab.updateTitle(); - } - - tabSnapshot(): TabSnapshot | undefined { - return this._tabSnapshot; - } - - serialize(): { content: (TextContent | ImageContent)[], isError?: boolean } { - const response: string[] = []; - - // Start with command result. - if (this._result.length) { - response.push('### Result'); - response.push(this._result.join('\n')); - response.push(''); - } - - // Add code if it exists. - if (this._code.length) { - response.push(`### Ran Playwright code -\`\`\`js -${this._code.join('\n')} -\`\`\``); - response.push(''); - } - - // List browser tabs. - if (this._includeSnapshot || this._includeTabs) - response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs)); - - // Add snapshot if provided. - if (this._tabSnapshot?.modalStates.length) { - response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates)); - response.push(''); - } else if (this._tabSnapshot) { - response.push(renderTabSnapshot(this._tabSnapshot)); - response.push(''); - } - - // Main response part - const content: (TextContent | ImageContent)[] = [ - { type: 'text', text: response.join('\n') }, - ]; - - // Image attachments. - if (this._context.config.imageResponses !== 'omit') { - for (const image of this._images) - content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType }); - } - - return { content, isError: this._isError }; - } -} - -function renderTabSnapshot(tabSnapshot: TabSnapshot): string { - const lines: string[] = []; - - if (tabSnapshot.consoleMessages.length) { - lines.push(`### New console messages`); - for (const message of tabSnapshot.consoleMessages) - lines.push(`- ${trim(message.toString(), 100)}`); - lines.push(''); - } - - if (tabSnapshot.downloads.length) { - lines.push(`### Downloads`); - for (const entry of tabSnapshot.downloads) { - if (entry.finished) - lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`); - else - lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`); - } - lines.push(''); - } - - lines.push(`### Page state`); - lines.push(`- Page URL: ${tabSnapshot.url}`); - lines.push(`- Page Title: ${tabSnapshot.title}`); - lines.push(`- Page Snapshot:`); - lines.push('```yaml'); - lines.push(tabSnapshot.ariaSnapshot); - lines.push('```'); - - return lines.join('\n'); -} - -function renderTabsMarkdown(tabs: Tab[], force: boolean = false): string[] { - if (tabs.length === 1 && !force) - return []; - - if (!tabs.length) { - return [ - '### Open tabs', - 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.', - '', - ]; - } - - const lines: string[] = ['### Open tabs']; - for (let i = 0; i < tabs.length; i++) { - const tab = tabs[i]; - const current = tab.isCurrentTab() ? ' (current)' : ''; - lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`); - } - lines.push(''); - return lines; -} - -function trim(text: string, maxLength: number) { - if (text.length <= maxLength) - return text; - return text.slice(0, maxLength) + '...'; -} diff --git a/src/browser/sessionLog.ts b/src/browser/sessionLog.ts deleted file mode 100644 index 7e80087..0000000 --- a/src/browser/sessionLog.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; - -import { Response } from './response'; -import { logUnhandledError } from '../log'; -import { outputFile } from './config'; - -import type { FullConfig } from './config'; -import type * as actions from './actions'; -import type { Tab, TabSnapshot } from './tab'; - -type LogEntry = { - timestamp: number; - toolCall?: { - toolName: string; - toolArgs: Record; - result: string; - isError?: boolean; - }; - userAction?: actions.Action; - code: string; - tabSnapshot?: TabSnapshot; -}; - -export class SessionLog { - private _folder: string; - private _file: string; - private _ordinal = 0; - private _pendingEntries: LogEntry[] = []; - private _sessionFileQueue = Promise.resolve(); - private _flushEntriesTimeout: NodeJS.Timeout | undefined; - - constructor(sessionFolder: string) { - this._folder = sessionFolder; - this._file = path.join(this._folder, 'session.md'); - } - - static async create(config: FullConfig, rootPath: string | undefined): Promise { - const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`); - await fs.promises.mkdir(sessionFolder, { recursive: true }); - // eslint-disable-next-line no-console - console.error(`Session: ${sessionFolder}`); - return new SessionLog(sessionFolder); - } - - logResponse(response: Response) { - const entry: LogEntry = { - timestamp: performance.now(), - toolCall: { - toolName: response.toolName, - toolArgs: response.toolArgs, - result: response.result(), - isError: response.isError(), - }, - code: response.code(), - tabSnapshot: response.tabSnapshot(), - }; - this._appendEntry(entry); - } - - logUserAction(action: actions.Action, tab: Tab, code: string, isUpdate: boolean) { - code = code.trim(); - if (isUpdate) { - const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; - if (lastEntry.userAction?.name === action.name) { - lastEntry.userAction = action; - lastEntry.code = code; - return; - } - } - if (action.name === 'navigate') { - // Already logged at this location. - const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; - if (lastEntry?.tabSnapshot?.url === action.url) - return; - } - const entry: LogEntry = { - timestamp: performance.now(), - userAction: action, - code, - tabSnapshot: { - url: tab.page.url(), - title: '', - ariaSnapshot: action.ariaSnapshot || '', - modalStates: [], - consoleMessages: [], - downloads: [], - }, - }; - this._appendEntry(entry); - } - - private _appendEntry(entry: LogEntry) { - this._pendingEntries.push(entry); - if (this._flushEntriesTimeout) - clearTimeout(this._flushEntriesTimeout); - this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1000); - } - - private async _flushEntries() { - clearTimeout(this._flushEntriesTimeout); - const entries = this._pendingEntries; - this._pendingEntries = []; - const lines: string[] = ['']; - - for (const entry of entries) { - const ordinal = (++this._ordinal).toString().padStart(3, '0'); - if (entry.toolCall) { - lines.push( - `### Tool call: ${entry.toolCall.toolName}`, - `- Args`, - '```json', - JSON.stringify(entry.toolCall.toolArgs, null, 2), - '```', - ); - if (entry.toolCall.result) { - lines.push( - entry.toolCall.isError ? `- Error` : `- Result`, - '```', - entry.toolCall.result, - '```', - ); - } - } - - if (entry.userAction) { - const actionData = { ...entry.userAction } as any; - delete actionData.ariaSnapshot; - delete actionData.selector; - delete actionData.signals; - - lines.push( - `### User action: ${entry.userAction.name}`, - `- Args`, - '```json', - JSON.stringify(actionData, null, 2), - '```', - ); - } - - if (entry.code) { - lines.push( - `- Code`, - '```js', - entry.code, - '```'); - } - - if (entry.tabSnapshot) { - const fileName = `${ordinal}.snapshot.yml`; - fs.promises.writeFile(path.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(logUnhandledError); - lines.push(`- Snapshot: ${fileName}`); - } - - lines.push('', ''); - } - - this._sessionFileQueue = this._sessionFileQueue.then(() => fs.promises.appendFile(this._file, lines.join('\n'))); - } -} diff --git a/src/browser/tab.ts b/src/browser/tab.ts deleted file mode 100644 index 8d9ea5c..0000000 --- a/src/browser/tab.ts +++ /dev/null @@ -1,313 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { EventEmitter } from 'events'; -import * as playwright from 'playwright'; -import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; -import { logUnhandledError } from '../log'; -import { ManualPromise } from '../sdk/manualPromise'; -import { ModalState } from './tools/tool'; - -import type { Context } from './context'; - -type PageEx = playwright.Page & { - _snapshotForAI: () => Promise; -}; - -export const TabEvents = { - modalState: 'modalState' -}; - -export type TabEventsInterface = { - [TabEvents.modalState]: [modalState: ModalState]; -}; - -export type TabSnapshot = { - url: string; - title: string; - ariaSnapshot: string; - modalStates: ModalState[]; - consoleMessages: ConsoleMessage[]; - downloads: { download: playwright.Download, finished: boolean, outputFile: string }[]; -}; - -export class Tab extends EventEmitter { - readonly context: Context; - readonly page: playwright.Page; - private _lastTitle = 'about:blank'; - private _consoleMessages: ConsoleMessage[] = []; - private _recentConsoleMessages: ConsoleMessage[] = []; - private _requests: Map = new Map(); - private _onPageClose: (tab: Tab) => void; - private _modalStates: ModalState[] = []; - private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = []; - - constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { - super(); - this.context = context; - this.page = page; - this._onPageClose = onPageClose; - page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event))); - page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error))); - page.on('request', request => this._requests.set(request, null)); - page.on('response', response => this._requests.set(response.request(), response)); - page.on('close', () => this._onClose()); - page.on('filechooser', chooser => { - this.setModalState({ - type: 'fileChooser', - description: 'File chooser', - fileChooser: chooser, - }); - }); - page.on('dialog', dialog => this._dialogShown(dialog)); - page.on('download', download => { - void this._downloadStarted(download); - }); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(5000); - (page as any)[tabSymbol] = this; - } - - static forPage(page: playwright.Page): Tab | undefined { - return (page as any)[tabSymbol]; - } - - modalStates(): ModalState[] { - return this._modalStates; - } - - setModalState(modalState: ModalState) { - this._modalStates.push(modalState); - this.emit(TabEvents.modalState, modalState); - } - - clearModalState(modalState: ModalState) { - this._modalStates = this._modalStates.filter(state => state !== modalState); - } - - modalStatesMarkdown(): string[] { - return renderModalStates(this.context, this.modalStates()); - } - - private _dialogShown(dialog: playwright.Dialog) { - this.setModalState({ - type: 'dialog', - description: `"${dialog.type()}" dialog with message "${dialog.message()}"`, - dialog, - }); - } - - private async _downloadStarted(download: playwright.Download) { - const entry = { - download, - finished: false, - outputFile: await this.context.outputFile(download.suggestedFilename()) - }; - this._downloads.push(entry); - await download.saveAs(entry.outputFile); - entry.finished = true; - } - - private _clearCollectedArtifacts() { - this._consoleMessages.length = 0; - this._recentConsoleMessages.length = 0; - this._requests.clear(); - } - - private _handleConsoleMessage(message: ConsoleMessage) { - this._consoleMessages.push(message); - this._recentConsoleMessages.push(message); - } - - private _onClose() { - this._clearCollectedArtifacts(); - this._onPageClose(this); - } - - async updateTitle() { - await this._raceAgainstModalStates(async () => { - this._lastTitle = await callOnPageNoTrace(this.page, page => page.title()); - }); - } - - lastTitle(): string { - return this._lastTitle; - } - - isCurrentTab(): boolean { - return this === this.context.currentTab(); - } - - async waitForLoadState(state: 'load', options?: { timeout?: number }): Promise { - await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError)); - } - - async navigate(url: string) { - this._clearCollectedArtifacts(); - - const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(logUnhandledError)); - try { - await this.page.goto(url, { waitUntil: 'domcontentloaded' }); - } catch (_e: unknown) { - const e = _e as Error; - const mightBeDownload = - e.message.includes('net::ERR_ABORTED') // chromium - || e.message.includes('Download is starting'); // firefox + webkit - if (!mightBeDownload) - throw e; - // on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit - const download = await Promise.race([ - downloadEvent, - new Promise(resolve => setTimeout(resolve, 3000)), - ]); - if (!download) - throw e; - // Make sure other "download" listeners are notified first. - await new Promise(resolve => setTimeout(resolve, 500)); - return; - } - - // Cap load event to 5 seconds, the page is operational at this point. - await this.waitForLoadState('load', { timeout: 5000 }); - } - - consoleMessages(): ConsoleMessage[] { - return this._consoleMessages; - } - - requests(): Map { - return this._requests; - } - - async captureSnapshot(): Promise { - let tabSnapshot: TabSnapshot | undefined; - const modalStates = await this._raceAgainstModalStates(async () => { - const snapshot = await (this.page as PageEx)._snapshotForAI(); - tabSnapshot = { - url: this.page.url(), - title: await this.page.title(), - ariaSnapshot: snapshot, - modalStates: [], - consoleMessages: [], - downloads: this._downloads, - }; - }); - if (tabSnapshot) { - // Assign console message late so that we did not lose any to modal state. - tabSnapshot.consoleMessages = this._recentConsoleMessages; - this._recentConsoleMessages = []; - } - return tabSnapshot ?? { - url: this.page.url(), - title: '', - ariaSnapshot: '', - modalStates, - consoleMessages: [], - downloads: [], - }; - } - - private _javaScriptBlocked(): boolean { - return this._modalStates.some(state => state.type === 'dialog'); - } - - private async _raceAgainstModalStates(action: () => Promise): Promise { - if (this.modalStates().length) - return this.modalStates(); - - const promise = new ManualPromise(); - const listener = (modalState: ModalState) => promise.resolve([modalState]); - this.once(TabEvents.modalState, listener); - - return await Promise.race([ - action().then(() => { - this.off(TabEvents.modalState, listener); - return []; - }), - promise, - ]); - } - - async waitForCompletion(callback: () => Promise) { - await this._raceAgainstModalStates(() => waitForCompletion(this, callback)); - } - - async refLocator(params: { element: string, ref: string }): Promise { - return (await this.refLocators([params]))[0]; - } - - async refLocators(params: { element: string, ref: string }[]): Promise { - const snapshot = await (this.page as PageEx)._snapshotForAI(); - return params.map(param => { - if (!snapshot.includes(`[ref=${param.ref}]`)) - throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`); - return this.page.locator(`aria-ref=${param.ref}`).describe(param.element); - }); - } - - async waitForTimeout(time: number) { - if (this._javaScriptBlocked()) { - await new Promise(f => setTimeout(f, time)); - return; - } - - await callOnPageNoTrace(this.page, page => { - return page.evaluate(() => new Promise(f => setTimeout(f, 1000))); - }); - } -} - -export type ConsoleMessage = { - type: ReturnType | undefined; - text: string; - toString(): string; -}; - -function messageToConsoleMessage(message: playwright.ConsoleMessage): ConsoleMessage { - return { - type: message.type(), - text: message.text(), - toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`, - }; -} - -function pageErrorToConsoleMessage(errorOrValue: Error | any): ConsoleMessage { - if (errorOrValue instanceof Error) { - return { - type: undefined, - text: errorOrValue.message, - toString: () => errorOrValue.stack || errorOrValue.message, - }; - } - return { - type: undefined, - text: String(errorOrValue), - toString: () => String(errorOrValue), - }; -} - -export function renderModalStates(context: Context, modalStates: ModalState[]): string[] { - const result: string[] = ['### Modal state']; - if (modalStates.length === 0) - result.push('- There is no modal state present'); - for (const state of modalStates) { - const tool = context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type); - result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`); - } - return result; -} - -const tabSymbol = Symbol('tabSymbol'); diff --git a/src/browser/tools.ts b/src/browser/tools.ts deleted file mode 100644 index b1e8059..0000000 --- a/src/browser/tools.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import common from './tools/common'; -import console from './tools/console'; -import dialogs from './tools/dialogs'; -import evaluate from './tools/evaluate'; -import files from './tools/files'; -import form from './tools/form'; -import install from './tools/install'; -import keyboard from './tools/keyboard'; -import mouse from './tools/mouse'; -import navigate from './tools/navigate'; -import network from './tools/network'; -import pdf from './tools/pdf'; -import snapshot from './tools/snapshot'; -import tabs from './tools/tabs'; -import screenshot from './tools/screenshot'; -import wait from './tools/wait'; -import verify from './tools/verify'; - -import type { Tool } from './tools/tool'; -import type { FullConfig } from './config'; - -export const allTools: Tool[] = [ - ...common, - ...console, - ...dialogs, - ...evaluate, - ...files, - ...form, - ...install, - ...keyboard, - ...navigate, - ...network, - ...mouse, - ...pdf, - ...screenshot, - ...snapshot, - ...tabs, - ...wait, - ...verify, -]; - -export function filteredTools(config: FullConfig) { - return allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability)); -} diff --git a/src/browser/tools/DEPS.list b/src/browser/tools/DEPS.list deleted file mode 100644 index 11683db..0000000 --- a/src/browser/tools/DEPS.list +++ /dev/null @@ -1,3 +0,0 @@ -[*] -../ -../../sdk/ \ No newline at end of file diff --git a/src/browser/tools/common.ts b/src/browser/tools/common.ts deleted file mode 100644 index 62f175e..0000000 --- a/src/browser/tools/common.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool, defineTool } from './tool'; - -const close = defineTool({ - capability: 'core', - - schema: { - name: 'browser_close', - title: 'Close browser', - description: 'Close the page', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (context, params, response) => { - await context.closeBrowserContext(); - response.setIncludeTabs(); - response.addCode(`await page.close()`); - }, -}); - -const resize = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_resize', - title: 'Resize browser window', - description: 'Resize the browser window', - inputSchema: z.object({ - width: z.number().describe('Width of the browser window'), - height: z.number().describe('Height of the browser window'), - }), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`); - - await tab.waitForCompletion(async () => { - await tab.page.setViewportSize({ width: params.width, height: params.height }); - }); - }, -}); - -export default [ - close, - resize -]; diff --git a/src/browser/tools/console.ts b/src/browser/tools/console.ts deleted file mode 100644 index 3ad0ba4..0000000 --- a/src/browser/tools/console.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; - -const console = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_console_messages', - title: 'Get console messages', - description: 'Returns all console messages', - inputSchema: z.object({}), - type: 'readOnly', - }, - handle: async (tab, params, response) => { - tab.consoleMessages().map(message => response.addResult(message.toString())); - }, -}); - -export default [ - console, -]; diff --git a/src/browser/tools/dialogs.ts b/src/browser/tools/dialogs.ts deleted file mode 100644 index f6954b7..0000000 --- a/src/browser/tools/dialogs.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; - -const handleDialog = defineTabTool({ - capability: 'core', - - schema: { - name: 'browser_handle_dialog', - title: 'Handle a dialog', - description: 'Handle a dialog', - inputSchema: z.object({ - accept: z.boolean().describe('Whether to accept the dialog.'), - promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - const dialogState = tab.modalStates().find(state => state.type === 'dialog'); - if (!dialogState) - throw new Error('No dialog visible'); - - tab.clearModalState(dialogState); - await tab.waitForCompletion(async () => { - if (params.accept) - await dialogState.dialog.accept(params.promptText); - else - await dialogState.dialog.dismiss(); - }); - }, - - clearsModalState: 'dialog', -}); - -export default [ - handleDialog, -]; diff --git a/src/browser/tools/evaluate.ts b/src/browser/tools/evaluate.ts deleted file mode 100644 index b3e20ee..0000000 --- a/src/browser/tools/evaluate.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; -import * as javascript from '../codegen'; -import { generateLocator } from './utils'; - -import type * as playwright from 'playwright'; - -const evaluateSchema = z.object({ - function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), - element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'), - ref: z.string().optional().describe('Exact target element reference from the page snapshot'), -}); - -const evaluate = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_evaluate', - title: 'Evaluate JavaScript', - description: 'Evaluate JavaScript expression on page or element', - inputSchema: evaluateSchema, - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - let locator: playwright.Locator | undefined; - if (params.ref && params.element) { - locator = await tab.refLocator({ ref: params.ref, element: params.element }); - response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`); - } else { - response.addCode(`await page.evaluate(${javascript.quote(params.function)});`); - } - - await tab.waitForCompletion(async () => { - const receiver = locator ?? tab.page as any; - const result = await receiver._evaluateFunction(params.function); - response.addResult(JSON.stringify(result, null, 2) || 'undefined'); - }); - }, -}); - -export default [ - evaluate, -]; diff --git a/src/browser/tools/files.ts b/src/browser/tools/files.ts deleted file mode 100644 index c1cdf66..0000000 --- a/src/browser/tools/files.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; - -const uploadFile = defineTabTool({ - capability: 'core', - - schema: { - name: 'browser_file_upload', - title: 'Upload files', - description: 'Upload one or multiple files', - inputSchema: z.object({ - paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - const modalState = tab.modalStates().find(state => state.type === 'fileChooser'); - if (!modalState) - throw new Error('No file chooser visible'); - - response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`); - - tab.clearModalState(modalState); - await tab.waitForCompletion(async () => { - await modalState.fileChooser.setFiles(params.paths); - }); - }, - clearsModalState: 'fileChooser', -}); - -export default [ - uploadFile, -]; diff --git a/src/browser/tools/form.ts b/src/browser/tools/form.ts deleted file mode 100644 index aad12ad..0000000 --- a/src/browser/tools/form.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; -import { generateLocator } from './utils'; -import * as javascript from '../codegen'; - -const fillForm = defineTabTool({ - capability: 'core', - - schema: { - name: 'browser_fill_form', - title: 'Fill form', - description: 'Fill multiple form fields', - inputSchema: z.object({ - fields: z.array(z.object({ - name: z.string().describe('Human-readable field name'), - type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'), - ref: z.string().describe('Exact target field reference from the page snapshot'), - value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'), - })).describe('Fields to fill in'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - for (const field of params.fields) { - const locator = await tab.refLocator({ element: field.name, ref: field.ref }); - const locatorSource = `await page.${await generateLocator(locator)}`; - if (field.type === 'textbox' || field.type === 'slider') { - await locator.fill(field.value); - response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`); - } else if (field.type === 'checkbox' || field.type === 'radio') { - await locator.setChecked(field.value === 'true'); - response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`); - } else if (field.type === 'combobox') { - await locator.selectOption({ label: field.value }); - response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`); - } - } - }, -}); - -export default [ - fillForm, -]; diff --git a/src/browser/tools/install.ts b/src/browser/tools/install.ts deleted file mode 100644 index 54d67a7..0000000 --- a/src/browser/tools/install.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { fork } from 'child_process'; -import path from 'path'; - -import { z } from '../../sdk/bundle'; -import { defineTool } from './tool'; - -const install = defineTool({ - capability: 'core-install', - schema: { - name: 'browser_install', - title: 'Install the browser specified in the config', - description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.', - inputSchema: z.object({}), - type: 'destructive', - }, - - handle: async (context, params, response) => { - const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome'; - const cliPath = path.join(require.resolve('playwright/package.json'), '../cli.js'); - const child = fork(cliPath, ['install', channel], { - stdio: 'pipe', - }); - const output: string[] = []; - child.stdout?.on('data', data => output.push(data.toString())); - child.stderr?.on('data', data => output.push(data.toString())); - await new Promise((resolve, reject) => { - child.on('close', code => { - if (code === 0) - resolve(); - else - reject(new Error(`Failed to install browser: ${output.join('')}`)); - }); - }); - response.setIncludeTabs(); - }, -}); - -export default [ - install, -]; diff --git a/src/browser/tools/keyboard.ts b/src/browser/tools/keyboard.ts deleted file mode 100644 index e567bcb..0000000 --- a/src/browser/tools/keyboard.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; -import { elementSchema } from './snapshot'; -import { generateLocator } from './utils'; -import * as javascript from '../codegen'; - -const pressKey = defineTabTool({ - capability: 'core', - - schema: { - name: 'browser_press_key', - title: 'Press a key', - description: 'Press a key on the keyboard', - inputSchema: z.object({ - key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - response.addCode(`// Press ${params.key}`); - response.addCode(`await page.keyboard.press('${params.key}');`); - - await tab.waitForCompletion(async () => { - await tab.page.keyboard.press(params.key); - }); - }, -}); - -const typeSchema = elementSchema.extend({ - text: z.string().describe('Text to type into the element'), - submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'), - slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'), -}); - -const type = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_type', - title: 'Type text', - description: 'Type text into editable element', - inputSchema: typeSchema, - type: 'destructive', - }, - - handle: async (tab, params, response) => { - const locator = await tab.refLocator(params); - - await tab.waitForCompletion(async () => { - if (params.slowly) { - response.setIncludeSnapshot(); - response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`); - await locator.pressSequentially(params.text); - } else { - response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`); - await locator.fill(params.text); - } - - if (params.submit) { - response.setIncludeSnapshot(); - response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`); - await locator.press('Enter'); - } - }); - }, -}); - -export default [ - pressKey, - type, -]; diff --git a/src/browser/tools/mouse.ts b/src/browser/tools/mouse.ts deleted file mode 100644 index 0e0fc88..0000000 --- a/src/browser/tools/mouse.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; - -const elementSchema = z.object({ - element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), -}); - -const mouseMove = defineTabTool({ - capability: 'vision', - schema: { - name: 'browser_mouse_move_xy', - title: 'Move mouse', - description: 'Move mouse to a given position', - inputSchema: elementSchema.extend({ - x: z.number().describe('X coordinate'), - y: z.number().describe('Y coordinate'), - }), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - response.addCode(`// Move mouse to (${params.x}, ${params.y})`); - response.addCode(`await page.mouse.move(${params.x}, ${params.y});`); - - await tab.waitForCompletion(async () => { - await tab.page.mouse.move(params.x, params.y); - }); - }, -}); - -const mouseClick = defineTabTool({ - capability: 'vision', - schema: { - name: 'browser_mouse_click_xy', - title: 'Click', - description: 'Click left mouse button at a given position', - inputSchema: elementSchema.extend({ - x: z.number().describe('X coordinate'), - y: z.number().describe('Y coordinate'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - response.addCode(`// Click mouse at coordinates (${params.x}, ${params.y})`); - response.addCode(`await page.mouse.move(${params.x}, ${params.y});`); - response.addCode(`await page.mouse.down();`); - response.addCode(`await page.mouse.up();`); - - await tab.waitForCompletion(async () => { - await tab.page.mouse.move(params.x, params.y); - await tab.page.mouse.down(); - await tab.page.mouse.up(); - }); - }, -}); - -const mouseDrag = defineTabTool({ - capability: 'vision', - schema: { - name: 'browser_mouse_drag_xy', - title: 'Drag mouse', - description: 'Drag left mouse button to a given position', - inputSchema: elementSchema.extend({ - startX: z.number().describe('Start X coordinate'), - startY: z.number().describe('Start Y coordinate'), - endX: z.number().describe('End X coordinate'), - endY: z.number().describe('End Y coordinate'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - response.addCode(`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`); - response.addCode(`await page.mouse.move(${params.startX}, ${params.startY});`); - response.addCode(`await page.mouse.down();`); - response.addCode(`await page.mouse.move(${params.endX}, ${params.endY});`); - response.addCode(`await page.mouse.up();`); - - await tab.waitForCompletion(async () => { - await tab.page.mouse.move(params.startX, params.startY); - await tab.page.mouse.down(); - await tab.page.mouse.move(params.endX, params.endY); - await tab.page.mouse.up(); - }); - }, -}); - -export default [ - mouseMove, - mouseClick, - mouseDrag, -]; diff --git a/src/browser/tools/navigate.ts b/src/browser/tools/navigate.ts deleted file mode 100644 index 9998756..0000000 --- a/src/browser/tools/navigate.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTool, defineTabTool } from './tool'; - -const navigate = defineTool({ - capability: 'core', - - schema: { - name: 'browser_navigate', - title: 'Navigate to a URL', - description: 'Navigate to a URL', - inputSchema: z.object({ - url: z.string().describe('The URL to navigate to'), - }), - type: 'destructive', - }, - - handle: async (context, params, response) => { - const tab = await context.ensureTab(); - await tab.navigate(params.url); - - response.setIncludeSnapshot(); - response.addCode(`await page.goto('${params.url}');`); - }, -}); - -const goBack = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_navigate_back', - title: 'Go back', - description: 'Go back to the previous page', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - await tab.page.goBack(); - response.setIncludeSnapshot(); - response.addCode(`await page.goBack();`); - }, -}); - -export default [ - navigate, - goBack, -]; diff --git a/src/browser/tools/network.ts b/src/browser/tools/network.ts deleted file mode 100644 index 6860fda..0000000 --- a/src/browser/tools/network.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; - -import type * as playwright from 'playwright'; - -const requests = defineTabTool({ - capability: 'core', - - schema: { - name: 'browser_network_requests', - title: 'List network requests', - description: 'Returns all network requests since loading the page', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const requests = tab.requests(); - [...requests.entries()].forEach(([req, res]) => response.addResult(renderRequest(req, res))); - }, -}); - -function renderRequest(request: playwright.Request, response: playwright.Response | null) { - const result: string[] = []; - result.push(`[${request.method().toUpperCase()}] ${request.url()}`); - if (response) - result.push(`=> [${response.status()}] ${response.statusText()}`); - return result.join(' '); -} - -export default [ - requests, -]; diff --git a/src/browser/tools/pdf.ts b/src/browser/tools/pdf.ts deleted file mode 100644 index b4c9b14..0000000 --- a/src/browser/tools/pdf.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; -import * as javascript from '../codegen'; - -const pdfSchema = z.object({ - filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'), -}); - -const pdf = defineTabTool({ - capability: 'pdf', - - schema: { - name: 'browser_pdf_save', - title: 'Save as PDF', - description: 'Save page as PDF', - inputSchema: pdfSchema, - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`); - response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`); - response.addResult(`Saved page as ${fileName}`); - await tab.page.pdf({ path: fileName }); - }, -}); - -export default [ - pdf, -]; diff --git a/src/browser/tools/screenshot.ts b/src/browser/tools/screenshot.ts deleted file mode 100644 index e1f3789..0000000 --- a/src/browser/tools/screenshot.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; -import * as javascript from '../codegen'; -import { generateLocator } from './utils'; - -import type * as playwright from 'playwright'; - -const screenshotSchema = z.object({ - type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'), - filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'), - element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'), - ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'), - fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'), -}).refine(data => { - return !!data.element === !!data.ref; -}, { - message: 'Both element and ref must be provided or neither.', - path: ['ref', 'element'] -}).refine(data => { - return !(data.fullPage && (data.element || data.ref)); -}, { - message: 'fullPage cannot be used with element screenshots.', - path: ['fullPage'] -}); - -const screenshot = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_take_screenshot', - title: 'Take a screenshot', - description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`, - inputSchema: screenshotSchema, - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const fileType = params.type || 'png'; - const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`); - const options: playwright.PageScreenshotOptions = { - type: fileType, - quality: fileType === 'png' ? undefined : 90, - scale: 'css', - path: fileName, - ...(params.fullPage !== undefined && { fullPage: params.fullPage }) - }; - const isElementScreenshot = params.element && params.ref; - - const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport'); - response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`); - - // Only get snapshot when element screenshot is needed - const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null; - - if (locator) - response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`); - else - response.addCode(`await page.screenshot(${javascript.formatObject(options)});`); - - const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options); - response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`); - - // https://github.com/microsoft/playwright-mcp/issues/817 - // Never return large images to LLM, saving them to the file system is enough. - if (!params.fullPage) { - response.addImage({ - contentType: fileType === 'png' ? 'image/png' : 'image/jpeg', - data: buffer - }); - } - } -}); - -export default [ - screenshot, -]; diff --git a/src/browser/tools/snapshot.ts b/src/browser/tools/snapshot.ts deleted file mode 100644 index 03c6852..0000000 --- a/src/browser/tools/snapshot.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool, defineTool } from './tool'; -import * as javascript from '../codegen'; -import { generateLocator } from './utils'; - -const snapshot = defineTool({ - capability: 'core', - schema: { - name: 'browser_snapshot', - title: 'Page snapshot', - description: 'Capture accessibility snapshot of the current page, this is better than screenshot', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (context, params, response) => { - await context.ensureTab(); - response.setIncludeSnapshot(); - }, -}); - -export const elementSchema = z.object({ - element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), - ref: z.string().describe('Exact target element reference from the page snapshot'), -}); - -const clickSchema = elementSchema.extend({ - doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'), - button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'), -}); - -const click = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_click', - title: 'Click', - description: 'Perform click on a web page', - inputSchema: clickSchema, - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - const locator = await tab.refLocator(params); - const button = params.button; - const buttonAttr = button ? `{ button: '${button}' }` : ''; - - if (params.doubleClick) - response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`); - else - response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`); - - - await tab.waitForCompletion(async () => { - if (params.doubleClick) - await locator.dblclick({ button }); - else - await locator.click({ button }); - }); - }, -}); - -const drag = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_drag', - title: 'Drag mouse', - description: 'Perform drag and drop between two elements', - inputSchema: z.object({ - startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'), - startRef: z.string().describe('Exact source element reference from the page snapshot'), - endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'), - endRef: z.string().describe('Exact target element reference from the page snapshot'), - }), - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - const [startLocator, endLocator] = await tab.refLocators([ - { ref: params.startRef, element: params.startElement }, - { ref: params.endRef, element: params.endElement }, - ]); - - await tab.waitForCompletion(async () => { - await startLocator.dragTo(endLocator); - }); - - response.addCode(`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`); - }, -}); - -const hover = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_hover', - title: 'Hover mouse', - description: 'Hover over element on page', - inputSchema: elementSchema, - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - const locator = await tab.refLocator(params); - response.addCode(`await page.${await generateLocator(locator)}.hover();`); - - await tab.waitForCompletion(async () => { - await locator.hover(); - }); - }, -}); - -const selectOptionSchema = elementSchema.extend({ - values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'), -}); - -const selectOption = defineTabTool({ - capability: 'core', - schema: { - name: 'browser_select_option', - title: 'Select option', - description: 'Select an option in a dropdown', - inputSchema: selectOptionSchema, - type: 'destructive', - }, - - handle: async (tab, params, response) => { - response.setIncludeSnapshot(); - - const locator = await tab.refLocator(params); - response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`); - - await tab.waitForCompletion(async () => { - await locator.selectOption(params.values); - }); - }, -}); - -export default [ - snapshot, - click, - drag, - hover, - selectOption, -]; diff --git a/src/browser/tools/tabs.ts b/src/browser/tools/tabs.ts deleted file mode 100644 index 77ad91f..0000000 --- a/src/browser/tools/tabs.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTool } from './tool'; - -const browserTabs = defineTool({ - capability: 'core-tabs', - - schema: { - name: 'browser_tabs', - title: 'Manage tabs', - description: 'List, create, close, or select a browser tab.', - inputSchema: z.object({ - action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'), - index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'), - }), - type: 'destructive', - }, - - handle: async (context, params, response) => { - switch (params.action) { - case 'list': { - await context.ensureTab(); - response.setIncludeTabs(); - return; - } - case 'new': { - await context.newTab(); - response.setIncludeTabs(); - return; - } - case 'close': { - await context.closeTab(params.index); - response.setIncludeSnapshot(); - return; - } - case 'select': { - if (params.index === undefined) - throw new Error('Tab index is required'); - await context.selectTab(params.index); - response.setIncludeSnapshot(); - return; - } - } - }, -}); - -export default [ - browserTabs, -]; diff --git a/src/browser/tools/tool.ts b/src/browser/tools/tool.ts deleted file mode 100644 index 8f4cdd5..0000000 --- a/src/browser/tools/tool.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { z } from 'zod'; -import type { Context } from '../context'; -import type * as playwright from 'playwright'; -import type { ToolCapability } from '../../../config'; -import type { Tab } from '../tab'; -import type { Response } from '../response'; -import type { ToolSchema } from '../../sdk/tool'; - -export type FileUploadModalState = { - type: 'fileChooser'; - description: string; - fileChooser: playwright.FileChooser; -}; - -export type DialogModalState = { - type: 'dialog'; - description: string; - dialog: playwright.Dialog; -}; - -export type ModalState = FileUploadModalState | DialogModalState; - -export type Tool = { - capability: ToolCapability; - schema: ToolSchema; - handle: (context: Context, params: z.output, response: Response) => Promise; -}; - -export function defineTool(tool: Tool): Tool { - return tool; -} - -export type TabTool = { - capability: ToolCapability; - schema: ToolSchema; - clearsModalState?: ModalState['type']; - handle: (tab: Tab, params: z.output, response: Response) => Promise; -}; - -export function defineTabTool(tool: TabTool): Tool { - return { - ...tool, - handle: async (context, params, response) => { - const tab = context.currentTabOrDie(); - const modalStates = tab.modalStates().map(state => state.type); - if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState)) - response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n')); - else if (!tool.clearsModalState && modalStates.length) - response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n')); - else - return tool.handle(tab, params, response); - }, - }; -} diff --git a/src/browser/tools/utils.ts b/src/browser/tools/utils.ts deleted file mode 100644 index eae01cb..0000000 --- a/src/browser/tools/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// @ts-ignore -import { asLocator } from 'playwright-core/lib/utils'; - -import type * as playwright from 'playwright'; -import type { Tab } from '../tab'; - -export async function waitForCompletion(tab: Tab, callback: () => Promise): Promise { - const requests = new Set(); - let frameNavigated = false; - let waitCallback: () => void = () => {}; - const waitBarrier = new Promise(f => { waitCallback = f; }); - - const requestListener = (request: playwright.Request) => requests.add(request); - const requestFinishedListener = (request: playwright.Request) => { - requests.delete(request); - if (!requests.size) - waitCallback(); - }; - - const frameNavigateListener = (frame: playwright.Frame) => { - if (frame.parentFrame()) - return; - frameNavigated = true; - dispose(); - clearTimeout(timeout); - void tab.waitForLoadState('load').then(waitCallback); - }; - - const onTimeout = () => { - dispose(); - waitCallback(); - }; - - tab.page.on('request', requestListener); - tab.page.on('requestfinished', requestFinishedListener); - tab.page.on('framenavigated', frameNavigateListener); - const timeout = setTimeout(onTimeout, 10000); - - const dispose = () => { - tab.page.off('request', requestListener); - tab.page.off('requestfinished', requestFinishedListener); - tab.page.off('framenavigated', frameNavigateListener); - clearTimeout(timeout); - }; - - try { - const result = await callback(); - if (!requests.size && !frameNavigated) - waitCallback(); - await waitBarrier; - await tab.waitForTimeout(1000); - return result; - } finally { - dispose(); - } -} - -export async function generateLocator(locator: playwright.Locator): Promise { - try { - const { resolvedSelector } = await (locator as any)._resolveSelector(); - return asLocator('javascript', resolvedSelector); - } catch (e) { - throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.'); - } -} - -export async function callOnPageNoTrace(page: playwright.Page, callback: (page: playwright.Page) => Promise): Promise { - return await (page as any)._wrapApiCall(() => callback(page), { internal: true }); -} diff --git a/src/browser/tools/verify.ts b/src/browser/tools/verify.ts deleted file mode 100644 index 809ed2a..0000000 --- a/src/browser/tools/verify.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTabTool } from './tool'; -import * as javascript from '../codegen'; -import { generateLocator } from './utils'; - -const verifyElement = defineTabTool({ - capability: 'verify', - schema: { - name: 'browser_verify_element_visible', - title: 'Verify element visible', - description: 'Verify element is visible on the page', - inputSchema: z.object({ - role: z.string().describe('ROLE of the element. Can be found in the snapshot like this: \`- {ROLE} "Accessible Name":\`'), - accessibleName: z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: \`- role "{ACCESSIBLE_NAME}"\`'), - }), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const locator = tab.page.getByRole(params.role as any, { name: params.accessibleName }); - if (await locator.count() === 0) { - response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`); - return; - } - - response.addCode(`await expect(page.getByRole(${javascript.escapeWithQuotes(params.role)}, { name: ${javascript.escapeWithQuotes(params.accessibleName)} })).toBeVisible();`); - response.addResult('Done'); - }, -}); - -const verifyText = defineTabTool({ - capability: 'verify', - schema: { - name: 'browser_verify_text_visible', - title: 'Verify text visible', - description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`, - inputSchema: z.object({ - text: z.string().describe('TEXT to verify. Can be found in the snapshot like this: \`- role "Accessible Name": {TEXT}\` or like this: \`- text: {TEXT}\`'), - }), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const locator = tab.page.getByText(params.text).filter({ visible: true }); - if (await locator.count() === 0) { - response.addError('Text not found'); - return; - } - - response.addCode(`await expect(page.getByText(${javascript.escapeWithQuotes(params.text)})).toBeVisible();`); - response.addResult('Done'); - }, -}); - -const verifyList = defineTabTool({ - capability: 'verify', - schema: { - name: 'browser_verify_list_visible', - title: 'Verify list visible', - description: 'Verify list is visible on the page', - inputSchema: z.object({ - element: z.string().describe('Human-readable list description'), - ref: z.string().describe('Exact target element reference that points to the list'), - items: z.array(z.string()).describe('Items to verify'), - }), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const locator = await tab.refLocator({ ref: params.ref, element: params.element }); - const itemTexts: string[] = []; - for (const item of params.items) { - const itemLocator = locator.getByText(item); - if (await itemLocator.count() === 0) { - response.addError(`Item "${item}" not found`); - return; - } - itemTexts.push((await itemLocator.textContent())!); - } - const ariaSnapshot = `\` -- list: -${itemTexts.map(t => ` - listitem: ${javascript.escapeWithQuotes(t, '"')}`).join('\n')} -\``; - response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`); - response.addResult('Done'); - }, -}); - -const verifyValue = defineTabTool({ - capability: 'verify', - schema: { - name: 'browser_verify_value', - title: 'Verify value', - description: 'Verify element value', - inputSchema: z.object({ - type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the element'), - element: z.string().describe('Human-readable element description'), - ref: z.string().describe('Exact target element reference that points to the element'), - value: z.string().describe('Value to verify. For checkbox, use "true" or "false".'), - }), - type: 'readOnly', - }, - - handle: async (tab, params, response) => { - const locator = await tab.refLocator({ ref: params.ref, element: params.element }); - const locatorSource = `page.${await generateLocator(locator)}`; - if (params.type === 'textbox' || params.type === 'slider' || params.type === 'combobox') { - const value = await locator.inputValue(); - if (value !== params.value) { - response.addError(`Expected value "${params.value}", but got "${value}"`); - return; - } - response.addCode(`await expect(${locatorSource}).toHaveValue(${javascript.quote(params.value)});`); - } else if (params.type === 'checkbox' || params.type === 'radio') { - const value = await locator.isChecked(); - if (value !== (params.value === 'true')) { - response.addError(`Expected value "${params.value}", but got "${value}"`); - return; - } - const matcher = value ? 'toBeChecked' : 'not.toBeChecked'; - response.addCode(`await expect(${locatorSource}).${matcher}();`); - } - response.addResult('Done'); - }, -}); - -export default [ - verifyElement, - verifyText, - verifyList, - verifyValue, -]; diff --git a/src/browser/tools/wait.ts b/src/browser/tools/wait.ts deleted file mode 100644 index a0f5382..0000000 --- a/src/browser/tools/wait.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../sdk/bundle'; -import { defineTool } from './tool'; - -const wait = defineTool({ - capability: 'core', - - schema: { - name: 'browser_wait_for', - title: 'Wait for', - description: 'Wait for text to appear or disappear or a specified time to pass', - inputSchema: z.object({ - time: z.number().optional().describe('The time to wait in seconds'), - text: z.string().optional().describe('The text to wait for'), - textGone: z.string().optional().describe('The text to wait for to disappear'), - }), - type: 'readOnly', - }, - - handle: async (context, params, response) => { - if (!params.text && !params.textGone && !params.time) - throw new Error('Either time, text or textGone must be provided'); - - if (params.time) { - response.addCode(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`); - await new Promise(f => setTimeout(f, Math.min(30000, params.time! * 1000))); - } - - const tab = context.currentTabOrDie(); - const locator = params.text ? tab.page.getByText(params.text).first() : undefined; - const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined; - - if (goneLocator) { - response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`); - await goneLocator.waitFor({ state: 'hidden' }); - } - - if (locator) { - response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`); - await locator.waitFor({ state: 'visible' }); - } - - response.addResult(`Waited for ${params.text || params.textGone || params.time}`); - response.setIncludeSnapshot(); - }, -}); - -export default [ - wait, -]; diff --git a/src/extension/DEPS.list b/src/extension/DEPS.list deleted file mode 100644 index 65f47af..0000000 --- a/src/extension/DEPS.list +++ /dev/null @@ -1,4 +0,0 @@ -[*] -../sdk/ -../browser/ -../log.ts diff --git a/src/extension/cdpRelay.ts b/src/extension/cdpRelay.ts deleted file mode 100644 index 7408b44..0000000 --- a/src/extension/cdpRelay.ts +++ /dev/null @@ -1,422 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * WebSocket server that bridges Playwright MCP and Chrome Extension - * - * Endpoints: - * - /cdp/guid - Full CDP interface for Playwright MCP - * - /extension/guid - Extension connection for chrome.debugger forwarding - */ - -import { spawn } from 'child_process'; -import http from 'http'; -import debug from 'debug'; -import { WebSocket, WebSocketServer } from 'ws'; - -// @ts-ignore -import { registry } from 'playwright-core/lib/server/registry/index'; - -import { httpAddressToString } from '../sdk/http'; -import { logUnhandledError } from '../log'; -import { ManualPromise } from '../sdk/manualPromise'; -import * as protocol from './protocol'; - -import type websocket from 'ws'; -import type { ClientInfo } from '../browser/browserContextFactory'; -import type { ExtensionCommand, ExtensionEvents } from './protocol'; - - -const debugLogger = debug('pw:mcp:relay'); - -type CDPCommand = { - id: number; - sessionId?: string; - method: string; - params?: any; -}; - -type CDPResponse = { - id?: number; - sessionId?: string; - method?: string; - params?: any; - result?: any; - error?: { code?: number; message: string }; -}; - -export class CDPRelayServer { - private _wsHost: string; - private _browserChannel: string; - private _userDataDir?: string; - private _executablePath?: string; - private _cdpPath: string; - private _extensionPath: string; - private _wss: WebSocketServer; - private _playwrightConnection: WebSocket | null = null; - private _extensionConnection: ExtensionConnection | null = null; - private _connectedTabInfo: { - targetInfo: any; - // Page sessionId that should be used by this connection. - sessionId: string; - } | undefined; - private _nextSessionId: number = 1; - private _extensionConnectionPromise!: ManualPromise; - - constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) { - this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws'); - this._browserChannel = browserChannel; - this._userDataDir = userDataDir; - this._executablePath = executablePath; - - const uuid = crypto.randomUUID(); - this._cdpPath = `/cdp/${uuid}`; - this._extensionPath = `/extension/${uuid}`; - - this._resetExtensionConnection(); - this._wss = new WebSocketServer({ server }); - this._wss.on('connection', this._onConnection.bind(this)); - } - - cdpEndpoint() { - return `${this._wsHost}${this._cdpPath}`; - } - - extensionEndpoint() { - return `${this._wsHost}${this._extensionPath}`; - } - - async ensureExtensionConnectionForMCPContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined) { - debugLogger('Ensuring extension connection for MCP context'); - if (this._extensionConnection) - return; - this._connectBrowser(clientInfo, toolName); - debugLogger('Waiting for incoming extension connection'); - await Promise.race([ - this._extensionConnectionPromise, - new Promise((_, reject) => setTimeout(() => { - reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md for installation instructions.`)); - }, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5_000)), - new Promise((_, reject) => abortSignal.addEventListener('abort', reject)) - ]); - debugLogger('Extension connection established'); - } - - private _connectBrowser(clientInfo: ClientInfo, toolName: string | undefined) { - const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`; - // Need to specify "key" in the manifest.json to make the id stable when loading from file. - const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint); - const client = { - name: clientInfo.name, - version: clientInfo.version, - }; - url.searchParams.set('client', JSON.stringify(client)); - url.searchParams.set('protocolVersion', process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString()); - if (toolName) - url.searchParams.set('newTab', String(toolName === 'browser_navigate')); - const href = url.toString(); - - let executablePath = this._executablePath; - if (!executablePath) { - const executableInfo = registry.findExecutable(this._browserChannel); - if (!executableInfo) - throw new Error(`Unsupported channel: "${this._browserChannel}"`); - executablePath = executableInfo.executablePath(); - if (!executablePath) - throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`); - } - - const args: string[] = []; - if (this._userDataDir) - args.push(`--user-data-dir=${this._userDataDir}`); - args.push(href); - - spawn(executablePath, args, { - windowsHide: true, - detached: true, - shell: false, - stdio: 'ignore', - }); - } - - stop(): void { - this.closeConnections('Server stopped'); - this._wss.close(); - } - - closeConnections(reason: string) { - this._closePlaywrightConnection(reason); - this._closeExtensionConnection(reason); - } - - private _onConnection(ws: WebSocket, request: http.IncomingMessage): void { - const url = new URL(`http://localhost${request.url}`); - debugLogger(`New connection to ${url.pathname}`); - if (url.pathname === this._cdpPath) { - this._handlePlaywrightConnection(ws); - } else if (url.pathname === this._extensionPath) { - this._handleExtensionConnection(ws); - } else { - debugLogger(`Invalid path: ${url.pathname}`); - ws.close(4004, 'Invalid path'); - } - } - - private _handlePlaywrightConnection(ws: WebSocket): void { - if (this._playwrightConnection) { - debugLogger('Rejecting second Playwright connection'); - ws.close(1000, 'Another CDP client already connected'); - return; - } - this._playwrightConnection = ws; - ws.on('message', async data => { - try { - const message = JSON.parse(data.toString()); - await this._handlePlaywrightMessage(message); - } catch (error: any) { - debugLogger(`Error while handling Playwright message\n${data.toString()}\n`, error); - } - }); - ws.on('close', () => { - if (this._playwrightConnection !== ws) - return; - this._playwrightConnection = null; - this._closeExtensionConnection('Playwright client disconnected'); - debugLogger('Playwright WebSocket closed'); - }); - ws.on('error', error => { - debugLogger('Playwright WebSocket error:', error); - }); - debugLogger('Playwright MCP connected'); - } - - private _closeExtensionConnection(reason: string) { - this._extensionConnection?.close(reason); - this._extensionConnectionPromise.reject(new Error(reason)); - this._resetExtensionConnection(); - } - - private _resetExtensionConnection() { - this._connectedTabInfo = undefined; - this._extensionConnection = null; - this._extensionConnectionPromise = new ManualPromise(); - void this._extensionConnectionPromise.catch(logUnhandledError); - } - - private _closePlaywrightConnection(reason: string) { - if (this._playwrightConnection?.readyState === WebSocket.OPEN) - this._playwrightConnection.close(1000, reason); - this._playwrightConnection = null; - } - - private _handleExtensionConnection(ws: WebSocket): void { - if (this._extensionConnection) { - ws.close(1000, 'Another extension connection already established'); - return; - } - this._extensionConnection = new ExtensionConnection(ws); - this._extensionConnection.onclose = (c, reason) => { - debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection); - if (this._extensionConnection !== c) - return; - this._resetExtensionConnection(); - this._closePlaywrightConnection(`Extension disconnected: ${reason}`); - }; - this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this); - this._extensionConnectionPromise.resolve(); - } - - private _handleExtensionMessage(method: M, params: ExtensionEvents[M]['params']) { - switch (method) { - case 'forwardCDPEvent': - const sessionId = params.sessionId || this._connectedTabInfo?.sessionId; - this._sendToPlaywright({ - sessionId, - method: params.method, - params: params.params - }); - break; - } - } - - private async _handlePlaywrightMessage(message: CDPCommand): Promise { - debugLogger('← Playwright:', `${message.method} (id=${message.id})`); - const { id, sessionId, method, params } = message; - try { - const result = await this._handleCDPCommand(method, params, sessionId); - this._sendToPlaywright({ id, sessionId, result }); - } catch (e) { - debugLogger('Error in the extension:', e); - this._sendToPlaywright({ - id, - sessionId, - error: { message: (e as Error).message } - }); - } - } - - private async _handleCDPCommand(method: string, params: any, sessionId: string | undefined): Promise { - switch (method) { - case 'Browser.getVersion': { - return { - protocolVersion: '1.3', - product: 'Chrome/Extension-Bridge', - userAgent: 'CDP-Bridge-Server/1.0.0', - }; - } - case 'Browser.setDownloadBehavior': { - return { }; - } - case 'Target.setAutoAttach': { - // Forward child session handling. - if (sessionId) - break; - // Simulate auto-attach behavior with real target info - const { targetInfo } = await this._extensionConnection!.send('attachToTab', { }); - this._connectedTabInfo = { - targetInfo, - sessionId: `pw-tab-${this._nextSessionId++}`, - }; - debugLogger('Simulating auto-attach'); - this._sendToPlaywright({ - method: 'Target.attachedToTarget', - params: { - sessionId: this._connectedTabInfo.sessionId, - targetInfo: { - ...this._connectedTabInfo.targetInfo, - attached: true, - }, - waitingForDebugger: false - } - }); - return { }; - } - case 'Target.getTargetInfo': { - return this._connectedTabInfo?.targetInfo; - } - } - return await this._forwardToExtension(method, params, sessionId); - } - - private async _forwardToExtension(method: string, params: any, sessionId: string | undefined): Promise { - if (!this._extensionConnection) - throw new Error('Extension not connected'); - // Top level sessionId is only passed between the relay and the client. - if (this._connectedTabInfo?.sessionId === sessionId) - sessionId = undefined; - return await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params }); - } - - private _sendToPlaywright(message: CDPResponse): void { - debugLogger('→ Playwright:', `${message.method ?? `response(id=${message.id})`}`); - this._playwrightConnection?.send(JSON.stringify(message)); - } -} - -type ExtensionResponse = { - id?: number; - method?: string; - params?: any; - result?: any; - error?: string; -}; - -class ExtensionConnection { - private readonly _ws: WebSocket; - private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error }>(); - private _lastId = 0; - - onmessage?: (method: M, params: ExtensionEvents[M]['params']) => void; - onclose?: (self: ExtensionConnection, reason: string) => void; - - constructor(ws: WebSocket) { - this._ws = ws; - this._ws.on('message', this._onMessage.bind(this)); - this._ws.on('close', this._onClose.bind(this)); - this._ws.on('error', this._onError.bind(this)); - } - - async send(method: M, params: ExtensionCommand[M]['params']): Promise { - if (this._ws.readyState !== WebSocket.OPEN) - throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`); - const id = ++this._lastId; - this._ws.send(JSON.stringify({ id, method, params })); - const error = new Error(`Protocol error: ${method}`); - return new Promise((resolve, reject) => { - this._callbacks.set(id, { resolve, reject, error }); - }); - } - - close(message: string) { - debugLogger('closing extension connection:', message); - if (this._ws.readyState === WebSocket.OPEN) - this._ws.close(1000, message); - } - - private _onMessage(event: websocket.RawData) { - const eventData = event.toString(); - let parsedJson; - try { - parsedJson = JSON.parse(eventData); - } catch (e: any) { - debugLogger(` Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`); - this._ws.close(); - return; - } - try { - this._handleParsedMessage(parsedJson); - } catch (e: any) { - debugLogger(` Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`); - this._ws.close(); - } - } - - private _handleParsedMessage(object: ExtensionResponse) { - if (object.id && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id)!; - this._callbacks.delete(object.id); - if (object.error) { - const error = callback.error; - error.message = object.error; - callback.reject(error); - } else { - callback.resolve(object.result); - } - } else if (object.id) { - debugLogger('← Extension: unexpected response', object); - } else { - this.onmessage?.(object.method! as keyof ExtensionEvents, object.params); - } - } - - private _onClose(event: websocket.CloseEvent) { - debugLogger(` code=${event.code} reason=${event.reason}`); - this._dispose(); - this.onclose?.(this, event.reason); - } - - private _onError(event: websocket.ErrorEvent) { - debugLogger(` message=${event.message} type=${event.type} target=${event.target}`); - this._dispose(); - } - - private _dispose() { - for (const callback of this._callbacks.values()) - callback.reject(new Error('WebSocket closed')); - this._callbacks.clear(); - } -} diff --git a/src/extension/extensionContextFactory.ts b/src/extension/extensionContextFactory.ts deleted file mode 100644 index 68533f6..0000000 --- a/src/extension/extensionContextFactory.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; -import * as playwright from 'playwright'; -import { startHttpServer } from '../sdk/http'; -import { CDPRelayServer } from './cdpRelay'; - -import type { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; - -const debugLogger = debug('pw:mcp:relay'); - -export class ExtensionContextFactory implements BrowserContextFactory { - private _browserChannel: string; - private _userDataDir?: string; - private _executablePath?: string; - - constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) { - this._browserChannel = browserChannel; - this._userDataDir = userDataDir; - this._executablePath = executablePath; - } - - async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { - const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName); - return { - browserContext: browser.contexts()[0], - close: async () => { - debugLogger('close() called for browser context'); - await browser.close(); - } - }; - } - - private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise { - const relay = await this._startRelay(abortSignal); - await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName); - return await playwright.chromium.connectOverCDP(relay.cdpEndpoint()); - } - - private async _startRelay(abortSignal: AbortSignal) { - const httpServer = await startHttpServer({}); - if (abortSignal.aborted) { - httpServer.close(); - throw new Error(abortSignal.reason); - } - const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath); - abortSignal.addEventListener('abort', () => cdpRelayServer.stop()); - debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`); - return cdpRelayServer; - } -} diff --git a/src/extension/protocol.ts b/src/extension/protocol.ts deleted file mode 100644 index 7b0cb26..0000000 --- a/src/extension/protocol.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Whenever the commands/events change, the version must be updated. The latest -// extension version should be compatible with the old MCP clients. -export const VERSION = 1; - -export type ExtensionCommand = { - 'attachToTab': { - params: {}; - }; - 'forwardCDPCommand': { - params: { - method: string, - sessionId?: string - params?: any, - }; - }; -}; - -export type ExtensionEvents = { - 'forwardCDPEvent': { - params: { - method: string, - sessionId?: string - params?: any, - }; - }; -}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 398e49e..0000000 --- a/src/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BrowserServerBackend } from './browser/browserServerBackend'; -import { resolveConfig } from './browser/config'; -import { contextFactory } from './browser/browserContextFactory'; -import * as mcpServer from './sdk/server'; -import { packageJSON } from './package'; - -import type { Config } from '../config'; -import type { BrowserContext } from 'playwright'; -import type { BrowserContextFactory } from './browser/browserContextFactory'; -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; - -export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise): Promise { - const config = await resolveConfig(userConfig); - const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config); - return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false); -} - -class SimpleBrowserContextFactory implements BrowserContextFactory { - name = 'custom'; - description = 'Connect to a browser using a custom context getter'; - - private readonly _contextGetter: () => Promise; - - constructor(contextGetter: () => Promise) { - this._contextGetter = contextGetter; - } - - async createContext(): Promise<{ browserContext: BrowserContext, close: () => Promise }> { - const browserContext = await this._contextGetter(); - return { - browserContext, - close: () => browserContext.close() - }; - } -} diff --git a/src/log.ts b/src/log.ts deleted file mode 100644 index 15d09dd..0000000 --- a/src/log.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; - -const errorsDebug = debug('pw:mcp:errors'); - -export function logUnhandledError(error: unknown) { - errorsDebug(error); -} - -export const testDebug = debug('pw:mcp:test'); diff --git a/src/package.ts b/src/package.ts deleted file mode 100644 index 9874319..0000000 --- a/src/package.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; - -export const packageJSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); diff --git a/src/program.ts b/src/program.ts deleted file mode 100644 index 35a74d8..0000000 --- a/src/program.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { program, Option } from 'commander'; -import * as mcpServer from './sdk/server'; -import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; -import { packageJSON } from './package'; -import { Context } from './browser/context'; -import { contextFactory } from './browser/browserContextFactory'; -import { ProxyBackend } from './sdk/proxyBackend'; -import { BrowserServerBackend } from './browser/browserServerBackend'; -import { ExtensionContextFactory } from './extension/extensionContextFactory'; - -import { runVSCodeTools } from './vscode/host'; -import type { MCPProvider } from './sdk/proxyBackend'; - -program - .version('Version ' + packageJSON.version) - .name(packageJSON.name) - .option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) - .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) - .option('--block-service-workers', 'block service workers') - .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') - .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) - .option('--cdp-endpoint ', 'CDP endpoint to connect to.') - .option('--config ', 'path to the configuration file.') - .option('--device ', 'device to emulate, for example: "iPhone 15"') - .option('--executable-path ', 'path to the browser executable.') - .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.') - .option('--headless', 'run browser in headless mode, headed by default') - .option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') - .option('--ignore-https-errors', 'ignore https errors') - .option('--isolated', 'keep the browser profile in memory, do not save it to disk.') - .option('--image-responses ', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".') - .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.') - .option('--output-dir ', 'path to the directory for output files.') - .option('--port ', 'port to listen on for SSE transport.') - .option('--proxy-bypass ', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"') - .option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') - .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') - .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') - .option('--storage-state ', 'path to the storage state file for isolated sessions.') - .option('--user-agent ', 'specify user agent string') - .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') - .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') - .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp()) - .addOption(new Option('--vscode', 'VS Code tools.').hideHelp()) - .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp()) - .action(async options => { - setupExitWatchdog(); - - if (options.vision) { - // eslint-disable-next-line no-console - console.error('The --vision option is deprecated, use --caps=vision instead'); - options.caps = 'vision'; - } - - const config = await resolveCLIConfig(options); - const browserContextFactory = contextFactory(config); - const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath); - - if (options.extension) { - const serverBackendFactory: mcpServer.ServerBackendFactory = { - name: 'Playwright w/ extension', - nameInConfig: 'playwright-extension', - version: packageJSON.version, - create: () => new BrowserServerBackend(config, extensionContextFactory) - }; - await mcpServer.start(serverBackendFactory, config.server); - return; - } - - if (options.vscode) { - await runVSCodeTools(config); - return; - } - - if (options.connectTool) { - const providers: MCPProvider[] = [ - { - name: 'default', - description: 'Starts standalone browser', - connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)), - }, - { - name: 'extension', - description: 'Connect to a browser using the Playwright MCP extension', - connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)), - }, - ]; - const factory: mcpServer.ServerBackendFactory = { - name: 'Playwright w/ switch', - nameInConfig: 'playwright-switch', - version: packageJSON.version, - create: () => new ProxyBackend(providers), - }; - await mcpServer.start(factory, config.server); - return; - } - - const factory: mcpServer.ServerBackendFactory = { - name: 'Playwright', - nameInConfig: 'playwright', - version: packageJSON.version, - create: () => new BrowserServerBackend(config, browserContextFactory) - }; - await mcpServer.start(factory, config.server); - }); - -function setupExitWatchdog() { - let isExiting = false; - const handleExit = async () => { - if (isExiting) - return; - isExiting = true; - setTimeout(() => process.exit(0), 15000); - await Context.disposeAll(); - process.exit(0); - }; - - process.stdin.on('close', handleExit); - process.on('SIGINT', handleExit); - process.on('SIGTERM', handleExit); -} - -void program.parseAsync(process.argv); diff --git a/src/sdk/DEPS.list b/src/sdk/DEPS.list deleted file mode 100644 index e43dcb5..0000000 --- a/src/sdk/DEPS.list +++ /dev/null @@ -1 +0,0 @@ -[*] diff --git a/src/sdk/README.md b/src/sdk/README.md deleted file mode 100644 index b8b280e..0000000 --- a/src/sdk/README.md +++ /dev/null @@ -1 +0,0 @@ -- Generic MCP utils, no dependencies on anything. diff --git a/src/sdk/bundle.ts b/src/sdk/bundle.ts deleted file mode 100644 index 4def9bf..0000000 --- a/src/sdk/bundle.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// @ts-ignore -import * as bundle from 'playwright/lib/mcpBundleImpl'; - -const zodToJsonSchema: typeof import('zod-to-json-schema').zodToJsonSchema = bundle.zodToJsonSchema; -const Client: typeof import('@modelcontextprotocol/sdk/client/index.js').Client = bundle.Client; -const Server: typeof import('@modelcontextprotocol/sdk/server/index.js').Server = bundle.Server; -const SSEServerTransport: typeof import('@modelcontextprotocol/sdk/server/sse.js').SSEServerTransport = bundle.SSEServerTransport; -const StdioClientTransport: typeof import('@modelcontextprotocol/sdk/client/stdio.js').StdioClientTransport = bundle.StdioClientTransport; -const StdioServerTransport: typeof import('@modelcontextprotocol/sdk/server/stdio.js').StdioServerTransport = bundle.StdioServerTransport; -const StreamableHTTPServerTransport: typeof import('@modelcontextprotocol/sdk/server/streamableHttp.js').StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport; -const StreamableHTTPClientTransport: typeof import('@modelcontextprotocol/sdk/client/streamableHttp.js').StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport; -const CallToolRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').CallToolRequestSchema = bundle.CallToolRequestSchema; -const ListRootsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListRootsRequestSchema = bundle.ListRootsRequestSchema; -const ListToolsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema = bundle.ListToolsRequestSchema; -const PingRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').PingRequestSchema = bundle.PingRequestSchema; -const z: typeof import('zod') = bundle.z; - -export { - zodToJsonSchema, - Client, - Server, - SSEServerTransport, - StdioClientTransport, - StdioServerTransport, - StreamableHTTPClientTransport, - StreamableHTTPServerTransport, - CallToolRequestSchema, - ListRootsRequestSchema, - ListToolsRequestSchema, - PingRequestSchema, - z, -}; diff --git a/src/sdk/http.ts b/src/sdk/http.ts deleted file mode 100644 index 51101a7..0000000 --- a/src/sdk/http.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import assert from 'assert'; -import net from 'net'; -import http from 'http'; -import crypto from 'crypto'; - -import debug from 'debug'; - -import * as mcpBundle from './bundle'; -import * as mcpServer from './server'; - -import type { ServerBackendFactory } from './server'; -import type { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; -import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; - -const testDebug = debug('pw:mcp:test'); - -export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise { - const { host, port } = config; - const httpServer = http.createServer(); - decorateServer(httpServer); - await new Promise((resolve, reject) => { - httpServer.on('error', reject); - abortSignal?.addEventListener('abort', () => { - httpServer.close(); - reject(new Error('Aborted')); - }); - httpServer.listen(port, host, () => { - resolve(); - httpServer.removeListener('error', reject); - }); - }); - return httpServer; -} - -export function httpAddressToString(address: string | net.AddressInfo | null): string { - assert(address, 'Could not bind server socket'); - if (typeof address === 'string') - return address; - const resolvedPort = address.port; - let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; - if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]') - resolvedHost = 'localhost'; - return `http://${resolvedHost}:${resolvedPort}`; -} - -export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) { - const sseSessions = new Map(); - const streamableSessions = new Map(); - httpServer.on('request', async (req, res) => { - const url = new URL(`http://localhost${req.url}`); - if (url.pathname.startsWith('/sse')) - await handleSSE(serverBackendFactory, req, res, url, sseSessions); - else - await handleStreamable(serverBackendFactory, req, res, streamableSessions); - }); -} - -async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map) { - if (req.method === 'POST') { - const sessionId = url.searchParams.get('sessionId'); - if (!sessionId) { - res.statusCode = 400; - return res.end('Missing sessionId'); - } - - const transport = sessions.get(sessionId); - if (!transport) { - res.statusCode = 404; - return res.end('Session not found'); - } - - return await transport.handlePostMessage(req, res); - } else if (req.method === 'GET') { - const transport = new mcpBundle.SSEServerTransport('/sse', res); - sessions.set(transport.sessionId, transport); - testDebug(`create SSE session: ${transport.sessionId}`); - await mcpServer.connect(serverBackendFactory, transport, false); - res.on('close', () => { - testDebug(`delete SSE session: ${transport.sessionId}`); - sessions.delete(transport.sessionId); - }); - return; - } - - res.statusCode = 405; - res.end('Method not allowed'); -} - -async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, sessions: Map) { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (sessionId) { - const transport = sessions.get(sessionId); - if (!transport) { - res.statusCode = 404; - res.end('Session not found'); - return; - } - return await transport.handleRequest(req, res); - } - - if (req.method === 'POST') { - const transport = new mcpBundle.StreamableHTTPServerTransport({ - sessionIdGenerator: () => crypto.randomUUID(), - onsessioninitialized: async sessionId => { - testDebug(`create http session: ${transport.sessionId}`); - await mcpServer.connect(serverBackendFactory, transport, true); - sessions.set(sessionId, transport); - } - }); - - transport.onclose = () => { - if (!transport.sessionId) - return; - sessions.delete(transport.sessionId); - testDebug(`delete http session: ${transport.sessionId}`); - }; - - await transport.handleRequest(req, res); - return; - } - - res.statusCode = 400; - res.end('Invalid request'); -} - -function decorateServer(server: net.Server) { - const sockets = new Set(); - server.on('connection', socket => { - sockets.add(socket); - socket.once('close', () => sockets.delete(socket)); - }); - - const close = server.close; - server.close = (callback?: (err?: Error) => void) => { - for (const socket of sockets) - socket.destroy(); - sockets.clear(); - return close.call(server, callback); - }; -} diff --git a/src/sdk/inProcessTransport.ts b/src/sdk/inProcessTransport.ts deleted file mode 100644 index 317cff1..0000000 --- a/src/sdk/inProcessTransport.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js'; - -export class InProcessTransport implements Transport { - private _server: Server; - private _serverTransport: InProcessServerTransport; - private _connected: boolean = false; - - constructor(server: Server) { - this._server = server; - this._serverTransport = new InProcessServerTransport(this); - } - - async start(): Promise { - if (this._connected) - throw new Error('InprocessTransport already started!'); - - await this._server.connect(this._serverTransport); - this._connected = true; - } - - async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { - if (!this._connected) - throw new Error('Transport not connected'); - - - this._serverTransport._receiveFromClient(message); - } - - async close(): Promise { - if (this._connected) { - this._connected = false; - this.onclose?.(); - this._serverTransport.onclose?.(); - } - } - - onclose?: (() => void) | undefined; - onerror?: ((error: Error) => void) | undefined; - onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined; - sessionId?: string | undefined; - setProtocolVersion?: ((version: string) => void) | undefined; - - _receiveFromServer(message: JSONRPCMessage, extra?: MessageExtraInfo): void { - this.onmessage?.(message, extra); - } -} - -class InProcessServerTransport implements Transport { - private _clientTransport: InProcessTransport; - - constructor(clientTransport: InProcessTransport) { - this._clientTransport = clientTransport; - } - - async start(): Promise { - } - - async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { - this._clientTransport._receiveFromServer(message); - } - - async close(): Promise { - this.onclose?.(); - } - - onclose?: (() => void) | undefined; - onerror?: ((error: Error) => void) | undefined; - onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined; - sessionId?: string | undefined; - setProtocolVersion?: ((version: string) => void) | undefined; - _receiveFromClient(message: JSONRPCMessage): void { - this.onmessage?.(message); - } -} diff --git a/src/sdk/manualPromise.ts b/src/sdk/manualPromise.ts deleted file mode 100644 index a5034e0..0000000 --- a/src/sdk/manualPromise.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class ManualPromise extends Promise { - private _resolve!: (t: T) => void; - private _reject!: (e: Error) => void; - private _isDone: boolean; - - constructor() { - let resolve: (t: T) => void; - let reject: (e: Error) => void; - super((f, r) => { - resolve = f; - reject = r; - }); - this._isDone = false; - this._resolve = resolve!; - this._reject = reject!; - } - - isDone() { - return this._isDone; - } - - resolve(t: T) { - this._isDone = true; - this._resolve(t); - } - - reject(e: Error) { - this._isDone = true; - this._reject(e); - } - - static override get [Symbol.species]() { - return Promise; - } - - override get [Symbol.toStringTag]() { - return 'ManualPromise'; - } -} - -export class LongStandingScope { - private _terminateError: Error | undefined; - private _closeError: Error | undefined; - private _terminatePromises = new Map, string[]>(); - private _isClosed = false; - - reject(error: Error) { - this._isClosed = true; - this._terminateError = error; - for (const p of this._terminatePromises.keys()) - p.resolve(error); - } - - close(error: Error) { - this._isClosed = true; - this._closeError = error; - for (const [p, frames] of this._terminatePromises) - p.resolve(cloneError(error, frames)); - } - - isClosed() { - return this._isClosed; - } - - static async raceMultiple(scopes: LongStandingScope[], promise: Promise): Promise { - return Promise.race(scopes.map(s => s.race(promise))); - } - - async race(promise: Promise | Promise[]): Promise { - return this._race(Array.isArray(promise) ? promise : [promise], false) as Promise; - } - - async safeRace(promise: Promise, defaultValue?: T): Promise { - return this._race([promise], true, defaultValue); - } - - private async _race(promises: Promise[], safe: boolean, defaultValue?: any): Promise { - const terminatePromise = new ManualPromise(); - const frames = captureRawStack(); - if (this._terminateError) - terminatePromise.resolve(this._terminateError); - if (this._closeError) - terminatePromise.resolve(cloneError(this._closeError, frames)); - this._terminatePromises.set(terminatePromise, frames); - try { - return await Promise.race([ - terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)), - ...promises - ]); - } finally { - this._terminatePromises.delete(terminatePromise); - } - } -} - -function cloneError(error: Error, frames: string[]) { - const clone = new Error(); - clone.name = error.name; - clone.message = error.message; - clone.stack = [error.name + ':' + error.message, ...frames].join('\n'); - return clone; -} - -function captureRawStack(): string[] { - const stackTraceLimit = Error.stackTraceLimit; - Error.stackTraceLimit = 50; - const error = new Error(); - const stack = error.stack || ''; - Error.stackTraceLimit = stackTraceLimit; - return stack.split('\n'); -} diff --git a/src/sdk/mdb.ts b/src/sdk/mdb.ts deleted file mode 100644 index bdeca18..0000000 --- a/src/sdk/mdb.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; - -import { defineToolSchema } from './tool'; -import * as mcpBundle from './bundle'; -import * as mcpServer from './server'; -import * as mcpHttp from './http'; -import { wrapInProcess } from './server'; -import { ManualPromise } from './manualPromise'; - -import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -const mdbDebug = debug('pw:mcp:mdb'); -const errorsDebug = debug('pw:mcp:errors'); -const z = mcpBundle.z; - -export class MDBBackend implements mcpServer.ServerBackend { - private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise | undefined }[] = []; - private _interruptPromise: ManualPromise | undefined; - private _topLevelBackend: mcpServer.ServerBackend; - private _initialized = false; - - constructor(topLevelBackend: mcpServer.ServerBackend) { - this._topLevelBackend = topLevelBackend; - } - - async initialize(server: mcpServer.Server): Promise { - if (this._initialized) - return; - this._initialized = true; - const transport = await wrapInProcess(this._topLevelBackend); - await this._pushClient(transport); - } - - async listTools(): Promise { - const response = await this._client().listTools(); - return response.tools; - } - - async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { - if (name === pushToolsSchema.name) - return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {})); - - const interruptPromise = new ManualPromise(); - this._interruptPromise = interruptPromise; - let [entry] = this._stack; - - // Pop the client while the tool is not found. - while (entry && !entry.toolNames.includes(name)) { - mdbDebug('popping client from stack for ', name); - this._stack.shift(); - await entry.client.close(); - entry = this._stack[0]; - } - if (!entry) - throw new Error(`Tool ${name} not found in the tool stack`); - - const resultPromise = new ManualPromise(); - entry.resultPromise = resultPromise; - - this._client().callTool({ - name, - arguments: args, - }).then(result => { - resultPromise.resolve(result as mcpServer.CallToolResult); - }).catch(e => { - mdbDebug('error in client call', e); - if (this._stack.length < 2) - throw e; - this._stack.shift(); - const prevEntry = this._stack[0]; - void prevEntry.resultPromise!.then(result => resultPromise.resolve(result)); - }); - const result = await Promise.race([interruptPromise, resultPromise]); - if (interruptPromise.isDone()) - mdbDebug('client call intercepted', result); - else - mdbDebug('client call result', result); - return result; - } - - private _client(): Client { - const [entry] = this._stack; - if (!entry) - throw new Error('No debugging backend available'); - return entry.client; - } - - private async _pushTools(params: { mcpUrl: string, introMessage?: string }): Promise { - mdbDebug('pushing tools to the stack', params.mcpUrl); - const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(params.mcpUrl)); - await this._pushClient(transport, params.introMessage); - return { content: [{ type: 'text', text: 'Tools pushed' }] }; - } - - private async _pushClient(transport: Transport, introMessage?: string): Promise { - mdbDebug('pushing client to the stack'); - const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); - client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); - await client.connect(transport); - mdbDebug('connected to the new client'); - const { tools } = await client.listTools(); - this._stack.unshift({ client, toolNames: tools.map(tool => tool.name), resultPromise: undefined }); - mdbDebug('new tools added to the stack:', tools.map(tool => tool.name)); - mdbDebug('interrupting current call:', !!this._interruptPromise); - this._interruptPromise?.resolve({ - content: [{ - type: 'text', - text: introMessage || '', - }], - }); - this._interruptPromise = undefined; - return { content: [{ type: 'text', text: 'Tools pushed' }] }; - } -} - -const pushToolsSchema = defineToolSchema({ - name: 'mdb_push_tools', - title: 'Push MCP tools to the tools stack', - description: 'Push MCP tools to the tools stack', - inputSchema: z.object({ - mcpUrl: z.string(), - introMessage: z.string().optional(), - }), - type: 'readOnly', -}); - -export type ServerBackendOnPause = mcpServer.ServerBackend & { - requestSelfDestruct?: () => void; -}; - -export async function runMainBackend(backendFactory: mcpServer.ServerBackendFactory, options?: { port?: number }): Promise { - const mdbBackend = new MDBBackend(backendFactory.create()); - // Start HTTP unconditionally. - const factory: mcpServer.ServerBackendFactory = { - ...backendFactory, - create: () => mdbBackend - }; - const url = await startAsHttp(factory, { port: options?.port || 0 }); - process.env.PLAYWRIGHT_DEBUGGER_MCP = url; - - if (options?.port !== undefined) - return url; - - // Start stdio conditionally. - await mcpServer.connect(factory, new mcpBundle.StdioServerTransport(), false); -} - -export async function runOnPauseBackendLoop(mdbUrl: string, backend: ServerBackendOnPause, introMessage: string) { - const wrappedBackend = new OnceTimeServerBackendWrapper(backend); - - const factory = { - name: 'on-pause-backend', - nameInConfig: 'on-pause-backend', - version: '0.0.0', - create: () => wrappedBackend, - }; - - const httpServer = await mcpHttp.startHttpServer({ port: 0 }); - await mcpHttp.installHttpTransport(httpServer, factory); - const url = mcpHttp.httpAddressToString(httpServer.address()); - - const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); - client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); - const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(mdbUrl)); - await client.connect(transport); - - const pushToolsResult = await client.callTool({ - name: pushToolsSchema.name, - arguments: { - mcpUrl: url, - introMessage, - }, - }); - if (pushToolsResult.isError) - errorsDebug('Failed to push tools', pushToolsResult.content); - await transport.terminateSession(); - await client.close(); - - await wrappedBackend.waitForClosed(); - httpServer.close(); -} - -async function startAsHttp(backendFactory: mcpServer.ServerBackendFactory, options: { port: number }) { - const httpServer = await mcpHttp.startHttpServer(options); - await mcpHttp.installHttpTransport(httpServer, backendFactory); - return mcpHttp.httpAddressToString(httpServer.address()); -} - - -class OnceTimeServerBackendWrapper implements mcpServer.ServerBackend { - private _backend: ServerBackendOnPause; - private _selfDestructPromise = new ManualPromise(); - - constructor(backend: ServerBackendOnPause) { - this._backend = backend; - this._backend.requestSelfDestruct = () => this._selfDestructPromise.resolve(); - } - - async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { - await this._backend.initialize?.(server, clientVersion, roots); - } - - async listTools(): Promise { - return this._backend.listTools(); - } - - async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { - return this._backend.callTool(name, args); - } - - serverClosed(server: mcpServer.Server) { - this._backend.serverClosed?.(server); - this._selfDestructPromise.resolve(); - } - - async waitForClosed() { - await this._selfDestructPromise; - } -} diff --git a/src/sdk/proxyBackend.ts b/src/sdk/proxyBackend.ts deleted file mode 100644 index 96b8c7f..0000000 --- a/src/sdk/proxyBackend.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; - -import * as mcpBundle from './bundle'; - -import type { ServerBackend, ClientVersion, Root, Server } from './server'; -import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -export type MCPProvider = { - name: string; - description: string; - connect(): Promise; -}; - -const errorsDebug = debug('pw:mcp:errors'); -const { z, zodToJsonSchema } = mcpBundle; - -export class ProxyBackend implements ServerBackend { - private _mcpProviders: MCPProvider[]; - private _currentClient: Client | undefined; - private _contextSwitchTool: Tool; - private _roots: Root[] = []; - - constructor(mcpProviders: MCPProvider[]) { - this._mcpProviders = mcpProviders; - this._contextSwitchTool = this._defineContextSwitchTool(); - } - - async initialize(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise { - this._roots = roots; - await this._setCurrentClient(this._mcpProviders[0]); - } - - async listTools(): Promise { - const response = await this._currentClient!.listTools(); - if (this._mcpProviders.length === 1) - return response.tools; - return [ - ...response.tools, - this._contextSwitchTool, - ]; - } - - async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise { - if (name === this._contextSwitchTool.name) - return this._callContextSwitchTool(args); - return await this._currentClient!.callTool({ - name, - arguments: args, - }) as CallToolResult; - } - - serverClosed?(): void { - void this._currentClient?.close().catch(errorsDebug); - } - - private async _callContextSwitchTool(params: any): Promise { - try { - const factory = this._mcpProviders.find(factory => factory.name === params.name); - if (!factory) - throw new Error('Unknown connection method: ' + params.name); - - await this._setCurrentClient(factory); - return { - content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }], - }; - } catch (error) { - return { - content: [{ type: 'text', text: `### Result\nError: ${error}\n` }], - isError: true, - }; - } - } - - private _defineContextSwitchTool(): Tool { - return { - name: 'browser_connect', - description: [ - 'Connect to a browser using one of the available methods:', - ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`), - ].join('\n'), - inputSchema: zodToJsonSchema(z.object({ - name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'), - }), { strictUnions: true }) as Tool['inputSchema'], - annotations: { - title: 'Connect to a browser context', - readOnlyHint: true, - openWorldHint: false, - }, - }; - } - - private async _setCurrentClient(factory: MCPProvider) { - await this._currentClient?.close(); - this._currentClient = undefined; - - const client = new mcpBundle.Client({ name: 'Playwright MCP Proxy', version: '0.0.0' }); - client.registerCapabilities({ - roots: { - listRoots: true, - }, - }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots })); - client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); - - const transport = await factory.connect(); - await client.connect(transport); - this._currentClient = client; - } -} diff --git a/src/sdk/server.ts b/src/sdk/server.ts deleted file mode 100644 index 4519092..0000000 --- a/src/sdk/server.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import debug from 'debug'; - -import * as mcpBundle from './bundle'; -import { httpAddressToString, installHttpTransport, startHttpServer } from './http'; -import { InProcessTransport } from './inProcessTransport'; - -import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; -import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; - -const serverDebug = debug('pw:mcp:server'); -const errorsDebug = debug('pw:mcp:errors'); - -export type ClientVersion = { name: string, version: string }; - -export interface ServerBackend { - initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise; - listTools(): Promise; - callTool(name: string, args: CallToolRequest['params']['arguments']): Promise; - serverClosed?(server: Server): void; -} - -export type ServerBackendFactory = { - name: string; - nameInConfig: string; - version: string; - create: () => ServerBackend; -}; - -export async function connect(factory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) { - const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat); - await server.connect(transport); -} - -export async function wrapInProcess(backend: ServerBackend): Promise { - const server = createServer('Internal', '0.0.0', backend, false); - return new InProcessTransport(server); -} - -export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server { - let initializedPromiseResolve = () => {}; - const initializedPromise = new Promise(resolve => initializedPromiseResolve = resolve); - const server = new mcpBundle.Server({ name, version }, { - capabilities: { - tools: {}, - } - }); - - server.setRequestHandler(mcpBundle.ListToolsRequestSchema, async () => { - serverDebug('listTools'); - await initializedPromise; - const tools = await backend.listTools(); - return { tools }; - }); - - let heartbeatRunning = false; - server.setRequestHandler(mcpBundle.CallToolRequestSchema, async request => { - serverDebug('callTool', request); - await initializedPromise; - - if (runHeartbeat && !heartbeatRunning) { - heartbeatRunning = true; - startHeartbeat(server); - } - - try { - return await backend.callTool(request.params.name, request.params.arguments || {}); - } catch (error) { - return { - content: [{ type: 'text', text: '### Result\n' + String(error) }], - isError: true, - }; - } - }); - addServerListener(server, 'initialized', async () => { - try { - const capabilities = server.getClientCapabilities(); - let clientRoots: Root[] = []; - if (capabilities?.roots) { - const { roots } = await server.listRoots(undefined, { timeout: 2_000 }).catch(() => ({ roots: [] })); - clientRoots = roots; - } - const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' }; - await backend.initialize?.(server, clientVersion, clientRoots); - initializedPromiseResolve(); - } catch (e) { - errorsDebug(e); - } - }); - addServerListener(server, 'close', () => backend.serverClosed?.(server)); - return server; -} - -const startHeartbeat = (server: Server) => { - const beat = () => { - Promise.race([ - server.ping(), - new Promise((_, reject) => setTimeout(() => reject(new Error('ping timeout')), 5000)), - ]).then(() => { - setTimeout(beat, 3000); - }).catch(() => { - void server.close(); - }); - }; - - beat(); -}; - -function addServerListener(server: Server, event: 'close' | 'initialized', listener: () => void) { - const oldListener = server[`on${event}`]; - server[`on${event}`] = () => { - oldListener?.(); - listener(); - }; -} - -export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { - if (options.port === undefined) { - await connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false); - return; - } - - const httpServer = await startHttpServer(options); - await installHttpTransport(httpServer, serverBackendFactory); - const url = httpAddressToString(httpServer.address()); - - const mcpConfig: any = { mcpServers: { } }; - mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = { - url: `${url}/mcp` - }; - const message = [ - `Listening on ${url}`, - 'Put this in your client config:', - JSON.stringify(mcpConfig, undefined, 2), - 'For legacy SSE transport support, you can use the /sse endpoint instead.', - ].join('\n'); - // eslint-disable-next-line no-console - console.error(message); -} diff --git a/src/sdk/tool.ts b/src/sdk/tool.ts deleted file mode 100644 index bd94547..0000000 --- a/src/sdk/tool.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { zodToJsonSchema } from '../sdk/bundle'; - -import type { z } from 'zod'; -import type * as mcpServer from './server'; - -export type ToolSchema = { - name: string; - title: string; - description: string; - inputSchema: Input; - type: 'readOnly' | 'destructive'; -}; - -export function toMcpTool(tool: ToolSchema): mcpServer.Tool { - return { - name: tool.name, - description: tool.description, - inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], - annotations: { - title: tool.title, - readOnlyHint: tool.type === 'readOnly', - destructiveHint: tool.type === 'destructive', - openWorldHint: true, - }, - }; -} - -export function defineToolSchema(tool: ToolSchema): ToolSchema { - return tool; -} diff --git a/src/vscode/DEPS.list b/src/vscode/DEPS.list deleted file mode 100644 index b75148d..0000000 --- a/src/vscode/DEPS.list +++ /dev/null @@ -1,7 +0,0 @@ -[*] -../sdk/ -../browser/config.ts -../browser/browserServerBackend.ts -../browser/browserContextFactory.ts -../log.ts -../package.ts diff --git a/src/vscode/host.ts b/src/vscode/host.ts deleted file mode 100644 index 4631f59..0000000 --- a/src/vscode/host.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from 'path'; - -import * as mcpBundle from '../sdk/bundle'; -import * as mcpServer from '../sdk/server'; -import { logUnhandledError } from '../log'; -import { packageJSON } from '../package'; - -import { FullConfig } from '../browser/config'; -import { BrowserServerBackend } from '../browser/browserServerBackend'; -import { contextFactory } from '../browser/browserContextFactory'; - -import type { z as zod } from 'zod'; -import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { ClientVersion, ServerBackend } from '../sdk/server'; -import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; -import type { Browser, BrowserContext, BrowserServer } from 'playwright'; -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -const { z, zodToJsonSchema } = mcpBundle; - -const contextSwitchOptions = z.object({ - connectionString: z.string().optional().describe('The connection string to use to connect to the browser'), - lib: z.string().optional().describe('The library to use for the connection'), - debugController: z.boolean().optional().describe('Enable the debug controller') -}); - -class VSCodeProxyBackend implements ServerBackend { - name = 'Playwright MCP Client Switcher'; - version = packageJSON.version; - - private _currentClient: Client | undefined; - private _contextSwitchTool: Tool; - private _roots: Root[] = []; - private _clientVersion?: ClientVersion; - private _context?: BrowserContext; - private _browser?: Browser; - private _browserServer?: BrowserServer; - - constructor(private readonly _config: FullConfig, private readonly _defaultTransportFactory: (delegate: VSCodeProxyBackend) => Promise) { - this._contextSwitchTool = this._defineContextSwitchTool(); - } - - async initialize(server: mcpServer.Server, clientVersion: ClientVersion, roots: Root[]): Promise { - this._clientVersion = clientVersion; - this._roots = roots; - const transport = await this._defaultTransportFactory(this); - await this._setCurrentClient(transport); - } - - async listTools(): Promise { - const response = await this._currentClient!.listTools(); - return [ - ...response.tools, - this._contextSwitchTool, - ]; - } - - async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise { - if (name === this._contextSwitchTool.name) - return this._callContextSwitchTool(args as any); - return await this._currentClient!.callTool({ - name, - arguments: args, - }) as CallToolResult; - } - - serverClosed?(server: mcpServer.Server): void { - void this._currentClient?.close().catch(logUnhandledError); - } - - onContext(context: BrowserContext) { - this._context = context; - context.on('close', () => { - this._context = undefined; - }); - } - - private async _getDebugControllerURL() { - if (!this._context) - return; - - const browser = this._context.browser() as any; - if (!browser || !browser._launchServer) - return; - - if (this._browser !== browser) - this._browserServer = undefined; - - if (!this._browserServer) - this._browserServer = await browser._launchServer({ _debugController: true }) as BrowserServer; - - const url = new URL(this._browserServer.wsEndpoint()); - url.searchParams.set('debug-controller', '1'); - return url.toString(); - } - - private async _callContextSwitchTool(params: zod.infer): Promise { - if (params.debugController) { - const url = await this._getDebugControllerURL(); - const lines = [`### Result`]; - if (url) { - lines.push(`URL: ${url}`); - lines.push(`Version: ${packageJSON.dependencies.playwright}`); - } else { - lines.push(`No open browsers.`); - } - return { content: [{ type: 'text', text: lines.join('\n') }] }; - } - - if (!params.connectionString || !params.lib) { - const transport = await this._defaultTransportFactory(this); - await this._setCurrentClient(transport); - return { - content: [{ type: 'text', text: '### Result\nSuccessfully disconnected.\n' }], - }; - } - - await this._setCurrentClient( - new mcpBundle.StdioClientTransport({ - command: process.execPath, - cwd: process.cwd(), - args: [ - path.join(__dirname, 'main.js'), - JSON.stringify(this._config), - params.connectionString, - params.lib, - ], - }) - ); - return { - content: [{ type: 'text', text: '### Result\nSuccessfully connected.\n' }], - }; - } - - private _defineContextSwitchTool(): Tool { - return { - name: 'browser_connect', - description: 'Do not call, this tool is used in the integration with the Playwright VS Code Extension and meant for programmatic usage only.', - inputSchema: zodToJsonSchema(contextSwitchOptions, { strictUnions: true }) as Tool['inputSchema'], - annotations: { - title: 'Connect to a browser running in VS Code.', - readOnlyHint: true, - openWorldHint: false, - }, - }; - } - - private async _setCurrentClient(transport: Transport) { - await this._currentClient?.close(); - this._currentClient = undefined; - - const client = new mcpBundle.Client(this._clientVersion!); - client.registerCapabilities({ - roots: { - listRoots: true, - }, - }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots })); - client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); - - await client.connect(transport); - this._currentClient = client; - } -} - -export async function runVSCodeTools(config: FullConfig) { - const serverBackendFactory: mcpServer.ServerBackendFactory = { - name: 'Playwright w/ vscode', - nameInConfig: 'playwright-vscode', - version: packageJSON.version, - create: () => new VSCodeProxyBackend( - config, - delegate => mcpServer.wrapInProcess( - new BrowserServerBackend(config, - { - async createContext(clientInfo, abortSignal, toolName) { - const context = await contextFactory(config).createContext(clientInfo, abortSignal, toolName); - delegate.onContext(context.browserContext); - return context; - }, - } - ) - ) - ) - }; - await mcpServer.start(serverBackendFactory, config.server); - return; -} diff --git a/src/vscode/main.ts b/src/vscode/main.ts deleted file mode 100644 index 5eb4656..0000000 --- a/src/vscode/main.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as mcpBundle from '../sdk/bundle'; -import * as mcpServer from '../sdk/server'; -import { BrowserServerBackend } from '../browser/browserServerBackend'; -import { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; - -import type { FullConfig } from '../browser/config'; -import type { BrowserContext } from 'playwright-core'; - -class VSCodeBrowserContextFactory implements BrowserContextFactory { - name = 'vscode'; - description = 'Connect to a browser running in the Playwright VS Code extension'; - - constructor(private _config: FullConfig, private _playwright: typeof import('playwright'), private _connectionString: string) {} - - async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: BrowserContext; close: () => Promise; }> { - let launchOptions: any = this._config.browser.launchOptions; - if (this._config.browser.userDataDir) { - launchOptions = { - ...launchOptions, - ...this._config.browser.contextOptions, - userDataDir: this._config.browser.userDataDir, - }; - } - const connectionString = new URL(this._connectionString); - connectionString.searchParams.set('launch-options', JSON.stringify(launchOptions)); - - const browserType = this._playwright.chromium; // it could also be firefox or webkit, we just need some browser type to call `connect` on - const browser = await browserType.connect(connectionString.toString()); - - const context = browser.contexts()[0] ?? await browser.newContext(this._config.browser.contextOptions); - - return { - browserContext: context, - close: async () => { - await browser.close(); - } - }; - } -} - -async function main(config: FullConfig, connectionString: string, lib: string) { - const playwright = await import(lib).then(mod => mod.default ?? mod); - const factory = new VSCodeBrowserContextFactory(config, playwright, connectionString); - await mcpServer.connect( - { - name: 'Playwright MCP', - nameInConfig: 'playwright-vscode', - create: () => new BrowserServerBackend(config, factory), - version: 'unused' - }, - new mcpBundle.StdioServerTransport(), - false - ); -} - -void (async () => { - await main( - JSON.parse(process.argv[2]), - process.argv[3], - process.argv[4] - ); -})(); diff --git a/tests/cdp.spec.ts b/tests/cdp.spec.ts deleted file mode 100644 index 77702c4..0000000 --- a/tests/cdp.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from 'node:path'; -import { spawnSync } from 'node:child_process'; -import { test, expect } from './fixtures'; - -test('cdp server', async ({ cdpServer, startClient, server }) => { - await cdpServer.start(); - const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); -}); - -test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => { - const browserContext = await cdpServer.start(); - const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); - - const [page] = browserContext.pages(); - await page.goto(server.HELLO_WORLD); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Hello, world!', - ref: 'f0', - }, - })).toHaveResponse({ - result: `Error: No open pages available. Use the "browser_navigate" tool to navigate to a page first.`, - isError: true, - }); - - expect(await client.callTool({ - name: 'browser_snapshot', - })).toHaveResponse({ - pageState: expect.stringContaining(`- Page URL: ${server.HELLO_WORLD} -- Page Title: Title -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Hello, world! -\`\`\``), - }); -}); - -test('should throw connection error and allow re-connecting', async ({ cdpServer, startClient, server }) => { - const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); - - server.setContent('/', ` - Title - Hello, world! - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - result: expect.stringContaining(`Error: browserType.connectOverCDP: connect ECONNREFUSED`), - isError: true, - }); - await cdpServer.start(); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); -}); - -test('does not support --device', async () => { - const result = spawnSync('node', [ - path.join(__filename, '../../cli.js'), '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234', - ]); - expect(result.error).toBeUndefined(); - expect(result.status).toBe(1); - expect(result.stderr.toString()).toContain('Device emulation is not supported with cdpEndpoint.'); -}); diff --git a/tests/click.spec.ts b/tests/click.spec.ts index 53db231..862bbf1 100644 --- a/tests/click.spec.ts +++ b/tests/click.spec.ts @@ -38,62 +38,3 @@ test('browser_click', async ({ client, server, mcpBrowser }) => { pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`), }); }); - -test('browser_click (double)', async ({ client, server }) => { - server.setContent('/', ` - Title - -

Click me

- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Click me', - ref: 'e2', - doubleClick: true, - }, - })).toHaveResponse({ - code: `await page.getByRole('heading', { name: 'Click me' }).dblclick();`, - pageState: expect.stringContaining(`- heading "Double clicked" [level=1] [ref=e3]`), - }); -}); - -test('browser_click (right)', async ({ client, server }) => { - server.setContent('/', ` - - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - const result = await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Menu', - ref: 'e2', - button: 'right', - }, - }); - expect(result).toHaveResponse({ - code: `await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`, - pageState: expect.stringContaining(`- button "Right clicked"`), - }); -}); diff --git a/tests/config.spec.ts b/tests/config.spec.ts deleted file mode 100644 index cdd5d8b..0000000 --- a/tests/config.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'node:fs'; - -import { Config } from '../config'; -import { test, expect } from './fixtures'; -import { configFromCLIOptions } from '../lib/browser/config'; - -test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => { - server.setContent('/', ` - Title - Hello, world! - `, 'text/html'); - - const config: Config = { - browser: { - userDataDir: testInfo.outputPath('user-data-dir'), - }, - }; - const configPath = testInfo.outputPath('config.json'); - await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); - - const { client } = await startClient({ args: ['--config', configPath] }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Hello, world!`), - }); - - const files = await fs.promises.readdir(config.browser!.userDataDir!); - expect(files.length).toBeGreaterThan(0); -}); - -test.describe(() => { - test.use({ mcpBrowser: '' }); - test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => { - const config: Config = { - browser: { - browserName: 'firefox', - }, - }; - const configPath = testInfo.outputPath('config.json'); - await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); - - const { client } = await startClient({ args: ['--config', configPath] }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: 'data:text/html,' }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Firefox`), - }); - }); -}); - -test.describe('sandbox configuration', () => { - test('should enable sandbox by default (no --no-sandbox flag)', async () => { - const config = configFromCLIOptions({ sandbox: undefined }); - expect(config.browser?.launchOptions?.chromiumSandbox).toBeUndefined(); - }); - - test('should disable sandbox when --no-sandbox flag is passed', async () => { - const config = configFromCLIOptions({ sandbox: false }); - expect(config.browser?.launchOptions?.chromiumSandbox).toBe(false); - }); -}); diff --git a/tests/console.spec.ts b/tests/console.spec.ts deleted file mode 100644 index 835b453..0000000 --- a/tests/console.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_console_messages', async ({ client, server }) => { - server.setContent('/', ` - - - - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - - const resource = await client.callTool({ - name: 'browser_console_messages', - }); - expect(resource).toHaveResponse({ - result: `[LOG] Hello, world! @ ${server.PREFIX}:4 -[ERROR] Error @ ${server.PREFIX}:5`, - }); -}); - -test('browser_console_messages (page error)', async ({ client, server }) => { - server.setContent('/', ` - - - - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - - const resource = await client.callTool({ - name: 'browser_console_messages', - }); - expect(resource).toHaveResponse({ - result: expect.stringContaining(`Error: Error in script`), - }); - expect(resource).toHaveResponse({ - result: expect.stringContaining(server.PREFIX), - }); -}); - -test('recent console messages', async ({ client, server }) => { - server.setContent('/', ` - - - - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - - const response = await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Click me', - ref: 'e2', - }, - }); - - expect(response).toHaveResponse({ - consoleMessages: expect.stringContaining(`- [LOG] Hello, world! @`), - }); -}); diff --git a/tests/core.spec.ts b/tests/core.spec.ts index f5bd171..13ba4a0 100644 --- a/tests/core.spec.ts +++ b/tests/core.spec.ts @@ -30,161 +30,3 @@ test('browser_navigate', async ({ client, server }) => { \`\`\``, }); }); - -test('browser_select_option', async ({ client, server }) => { - server.setContent('/', ` - Title - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_select_option', - arguments: { - element: 'Select', - ref: 'e2', - values: ['bar'], - }, - })).toHaveResponse({ - code: `await page.getByRole('combobox').selectOption(['bar']);`, - pageState: `- Page URL: ${server.PREFIX} -- Page Title: Title -- Page Snapshot: -\`\`\`yaml -- combobox [ref=e2]: - - option "Foo" - - option "Bar" [selected] -\`\`\``, - }); -}); - -test('browser_select_option (multiple)', async ({ client, server }) => { - server.setContent('/', ` - Title - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_select_option', - arguments: { - element: 'Select', - ref: 'e2', - values: ['bar', 'baz'], - }, - })).toHaveResponse({ - code: `await page.getByRole('listbox').selectOption(['bar', 'baz']);`, - pageState: expect.stringContaining(` -- listbox [ref=e2]: - - option "Foo" [ref=e3] - - option "Bar" [selected] [ref=e4] - - option "Baz" [selected] [ref=e5]`), - }); -}); - -test('browser_resize', async ({ client, server }) => { - server.setContent('/', ` - Resize Test - -
Waiting for resize...
- - - `, 'text/html'); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - const response = await client.callTool({ - name: 'browser_resize', - arguments: { - width: 390, - height: 780, - }, - }); - expect(response).toHaveResponse({ - code: `await page.setViewportSize({ width: 390, height: 780 });`, - }); - await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({ - pageState: expect.stringContaining(`Window size: 390x780`), - }); -}); - -test('old locator error message', async ({ client, server }) => { - server.setContent('/', ` - - - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - })).toHaveResponse({ - pageState: expect.stringContaining(` - - button "Button 1" [ref=e2] - - button "Button 2" [ref=e3]`), - }); - - await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button 1', - ref: 'e2', - }, - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button 2', - ref: 'e3', - }, - })).toHaveResponse({ - result: expect.stringContaining(`Ref e3 not found in the current page snapshot. Try capturing new snapshot.`), - isError: true, - }); -}); - -test('visibility: hidden > visible should be shown', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/535' } }, async ({ client, server }) => { - server.setContent('/', ` -
-
- -
-
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_snapshot' - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button"`), - }); -}); diff --git a/tests/device.spec.ts b/tests/device.spec.ts deleted file mode 100644 index 986d321..0000000 --- a/tests/device.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('--device should work', async ({ startClient, server, mcpMode }) => { - const { client } = await startClient({ - args: ['--device', 'iPhone 15'], - }); - - server.route('/', (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` - - - - - - `); - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - })).toHaveResponse({ - pageState: expect.stringContaining(`393x659`), - }); -}); diff --git a/tests/dialogs.spec.ts b/tests/dialogs.spec.ts deleted file mode 100644 index 0b57714..0000000 --- a/tests/dialogs.spec.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('alert dialog', async ({ client, server }) => { - server.setContent('/', ``, 'text/html'); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button" [ref=e2]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - code: `await page.getByRole('button', { name: 'Button' }).click();`, - modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`, - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - code: undefined, - modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`, - }); - - expect(await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: true, - }, - })).toHaveResponse({ - modalState: undefined, - pageState: expect.stringContaining(`- button "Button"`), - }); -}); - -test('two alert dialogs', async ({ client, server }) => { - server.setContent('/', ` - Title - - - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button" [ref=e2]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - code: `await page.getByRole('button', { name: 'Button' }).click();`, - modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 1"]: can be handled by the "browser_handle_dialog" tool`), - }); - - const result = await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: true, - }, - }); - - expect(result).toHaveResponse({ - modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`), - }); - - const result2 = await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: true, - }, - }); - - expect(result2).not.toHaveResponse({ - modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`), - }); -}); - -test('confirm dialog (true)', async ({ client, server }) => { - server.setContent('/', ` - Title - - - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button" [ref=e2]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`), - }); - - expect(await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: true, - }, - })).toHaveResponse({ - modalState: undefined, - pageState: expect.stringContaining(`- generic [active] [ref=e1]: "true"`), - }); -}); - -test('confirm dialog (false)', async ({ client, server }) => { - server.setContent('/', ` - Title - - - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button" [ref=e2]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`), - }); - - expect(await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: false, - }, - })).toHaveResponse({ - modalState: undefined, - pageState: expect.stringContaining(`- generic [active] [ref=e1]: "false"`), - }); -}); - -test('prompt dialog', async ({ client, server }) => { - server.setContent('/', ` - Title - - - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button" [ref=e2]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - modalState: expect.stringContaining(`- ["prompt" dialog with message "Prompt"]: can be handled by the "browser_handle_dialog" tool`), - }); - - const result = await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: true, - promptText: 'Answer', - }, - }); - - expect(result).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Answer`), - }); -}); - -test('alert dialog w/ race', async ({ client, server }) => { - server.setContent('/', ``, 'text/html'); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- button "Button" [ref=e2]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e2', - }, - })).toHaveResponse({ - code: `await page.getByRole('button', { name: 'Button' }).click();`, - modalState: expect.stringContaining(`- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`), - }); - - const result = await client.callTool({ - name: 'browser_handle_dialog', - arguments: { - accept: true, - }, - }); - - expect(result).toHaveResponse({ - modalState: undefined, - pageState: expect.stringContaining(`- Page URL: ${server.PREFIX} -- Page Title: -- Page Snapshot: -\`\`\`yaml -- button "Button"`), - }); -}); diff --git a/tests/evaluate.spec.ts b/tests/evaluate.spec.ts deleted file mode 100644 index 68c8a39..0000000 --- a/tests/evaluate.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_evaluate', async ({ client, server }) => { - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- Page Title: Title`), - }); - - expect(await client.callTool({ - name: 'browser_evaluate', - arguments: { - function: '() => document.title', - }, - })).toHaveResponse({ - result: `"Title"`, - code: `await page.evaluate('() => document.title');`, - }); -}); - -test('browser_evaluate (element)', async ({ client, server }) => { - server.setContent('/', ` - Hello, world! - `, 'text/html'); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_evaluate', - arguments: { - function: 'element => element.style.backgroundColor', - element: 'body', - ref: 'e1', - }, - })).toHaveResponse({ - result: `"red"`, - code: `await page.getByText('Hello, world!').evaluate('element => element.style.backgroundColor');`, - }); -}); - -test('browser_evaluate object', async ({ client, server }) => { - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- Page Title: Title`), - }); - - expect(await client.callTool({ - name: 'browser_evaluate', - arguments: { - function: '() => ({ title: document.title, url: document.URL })', - }, - })).toHaveResponse({ - result: JSON.stringify({ title: 'Title', url: server.HELLO_WORLD }, null, 2), - code: `await page.evaluate('() => ({ title: document.title, url: document.URL })');`, - }); -}); - -test('browser_evaluate (error)', async ({ client, server }) => { - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- Page Title: Title`), - }); - - const result = await client.callTool({ - name: 'browser_evaluate', - arguments: { - function: '() => nonExistentVariable', - }, - }); - - expect(result.isError).toBe(true); - expect(result.content?.[0]?.text).toContain('nonExistentVariable'); - // Check for common error patterns across browsers - const errorText = result.content?.[0]?.text || ''; - expect(errorText).toMatch(/not defined|Can't find variable/); -}); diff --git a/tests/files.spec.ts b/tests/files.spec.ts deleted file mode 100644 index b3ac198..0000000 --- a/tests/files.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs/promises'; -import { test, expect } from './fixtures'; - -test('browser_file_upload', async ({ client, server }, testInfo) => { - server.setContent('/', ` - - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: - - button "Choose File" [ref=e2] - - button "Button" [ref=e3]`), - }); - - { - expect(await client.callTool({ - name: 'browser_file_upload', - arguments: { paths: [] }, - })).toHaveResponse({ - isError: true, - result: expect.stringContaining(`The tool "browser_file_upload" can only be used when there is related modal state present.`), - modalState: expect.stringContaining(`- There is no modal state present`), - }); - } - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Textbox', - ref: 'e2', - }, - })).toHaveResponse({ - modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`), - }); - - const filePath = testInfo.outputPath('test.txt'); - await fs.writeFile(filePath, 'Hello, world!'); - - { - const response = await client.callTool({ - name: 'browser_file_upload', - arguments: { - paths: [filePath], - }, - }); - - expect(response).toHaveResponse({ - code: expect.stringContaining(`await fileChooser.setFiles(`), - modalState: undefined, - }); - } - - { - const response = await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Textbox', - ref: 'e2', - }, - }); - - expect(response).toHaveResponse({ - modalState: `- [File chooser]: can be handled by the "browser_file_upload" tool`, - }); - } - - { - const response = await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button', - ref: 'e3', - }, - }); - - expect(response).toHaveResponse({ - result: `Error: Tool "browser_click" does not handle the modal state.`, - modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`), - }); - } -}); - -test('clicking on download link emits download', async ({ startClient, server, mcpMode }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - - server.setContent('/', `Download`, 'text/html'); - server.setContent('/download', 'Data', 'text/plain'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- link "Download" [ref=e2]`), - }); - await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Download link', - ref: 'e2', - }, - }); - await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({ - downloads: `- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`, - }); -}); - -test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - - test.skip(mcpBrowser !== 'chromium', 'This test is racy'); - server.route('/download', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/plain', - 'Content-Disposition': 'attachment; filename=test.txt', - }); - res.end('Hello world!'); - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX + 'download', - }, - })).toHaveResponse({ - downloads: expect.stringContaining(`- Downloaded file test.txt to`), - }); -}); diff --git a/tests/form.spec.ts b/tests/form.spec.ts deleted file mode 100644 index 339d79d..0000000 --- a/tests/form.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_fill_form (textbox)', async ({ client, server }) => { - server.setContent('/', ` - - - -
- - - - - -
- - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_fill_form', - arguments: { - fields: [ - { - name: 'Name textbox', - type: 'textbox', - ref: 'e4', - value: 'John Doe' - }, - { - name: 'Email textbox', - type: 'textbox', - ref: 'e6', - value: 'john.doe@example.com' - }, - { - name: 'Age textbox', - type: 'slider', - ref: 'e8', - value: '25' - }, - { - name: 'Country select', - type: 'combobox', - ref: 'e10', - value: 'United States' - }, - { - name: 'Subscribe checkbox', - type: 'checkbox', - ref: 'e12', - value: 'true' - }, - ] - }, - })).toHaveResponse({ - code: `await page.getByRole('textbox', { name: 'Name' }).fill('John Doe'); -await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com'); -await page.getByRole('slider', { name: 'Age' }).fill('25'); -await page.getByLabel('Choose a country United').selectOption('United States'); -await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`, - }); - - const response = await client.callTool({ - name: 'browser_snapshot', - arguments: { - }, - }); - expect.soft(response).toHaveResponse({ - pageState: expect.stringMatching(/textbox "Name".*John Doe/), - }); - expect.soft(response).toHaveResponse({ - pageState: expect.stringMatching(/textbox "Email".*john.doe@example.com/), - }); - expect.soft(response).toHaveResponse({ - pageState: expect.stringMatching(/slider "Age".*"25"/), - }); - expect.soft(response).toHaveResponse({ - pageState: expect.stringContaining('option \"United States\" [selected]'), - }); - expect.soft(response).toHaveResponse({ - pageState: expect.stringContaining('checkbox \"Subscribe to newsletter\" [checked]'), - }); -}); diff --git a/tests/headed.spec.ts b/tests/headed.spec.ts deleted file mode 100644 index af3be37..0000000 --- a/tests/headed.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -for (const mcpHeadless of [false, true]) { - test.describe(`mcpHeadless: ${mcpHeadless}`, () => { - test.use({ mcpHeadless }); - test.skip(process.platform === 'linux', 'Auto-detection wont let this test run on linux'); - test.skip(({ mcpMode, mcpHeadless }) => mcpMode === 'docker' && !mcpHeadless, 'Headed mode is not supported in docker'); - - test('browser', async ({ client, server, mcpBrowser }) => { - test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Only chrome is supported for this test'); - server.route('/', (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` - - - `); - }); - - const response = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - - expect(response).toHaveResponse({ - pageState: (mcpHeadless ? expect : expect.not).stringContaining(`HeadlessChrome`), - }); - }); - }); -} diff --git a/tests/http.spec.ts b/tests/http.spec.ts deleted file mode 100644 index 6558451..0000000 --- a/tests/http.spec.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { ChildProcess, spawn } from 'child_process'; -import path from 'path'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -import { test as baseTest, expect } from './fixtures'; -import type { Config } from '../config.d.ts'; - -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ - serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { - let cp: ChildProcess | undefined; - const userDataDir = testInfo.outputPath('user-data-dir'); - await use(async (options?: { args?: string[], noPort?: boolean }) => { - if (cp) - throw new Error('Process already running'); - - cp = spawn('node', [ - path.join(path.dirname(__filename), '../cli.js'), - ...(options?.noPort ? [] : ['--port=0']), - '--user-data-dir=' + userDataDir, - ...(mcpHeadless ? ['--headless'] : []), - ...(options?.args || []), - ], { - stdio: 'pipe', - env: { - ...process.env, - DEBUG: 'pw:mcp:test', - DEBUG_COLORS: '0', - DEBUG_HIDE_DATE: '1', - }, - }); - let stderr = ''; - const url = await new Promise(resolve => cp!.stderr?.on('data', data => { - stderr += data.toString(); - const match = stderr.match(/Listening on (http:\/\/.*)/); - if (match) - resolve(match[1]); - })); - - return { url: new URL(url), stderr: () => stderr }; - }); - cp?.kill('SIGTERM'); - }, -}); - -test('http transport', async ({ serverEndpoint }) => { - const { url } = await serverEndpoint(); - const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client = new Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); -}); - -test('http transport (config)', async ({ serverEndpoint }) => { - const config: Config = { - server: { - port: 0, - } - }; - const configFile = test.info().outputPath('config.json'); - await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2)); - - const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] }); - const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client = new Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); -}); - -test('http transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); - - const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - /** - * src/client/streamableHttp.ts - * Clients that no longer need a particular session - * (e.g., because the user is leaving the client application) SHOULD send an - * HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly - * terminate the session. - */ - await transport1.terminateSession(); - await client1.close(); - - const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await transport2.terminateSession(); - await client2.close(); - - await expect(async () => { - const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); - expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create context/)).length).toBe(2); - expect(lines.filter(line => line.match(/close context/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2); - expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2); - - expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2); - expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2); - }).toPass(); -}); - -test('http transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); - - const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await transport1.terminateSession(); - await client1.close(); - - const transport3 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client3 = new Client({ name: 'test', version: '1.0.0' }); - await client3.connect(transport3); - await client3.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - await transport2.terminateSession(); - await client2.close(); - await transport3.terminateSession(); - await client3.close(); - - await expect(async () => { - const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create http session/)).length).toBe(3); - expect(lines.filter(line => line.match(/delete http session/)).length).toBe(3); - - expect(lines.filter(line => line.match(/create context/)).length).toBe(3); - expect(lines.filter(line => line.match(/close context/)).length).toBe(3); - - expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3); - expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3); - - expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1); - expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1); - }).toPass(); -}); - -test('http transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint(); - - const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await transport1.terminateSession(); - await client1.close(); - - const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await transport2.terminateSession(); - await client2.close(); - - await expect(async () => { - const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); - expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create context/)).length).toBe(2); - expect(lines.filter(line => line.match(/close context/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2); - expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2); - - expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2); - expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2); - }).toPass(); -}); - -test('http transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => { - const { url } = await serverEndpoint(); - - const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - const response = await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - expect(response.isError).toBe(true); - expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser'); - - await client1.close(); - await client2.close(); -}); - -test('http transport (default)', async ({ serverEndpoint }) => { - const { url } = await serverEndpoint(); - const transport = new StreamableHTTPClientTransport(url); - const client = new Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); - expect(transport.sessionId, 'has session support').toBeDefined(); -}); diff --git a/tests/iframes.spec.ts b/tests/iframes.spec.ts deleted file mode 100644 index 3c53de1..0000000 --- a/tests/iframes.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('stitched aria frames', async ({ client }) => { - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: `data:text/html,

Hello

`, - }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: - - heading "Hello" [level=1] [ref=e2] - - iframe [ref=e3]: - - generic [active] [ref=f1e1]: - - button "World" [ref=f1e2] - - main [ref=f1e3]: - - iframe [ref=f1e4]: - - paragraph [ref=f2e2]: Nested -\`\`\``), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'World', - ref: 'f1e2', - }, - })).toHaveResponse({ - code: `await page.locator('iframe').first().contentFrame().getByRole('button', { name: 'World' }).click();`, - }); -}); diff --git a/tests/install.spec.ts b/tests/install.spec.ts deleted file mode 100644 index 70fde07..0000000 --- a/tests/install.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_install', async ({ client, mcpBrowser }) => { - test.skip(mcpBrowser !== 'chromium', 'Test only chromium'); - expect(await client.callTool({ - name: 'browser_install', - })).toHaveResponse({ - tabs: expect.stringContaining(`No open tabs`), - }); -}); diff --git a/tests/launch.spec.ts b/tests/launch.spec.ts deleted file mode 100644 index f7f0171..0000000 --- a/tests/launch.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { test, expect, formatOutput } from './fixtures'; - -test('test reopen browser', async ({ startClient, server, mcpMode }) => { - const { client, stderr } = await startClient(); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - expect(await client.callTool({ - name: 'browser_close', - })).toHaveResponse({ - code: `await page.close()`, - tabs: `No open tabs. Use the "browser_navigate" tool to navigate to a page first.`, - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - - await client.close(); - - if (process.platform === 'win32') - return; - - await expect.poll(() => formatOutput(stderr()), { timeout: 0 }).toEqual([ - 'create context', - 'create browser context (persistent)', - 'lock user data dir', - 'close context', - 'close browser context (persistent)', - 'release user data dir', - 'close browser context complete (persistent)', - 'create browser context (persistent)', - 'lock user data dir', - 'close context', - 'close browser context (persistent)', - 'release user data dir', - 'close browser context complete (persistent)', - ]); -}); - -test('executable path', async ({ startClient, server }) => { - const { client } = await startClient({ args: [`--executable-path=bogus`] }); - const response = await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - expect(response).toHaveResponse({ - result: expect.stringContaining(`executable doesn't exist`), - isError: true, - }); -}); - -test('persistent context', async ({ startClient, server }) => { - server.setContent('/', ` - - - - `, 'text/html'); - - const { client } = await startClient(); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Storage: NO`), - }); - - await new Promise(resolve => setTimeout(resolve, 3000)); - - await client.callTool({ - name: 'browser_close', - }); - - const { client: client2 } = await startClient(); - expect(await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Storage: YES`), - }); -}); - -test('isolated context', async ({ startClient, server }) => { - server.setContent('/', ` - - - - `, 'text/html'); - - const { client: client1 } = await startClient({ args: [`--isolated`] }); - expect(await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Storage: NO`), - }); - - await client1.callTool({ - name: 'browser_close', - }); - - const { client: client2 } = await startClient({ args: [`--isolated`] }); - expect(await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Storage: NO`), - }); -}); - -test('isolated context with storage state', async ({ startClient, server }, testInfo) => { - const storageStatePath = testInfo.outputPath('storage-state.json'); - await fs.promises.writeFile(storageStatePath, JSON.stringify({ - origins: [ - { - origin: server.PREFIX, - localStorage: [{ name: 'test', value: 'session-value' }], - }, - ], - })); - - server.setContent('/', ` - - - - `, 'text/html'); - - const { client } = await startClient({ args: [ - `--isolated`, - `--storage-state=${storageStatePath}`, - ] }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`Storage: session-value`), - }); -}); diff --git a/tests/mdb.spec.ts b/tests/mdb.spec.ts deleted file mode 100644 index 1d01a3a..0000000 --- a/tests/mdb.spec.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; - -import { runMainBackend, runOnPauseBackendLoop } from '../src/sdk/mdb'; - -import { test, expect } from './fixtures'; - -import type * as mcpServer from '../src/sdk/server'; -import type { ServerBackendOnPause } from '../src/sdk/mdb'; - -test('call top level tool', async () => { - const { mdbUrl } = await startMDBAndCLI(); - const mdbClient = await createMDBClient(mdbUrl); - - const { tools } = await mdbClient.client.listTools(); - expect(tools).toEqual([{ - name: 'cli_echo', - description: 'Echo a message', - inputSchema: expect.any(Object), - }, { - name: 'cli_pause_in_gdb', - description: 'Pause in gdb', - inputSchema: expect.any(Object), - }, { - name: 'cli_pause_in_gdb_twice', - description: 'Pause in gdb twice', - inputSchema: expect.any(Object), - } - ]); - - const echoResult = await mdbClient.client.callTool({ - name: 'cli_echo', - arguments: { - message: 'Hello, world!', - }, - }); - expect(echoResult.content).toEqual([{ type: 'text', text: 'Echo: Hello, world!' }]); - - await mdbClient.close(); -}); - -test('pause on error', async () => { - const { mdbUrl } = await startMDBAndCLI(); - const mdbClient = await createMDBClient(mdbUrl); - - // Make a call that results in a recoverable error. - const interruptResult = await mdbClient.client.callTool({ - name: 'cli_pause_in_gdb', - arguments: {}, - }); - expect(interruptResult.content).toEqual([{ type: 'text', text: 'Paused on exception' }]); - - // List new inner tools. - const { tools } = await mdbClient.client.listTools(); - expect(tools).toEqual([ - expect.objectContaining({ - name: 'gdb_bt', - }), - expect.objectContaining({ - name: 'gdb_continue', - }), - ]); - - // Call the new inner tool. - const btResult = await mdbClient.client.callTool({ - name: 'gdb_bt', - arguments: {}, - }); - expect(btResult.content).toEqual([{ type: 'text', text: 'Backtrace' }]); - - // Continue execution. - const continueResult = await mdbClient.client.callTool({ - name: 'gdb_continue', - arguments: {}, - }); - expect(continueResult.content).toEqual([{ type: 'text', text: 'Done' }]); - - await mdbClient.close(); -}); - -test('pause on error twice', async () => { - const { mdbUrl } = await startMDBAndCLI(); - const mdbClient = await createMDBClient(mdbUrl); - - // Make a call that results in a recoverable error. - const result = await mdbClient.client.callTool({ - name: 'cli_pause_in_gdb_twice', - arguments: {}, - }); - expect(result.content).toEqual([{ type: 'text', text: 'Paused on exception 1' }]); - - // Continue execution. - const continueResult1 = await mdbClient.client.callTool({ - name: 'gdb_continue', - arguments: {}, - }); - expect(continueResult1.content).toEqual([{ type: 'text', text: 'Paused on exception 2' }]); - - const continueResult2 = await mdbClient.client.callTool({ - name: 'gdb_continue', - arguments: {}, - }); - expect(continueResult2.content).toEqual([{ type: 'text', text: 'Done' }]); - - await mdbClient.close(); -}); - -async function startMDBAndCLI(): Promise<{ mdbUrl: string }> { - const mdbUrlBox = { mdbUrl: undefined as string | undefined }; - const cliBackendFactory = { - name: 'CLI', - nameInConfig: 'cli', - version: '0.0.0', - create: () => new CLIBackend(mdbUrlBox) - }; - - const mdbUrl = (await runMainBackend(cliBackendFactory, { port: 0 }))!; - mdbUrlBox.mdbUrl = mdbUrl; - return { mdbUrl }; -} - -async function createMDBClient(mdbUrl: string): Promise<{ client: Client, close: () => Promise }> { - const client = new Client({ name: 'Internal client', version: '0.0.0' }); - const transport = new StreamableHTTPClientTransport(new URL(mdbUrl)); - await client.connect(transport); - return { - client, - close: async () => { - await transport.terminateSession(); - await client.close(); - } - }; -} - -class CLIBackend implements mcpServer.ServerBackend { - constructor(private readonly mdbUrlBox: { mdbUrl: string | undefined }) {} - - async listTools(): Promise { - return [{ - name: 'cli_echo', - description: 'Echo a message', - inputSchema: zodToJsonSchema(z.object({ message: z.string() })) as any, - }, { - name: 'cli_pause_in_gdb', - description: 'Pause in gdb', - inputSchema: zodToJsonSchema(z.object({})) as any, - }, { - name: 'cli_pause_in_gdb_twice', - description: 'Pause in gdb twice', - inputSchema: zodToJsonSchema(z.object({})) as any, - }]; - } - - async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { - if (name === 'cli_echo') - return { content: [{ type: 'text', text: 'Echo: ' + (args?.message as string) }] }; - if (name === 'cli_pause_in_gdb') { - await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception'); - return { content: [{ type: 'text', text: 'Done' }] }; - } - if (name === 'cli_pause_in_gdb_twice') { - await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception 1'); - await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception 2'); - return { content: [{ type: 'text', text: 'Done' }] }; - } - throw new Error(`Unknown tool: ${name}`); - } -} - -class GDBBackend implements ServerBackendOnPause { - private _server!: mcpServer.Server; - - async initialize(server: mcpServer.Server): Promise { - this._server = server; - } - - async listTools(): Promise { - return [{ - name: 'gdb_bt', - description: 'Print backtrace', - inputSchema: zodToJsonSchema(z.object({})) as any, - }, { - name: 'gdb_continue', - description: 'Continue execution', - inputSchema: zodToJsonSchema(z.object({})) as any, - }]; - } - - async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { - if (name === 'gdb_bt') - return { content: [{ type: 'text', text: 'Backtrace' }] }; - if (name === 'gdb_continue') { - (this as ServerBackendOnPause).requestSelfDestruct?.(); - // Stall - await new Promise(f => setTimeout(f, 1000)); - } - throw new Error(`Unknown tool: ${name}`); - } -} diff --git a/tests/network.spec.ts b/tests/network.spec.ts deleted file mode 100644 index 40b3852..0000000 --- a/tests/network.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_network_requests', async ({ client, server }) => { - server.setContent('/', ` - - `, 'text/html'); - - server.setContent('/json', JSON.stringify({ name: 'John Doe' }), 'application/json'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - - await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Click me button', - ref: 'e2', - }, - }); - - await expect.poll(() => client.callTool({ - name: 'browser_network_requests', - })).toHaveResponse({ - result: expect.stringContaining(`[GET] ${`${server.PREFIX}`} => [200] OK -[GET] ${`${server.PREFIX}json`} => [200] OK`), - }); -}); diff --git a/tests/pdf.spec.ts b/tests/pdf.spec.ts deleted file mode 100644 index 937eb16..0000000 --- a/tests/pdf.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { test, expect } from './fixtures'; - -test('save as pdf unavailable', async ({ startClient, server }) => { - const { client } = await startClient(); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - expect(await client.callTool({ - name: 'browser_pdf_save', - })).toHaveResponse({ - result: 'Error: Tool "browser_pdf_save" not found', - isError: true, - }); -}); - -test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output'), capabilities: ['pdf'] }, - }); - - test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - - expect(await client.callTool({ - name: 'browser_pdf_save', - })).toHaveResponse({ - code: expect.stringContaining(`await page.pdf(`), - result: expect.stringMatching(/Saved page as.*page-[^:]+.pdf/), - }); -}); - -test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); - const { client } = await startClient({ - config: { outputDir, capabilities: ['pdf'] }, - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - - expect(await client.callTool({ - name: 'browser_pdf_save', - arguments: { - filename: 'output.pdf', - }, - })).toHaveResponse({ - result: expect.stringContaining(`output.pdf`), - code: expect.stringContaining(`await page.pdf(`), - }); - - const files = [...fs.readdirSync(outputDir)]; - - expect(fs.existsSync(outputDir)).toBeTruthy(); - const pdfFiles = files.filter(f => f.endsWith('.pdf')); - expect(pdfFiles).toHaveLength(1); - expect(pdfFiles[0]).toMatch(/^output.pdf$/); -}); diff --git a/tests/request-blocking.spec.ts b/tests/request-blocking.spec.ts deleted file mode 100644 index 21a7185..0000000 --- a/tests/request-blocking.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { test, expect } from './fixtures.ts'; - -const BLOCK_MESSAGE = /Blocked by Web Inspector|NS_ERROR_FAILURE|net::ERR_BLOCKED_BY_CLIENT/g; - -const fetchPage = async (client: Client, url: string) => { - const result = await client.callTool({ - name: 'browser_navigate', - arguments: { - url, - }, - }); - - return JSON.stringify(result, null, 2); -}; - -test('default to allow all', async ({ server, client }) => { - server.setContent('/ppp', 'content:PPP', 'text/html'); - const result = await fetchPage(client, server.PREFIX + 'ppp'); - expect(result).toContain('content:PPP'); -}); - -test('blocked works', async ({ startClient }) => { - const { client } = await startClient({ - args: ['--blocked-origins', 'microsoft.com;example.com;playwright.dev'] - }); - const result = await fetchPage(client, 'https://example.com/'); - expect(result).toMatch(BLOCK_MESSAGE); -}); - -test('allowed works', async ({ server, startClient }) => { - server.setContent('/ppp', 'content:PPP', 'text/html'); - const { client } = await startClient({ - args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`] - }); - const result = await fetchPage(client, server.PREFIX + 'ppp'); - expect(result).toContain('content:PPP'); -}); - -test('blocked takes precedence', async ({ startClient }) => { - const { client } = await startClient({ - args: [ - '--blocked-origins', 'example.com', - '--allowed-origins', 'example.com', - ], - }); - const result = await fetchPage(client, 'https://example.com/'); - expect(result).toMatch(BLOCK_MESSAGE); -}); - -test('allowed without blocked blocks all non-explicitly specified origins', async ({ startClient }) => { - const { client } = await startClient({ - args: ['--allowed-origins', 'playwright.dev'], - }); - const result = await fetchPage(client, 'https://example.com/'); - expect(result).toMatch(BLOCK_MESSAGE); -}); - -test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => { - server.setContent('/ppp', 'content:PPP', 'text/html'); - const { client } = await startClient({ - args: ['--blocked-origins', 'example.com'], - }); - const result = await fetchPage(client, server.PREFIX + 'ppp'); - expect(result).toContain('content:PPP'); -}); diff --git a/tests/roots.spec.ts b/tests/roots.spec.ts deleted file mode 100644 index 904a326..0000000 --- a/tests/roots.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import { pathToFileURL } from 'url'; - -import { test, expect } from './fixtures'; - -const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder'; - -test('should use separate user data by root path', async ({ startClient, server }, testInfo) => { - const { client } = await startClient({ - clientName: 'Visual Studio Code', - roots: [ - { - name: 'test', - uri: 'file://' + p.replace(/\\/g, '/'), - } - ], - }); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const hash = createHash(p); - const [file] = await fs.promises.readdir(testInfo.outputPath('ms-playwright')); - expect(file).toContain(hash); -}); - -test('check that trace is saved in workspace', async ({ startClient, server }, testInfo) => { - const rootPath = testInfo.outputPath('workspace'); - const { client } = await startClient({ - args: ['--save-trace'], - clientName: 'My client', - roots: [ - { - name: 'workspace', - uri: pathToFileURL(rootPath).toString(), - }, - ], - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - const [file] = await fs.promises.readdir(path.join(rootPath, '.playwright-mcp')); - expect(file).toContain('traces'); -}); - -test('should list all tools when listRoots is slow', async ({ startClient }) => { - const { client } = await startClient({ - clientName: 'Another custom client', - roots: [], - rootsResponseDelay: 1000, - }); - const tools = await client.listTools(); - expect(tools.tools.length).toBeGreaterThan(10); -}); - -function createHash(data: string): string { - return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); -} diff --git a/tests/screenshot.spec.ts b/tests/screenshot.spec.ts deleted file mode 100644 index 50b2eae..0000000 --- a/tests/screenshot.spec.ts +++ /dev/null @@ -1,327 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { test, expect } from './fixtures'; - -test('browser_take_screenshot (viewport)', async ({ startClient, server }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - })).toHaveResponse({ - code: expect.stringContaining(`await page.screenshot`), - attachments: [{ - data: expect.any(String), - mimeType: 'image/png', - type: 'image', - }], - }); -}); - -test('browser_take_screenshot (element)', async ({ startClient, server }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`[ref=e1]`), - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - arguments: { - element: 'hello button', - ref: 'e1', - }, - })).toEqual({ - content: [ - { - text: expect.stringContaining(`page.getByText('Hello, world!').screenshot`), - type: 'text', - }, - { - data: expect.any(String), - mimeType: 'image/png', - type: 'image', - }, - ], - }); -}); - -test('--output-dir should work', async ({ startClient, server }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - const { client } = await startClient({ - config: { outputDir }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - await client.callTool({ - name: 'browser_take_screenshot', - }); - - expect(fs.existsSync(outputDir)).toBeTruthy(); - const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png')); - expect(files).toHaveLength(1); - expect(files[0]).toMatch(/^page-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.png$/); -}); - -for (const type of ['png', 'jpeg']) { - test(`browser_take_screenshot (type: ${type})`, async ({ startClient, server }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - const { client } = await startClient({ - config: { outputDir }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - arguments: { type }, - })).toEqual({ - content: [ - { - text: expect.stringMatching( - new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.${type}`) - ), - type: 'text', - }, - { - data: expect.any(String), - mimeType: `image/${type}`, - type: 'image', - }, - ], - }); - - const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith(`.${type}`)); - - expect(fs.existsSync(outputDir)).toBeTruthy(); - expect(files).toHaveLength(1); - expect(files[0]).toMatch( - new RegExp(`^page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${type}$`) - ); - }); - -} - -test('browser_take_screenshot (default type should be png)', async ({ startClient, server }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - const { client } = await startClient({ - config: { outputDir }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - })).toHaveResponse({ - code: `await page.goto('${server.PREFIX}');`, - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - })).toEqual({ - content: [ - { - text: expect.stringMatching( - new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.png`) - ), - type: 'text', - }, - { - data: expect.any(String), - mimeType: 'image/png', - type: 'image', - }, - ], - }); - - const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png')); - - expect(fs.existsSync(outputDir)).toBeTruthy(); - expect(files).toHaveLength(1); - expect(files[0]).toMatch(/^page-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.png$/); -}); - -test('browser_take_screenshot (filename: "output.png")', async ({ startClient, server }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - const { client } = await startClient({ - config: { outputDir }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - arguments: { - filename: 'output.png', - }, - })).toEqual({ - content: [ - { - text: expect.stringContaining(`output.png`), - type: 'text', - }, - { - data: expect.any(String), - mimeType: 'image/png', - type: 'image', - }, - ], - }); - - const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png')); - - expect(fs.existsSync(outputDir)).toBeTruthy(); - expect(files).toHaveLength(1); - expect(files[0]).toMatch(/^output\.png$/); -}); - -test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, server }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - const { client } = await startClient({ - config: { - outputDir, - imageResponses: 'omit', - }, - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - await client.callTool({ - name: 'browser_take_screenshot', - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - })).toEqual({ - content: [ - { - text: expect.stringContaining(`await page.screenshot`), - type: 'text', - }, - ], - }); -}); - -test('browser_take_screenshot (fullPage: true)', async ({ startClient, server }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - expect(await client.callTool({ - name: 'browser_take_screenshot', - arguments: { fullPage: true }, - })).toEqual({ - content: [ - { - text: expect.stringContaining('fullPage: true'), - type: 'text', - } - ], - }); -}); - -test('browser_take_screenshot (fullPage with element should error)', async ({ startClient, server }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - pageState: expect.stringContaining(`[ref=e1]`), - }); - - const result = await client.callTool({ - name: 'browser_take_screenshot', - arguments: { - fullPage: true, - element: 'hello button', - ref: 'e1', - }, - }); - - expect(result.isError).toBe(true); - expect(result.content?.[0]?.text).toContain('fullPage cannot be used with element screenshots'); -}); - -test('browser_take_screenshot (viewport without snapshot)', async ({ startClient, server }, testInfo) => { - const { client } = await startClient({ - config: { outputDir: testInfo.outputPath('output') }, - }); - - // Ensure we have a tab but don't navigate anywhere (no snapshot captured) - expect(await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'list', - }, - })).toHaveResponse({ - tabs: `- 0: (current) [] (about:blank)`, - }); - - // This should work without requiring a snapshot since it's a viewport screenshot - expect(await client.callTool({ - name: 'browser_take_screenshot', - })).toEqual({ - content: [ - { - text: expect.stringContaining(`page.screenshot`), - type: 'text', - }, - { - data: expect.any(String), - mimeType: 'image/png', - type: 'image', - }, - ], - }); -}); diff --git a/tests/session-log.spec.ts b/tests/session-log.spec.ts deleted file mode 100644 index 364723d..0000000 --- a/tests/session-log.spec.ts +++ /dev/null @@ -1,275 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; - -import { test, expect } from './fixtures'; - -test('session log should record tool calls', async ({ startClient, server }, testInfo) => { - const { client, stderr } = await startClient({ - args: [ - '--save-session', - '--output-dir', testInfo.outputPath('output'), - ], - }); - - server.setContent('/', `Title`, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Submit button', - ref: 'e2', - }, - })).toHaveResponse({ - code: `await page.getByRole('button', { name: 'Submit' }).click();`, - pageState: expect.stringContaining(`- button "Submit"`), - }); - - const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; - const sessionFolder = output.substring('Session: '.length); - await expect.poll(() => readSessionLog(sessionFolder)).toBe(` -### Tool call: browser_navigate -- Args -\`\`\`json -{ - "url": "http://localhost:${server.PORT}/" -} -\`\`\` -- Code -\`\`\`js -await page.goto('http://localhost:${server.PORT}/'); -\`\`\` -- Snapshot: 001.snapshot.yml - - -### Tool call: browser_click -- Args -\`\`\`json -{ - "element": "Submit button", - "ref": "e2" -} -\`\`\` -- Code -\`\`\`js -await page.getByRole('button', { name: 'Submit' }).click(); -\`\`\` -- Snapshot: 002.snapshot.yml - -`); -}); - -test('session log should record user action', async ({ cdpServer, startClient }, testInfo) => { - const browserContext = await cdpServer.start(); - const { client, stderr } = await startClient({ - args: [ - '--save-session', - '--output-dir', testInfo.outputPath('output'), - `--cdp-endpoint=${cdpServer.endpoint}`, - ], - }); - - // Force browser context creation. - await client.callTool({ - name: 'browser_snapshot', - }); - - const [page] = browserContext.pages(); - await page.setContent(` - - - `); - - await page.getByRole('button', { name: 'Button 1' }).click(); - - const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; - const sessionFolder = output.substring('Session: '.length); - - await expect.poll(() => readSessionLog(sessionFolder)).toBe(` -### Tool call: browser_snapshot -- Args -\`\`\`json -{} -\`\`\` -- Snapshot: 001.snapshot.yml - - -### User action: click -- Args -\`\`\`json -{ - "name": "click", - "ref": "e2", - "button": "left", - "modifiers": 0, - "clickCount": 1 -} -\`\`\` -- Code -\`\`\`js -await page.getByRole('button', { name: 'Button 1' }).click(); -\`\`\` -- Snapshot: 002.snapshot.yml - -`); -}); - -test('session log should update user action', async ({ cdpServer, startClient }, testInfo) => { - const browserContext = await cdpServer.start(); - const { client, stderr } = await startClient({ - args: [ - '--save-session', - '--output-dir', testInfo.outputPath('output'), - `--cdp-endpoint=${cdpServer.endpoint}`, - ], - }); - - // Force browser context creation. - await client.callTool({ - name: 'browser_snapshot', - }); - - const [page] = browserContext.pages(); - await page.setContent(` - - - `); - - await page.getByRole('button', { name: 'Button 1' }).dblclick(); - - const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; - const sessionFolder = output.substring('Session: '.length); - - await expect.poll(() => readSessionLog(sessionFolder)).toBe(` -### Tool call: browser_snapshot -- Args -\`\`\`json -{} -\`\`\` -- Snapshot: 001.snapshot.yml - - -### User action: click -- Args -\`\`\`json -{ - "name": "click", - "ref": "e2", - "button": "left", - "modifiers": 0, - "clickCount": 2 -} -\`\`\` -- Code -\`\`\`js -await page.getByRole('button', { name: 'Button 1' }).dblclick(); -\`\`\` -- Snapshot: 002.snapshot.yml - -`); -}); - -test('session log should record tool calls and user actions', async ({ cdpServer, startClient }, testInfo) => { - const browserContext = await cdpServer.start(); - const { client, stderr } = await startClient({ - args: [ - '--save-session', - '--output-dir', testInfo.outputPath('output'), - `--cdp-endpoint=${cdpServer.endpoint}`, - ], - }); - - const [page] = browserContext.pages(); - await page.setContent(` - - - `); - - await client.callTool({ - name: 'browser_snapshot', - }); - - // Manual action. - await page.getByRole('button', { name: 'Button 1' }).click(); - - // This is to simulate a delay after the user action before the tool action. - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Tool action. - await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button 2', - ref: 'e3', - }, - }); - - const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; - const sessionFolder = output.substring('Session: '.length); - await expect.poll(() => readSessionLog(sessionFolder)).toBe(` -### Tool call: browser_snapshot -- Args -\`\`\`json -{} -\`\`\` -- Snapshot: 001.snapshot.yml - - -### User action: click -- Args -\`\`\`json -{ - "name": "click", - "ref": "e2", - "button": "left", - "modifiers": 0, - "clickCount": 1 -} -\`\`\` -- Code -\`\`\`js -await page.getByRole('button', { name: 'Button 1' }).click(); -\`\`\` -- Snapshot: 002.snapshot.yml - - -### Tool call: browser_click -- Args -\`\`\`json -{ - "element": "Button 2", - "ref": "e3" -} -\`\`\` -- Code -\`\`\`js -await page.getByRole('button', { name: 'Button 2' }).click(); -\`\`\` -- Snapshot: 003.snapshot.yml - -`); -}); - -async function readSessionLog(sessionFolder: string): Promise { - return await fs.promises.readFile(path.join(sessionFolder, 'session.md'), 'utf8').catch(() => ''); -} diff --git a/tests/sse.spec.ts b/tests/sse.spec.ts deleted file mode 100644 index 62183d9..0000000 --- a/tests/sse.spec.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { ChildProcess, spawn } from 'child_process'; -import path from 'path'; -import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -import { test as baseTest, expect } from './fixtures'; -import type { Config } from '../config.d.ts'; - -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ - serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { - let cp: ChildProcess | undefined; - const userDataDir = testInfo.outputPath('user-data-dir'); - await use(async (options?: { args?: string[], noPort?: boolean }) => { - if (cp) - throw new Error('Process already running'); - - cp = spawn('node', [ - path.join(path.dirname(__filename), '../cli.js'), - ...(options?.noPort ? [] : ['--port=0']), - '--user-data-dir=' + userDataDir, - ...(mcpHeadless ? ['--headless'] : []), - ...(options?.args || []), - ], { - stdio: 'pipe', - env: { - ...process.env, - DEBUG: 'pw:mcp:test', - DEBUG_COLORS: '0', - DEBUG_HIDE_DATE: '1', - }, - }); - let stderr = ''; - const url = await new Promise(resolve => cp!.stderr?.on('data', data => { - stderr += data.toString(); - const match = stderr.match(/Listening on (http:\/\/.*)/); - if (match) - resolve(match[1]); - })); - - return { url: new URL(url), stderr: () => stderr }; - }); - cp?.kill('SIGTERM'); - }, -}); - -test('sse transport', async ({ serverEndpoint }) => { - const { url } = await serverEndpoint(); - const transport = new SSEClientTransport(new URL('/sse', url)); - const client = new Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); -}); - -test('sse transport (config)', async ({ serverEndpoint }) => { - const config: Config = { - server: { - port: 0, - } - }; - const configFile = test.info().outputPath('config.json'); - await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2)); - - const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] }); - const transport = new SSEClientTransport(new URL('/sse', url)); - const client = new Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); -}); - -test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); - - const transport1 = new SSEClientTransport(new URL('/sse', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await client1.close(); - - const transport2 = new SSEClientTransport(new URL('/sse', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await client2.close(); - - await expect(async () => { - const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2); - expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create context/)).length).toBe(2); - expect(lines.filter(line => line.match(/close context/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2); - expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2); - - expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2); - expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2); - }).toPass(); -}); - -test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); - - const transport1 = new SSEClientTransport(new URL('/sse', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const transport2 = new SSEClientTransport(new URL('/sse', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await client1.close(); - - const transport3 = new SSEClientTransport(new URL('/sse', url)); - const client3 = new Client({ name: 'test', version: '1.0.0' }); - await client3.connect(transport3); - await client3.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - await client2.close(); - await client3.close(); - - await expect(async () => { - const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(3); - expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(3); - - expect(lines.filter(line => line.match(/create context/)).length).toBe(3); - expect(lines.filter(line => line.match(/close context/)).length).toBe(3); - - expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3); - expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3); - - expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1); - expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1); - }).toPass(); -}); - -test('sse transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint(); - - const transport1 = new SSEClientTransport(new URL('/sse', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await client1.close(); - - const transport2 = new SSEClientTransport(new URL('/sse', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - await client2.close(); - - await expect(async () => { - const lines = stderr().split('\n'); - expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2); - expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create context/)).length).toBe(2); - expect(lines.filter(line => line.match(/close context/)).length).toBe(2); - - expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2); - expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2); - - expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2); - expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2); - }).toPass(); -}); - -test('sse transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => { - const { url } = await serverEndpoint(); - - const transport1 = new SSEClientTransport(new URL('/sse', url)); - const client1 = new Client({ name: 'test', version: '1.0.0' }); - await client1.connect(transport1); - await client1.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const transport2 = new SSEClientTransport(new URL('/sse', url)); - const client2 = new Client({ name: 'test', version: '1.0.0' }); - await client2.connect(transport2); - const response = await client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - expect(response.isError).toBe(true); - expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser'); - - await client1.close(); - await client2.close(); -}); diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts deleted file mode 100644 index a0b40d8..0000000 --- a/tests/tabs.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -async function createTab(client: Client, title: string, body: string) { - await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'new', - }, - }); - return await client.callTool({ - name: 'browser_navigate', - arguments: { - url: `data:text/html,${title}${body}`, - }, - }); -} - -test('list initial tabs', async ({ client }) => { - expect(await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'list', - }, - })).toHaveResponse({ - tabs: `- 0: (current) [] (about:blank)`, - }); -}); - -test('list first tab', async ({ client }) => { - await createTab(client, 'Tab one', 'Body one'); - expect(await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'list', - }, - })).toHaveResponse({ - tabs: `- 0: [] (about:blank) -- 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, - }); -}); - -test('create new tab', async ({ client }) => { - expect(await createTab(client, 'Tab one', 'Body one')).toHaveResponse({ - tabs: `- 0: [] (about:blank) -- 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, - pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one -- Page Title: Tab one -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Body one -\`\`\``), - }); - - expect(await createTab(client, 'Tab two', 'Body two')).toHaveResponse({ - tabs: `- 0: [] (about:blank) -- 1: [Tab one] (data:text/html,Tab oneBody one) -- 2: (current) [Tab two] (data:text/html,Tab twoBody two)`, - pageState: expect.stringContaining(`- Page URL: data:text/html,Tab twoBody two -- Page Title: Tab two -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Body two -\`\`\``), - }); -}); - -test('select tab', async ({ client }) => { - await createTab(client, 'Tab one', 'Body one'); - await createTab(client, 'Tab two', 'Body two'); - - expect(await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'select', - index: 1, - }, - })).toHaveResponse({ - tabs: `- 0: [] (about:blank) -- 1: (current) [Tab one] (data:text/html,Tab oneBody one) -- 2: [Tab two] (data:text/html,Tab twoBody two)`, - pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one -- Page Title: Tab one -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Body one -\`\`\``), - }); - - expect(await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'select', - index: 0, - }, - })).toHaveResponse({ - tabs: `- 0: (current) [] (about:blank) -- 1: [Tab one] (data:text/html,Tab oneBody one) -- 2: [Tab two] (data:text/html,Tab twoBody two)`, - pageState: expect.stringContaining(`- Page URL: about:blank`), - }); -}); - -test('close tab', async ({ client }) => { - await createTab(client, 'Tab one', 'Body one'); - await createTab(client, 'Tab two', 'Body two'); - - expect(await client.callTool({ - name: 'browser_tabs', - arguments: { - action: 'close', - index: 2, - }, - })).toHaveResponse({ - tabs: `- 0: [] (about:blank) -- 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, - pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one -- Page Title: Tab one -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Body one -\`\`\``), - }); -}); - -test('reuse first tab when navigating', async ({ startClient, cdpServer, server }) => { - const browserContext = await cdpServer.start(); - const pages = browserContext.pages(); - - const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - expect(pages.length).toBe(1); - expect(await pages[0].title()).toBe('Title'); -}); diff --git a/tests/trace.spec.ts b/tests/trace.spec.ts deleted file mode 100644 index 927f23a..0000000 --- a/tests/trace.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { test, expect } from './fixtures'; - -test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - - const { client } = await startClient({ - args: ['--save-trace', `--output-dir=${outputDir}`], - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - const [file] = await fs.promises.readdir(outputDir); - expect(file).toContain('traces'); -}); diff --git a/tests/type.spec.ts b/tests/type.spec.ts deleted file mode 100644 index 7ac2314..0000000 --- a/tests/type.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_type', async ({ client, server }) => { - server.setContent('/', ` - - - - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - - { - const response = await client.callTool({ - name: 'browser_type', - arguments: { - element: 'textbox', - ref: 'e2', - text: 'Hi!', - submit: true, - }, - }); - expect(response).toHaveResponse({ - code: `await page.getByRole('textbox').fill('Hi!'); -await page.getByRole('textbox').press('Enter');`, - pageState: expect.stringContaining(`- textbox`), - }); - } - - expect(await client.callTool({ - name: 'browser_console_messages', - })).toHaveResponse({ - result: expect.stringContaining(`[LOG] Key pressed: Enter , Text: Hi!`), - }); -}); - -test('browser_type (slowly)', async ({ client, server }) => { - server.setContent('/', ` - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - { - const response = await client.callTool({ - name: 'browser_type', - arguments: { - element: 'textbox', - ref: 'e2', - text: 'Hi!', - slowly: true, - }, - }); - - expect(response).toHaveResponse({ - code: `await page.getByRole('textbox').pressSequentially('Hi!');`, - pageState: expect.stringContaining(`- textbox`), - }); - } - const response = await client.callTool({ - name: 'browser_console_messages', - }); - expect(response).toHaveResponse({ - result: expect.stringContaining(`[LOG] Key pressed: H Text: `), - }); - expect(response).toHaveResponse({ - result: expect.stringContaining(`[LOG] Key pressed: i Text: H`), - }); - expect(response).toHaveResponse({ - result: expect.stringContaining(`[LOG] Key pressed: ! Text: Hi`), - }); -}); - -test('browser_type (no submit)', async ({ client, server }) => { - server.setContent('/', ` - - `, 'text/html'); - - { - const response = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - }); - expect(response).toHaveResponse({ - pageState: expect.stringContaining(`- textbox`), - }); - } - { - const response = await client.callTool({ - name: 'browser_type', - arguments: { - element: 'textbox', - ref: 'e2', - text: 'Hi!', - }, - }); - expect(response).toHaveResponse({ - code: expect.stringContaining(`fill('Hi!')`), - // Should yield no snapshot. - pageState: expect.not.stringContaining(`- textbox`), - }); - } - { - const response = await client.callTool({ - name: 'browser_console_messages', - }); - expect(response).toHaveResponse({ - result: expect.stringContaining(`[LOG] New value: Hi!`), - }); - } -}); diff --git a/tests/verify.spec.ts b/tests/verify.spec.ts deleted file mode 100644 index 58c9b36..0000000 --- a/tests/verify.spec.ts +++ /dev/null @@ -1,522 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test.use({ mcpArgs: ['--caps=verify'] }); - -test('browser_verify_element_visible', async ({ client, server }) => { - server.setContent('/', ` - Test Page - -

Welcome

-
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_element_visible', - arguments: { - role: 'button', - accessibleName: 'Submit', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();`, - }); - - expect(await client.callTool({ - name: 'browser_verify_element_visible', - arguments: { - role: 'heading', - accessibleName: 'Welcome', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();`, - }); - - expect(await client.callTool({ - name: 'browser_verify_element_visible', - arguments: { - role: 'alert', - accessibleName: 'Success message', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByRole('alert', { name: 'Success message' })).toBeVisible();`, - }); -}); - -test('browser_verify_element_visible (not found)', async ({ client, server }) => { - server.setContent('/', ` - Test Page - - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_element_visible', - arguments: { - role: 'button', - accessibleName: 'Cancel', - }, - })).toHaveResponse({ - isError: true, - result: 'Element with role "button" and accessible name "Cancel" not found', - }); -}); - -test('browser_verify_text_visible', async ({ client, server }) => { - server.setContent('/', ` - Test Page -

Hello world

-
Welcome to our site
- Status: Active - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_text_visible', - arguments: { - text: 'Hello world', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByText('Hello world')).toBeVisible();`, - }); - - expect(await client.callTool({ - name: 'browser_verify_text_visible', - arguments: { - text: 'Welcome to our site', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByText('Welcome to our site')).toBeVisible();`, - }); - - expect(await client.callTool({ - name: 'browser_verify_text_visible', - arguments: { - text: 'Status: Active', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByText('Status: Active')).toBeVisible();`, - }); -}); - -test('browser_verify_text_visible (not found)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -

Hello world

- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_text_visible', - arguments: { - text: 'Goodbye world', - }, - })).toHaveResponse({ - isError: true, - result: 'Text not found', - }); -}); - -test('browser_verify_text_visible (with quotes)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -

She said "Hello world"

-
It's a beautiful day
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_text_visible', - arguments: { - text: 'She said "Hello world"', - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByText('She said "Hello world"')).toBeVisible();`, - }); - - expect(await client.callTool({ - name: 'browser_verify_text_visible', - arguments: { - text: "It's a beautiful day", - }, - })).toHaveResponse({ - result: 'Done', - code: `await expect(page.getByText('It\\'s a beautiful day')).toBeVisible();`, - }); -}); - -test('browser_verify_list_visible', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
    -
  • Apple
  • -
  • Banana
  • -
  • Cherry
  • -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_list_visible', - arguments: { - element: 'Fruit list', - ref: 'e2', - items: ['Apple', 'Banana', 'Cherry'], - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\` -- list: - - listitem: "Apple" - - listitem: "Banana" - - listitem: "Cherry" -\`);`), - }); -}); - -test('browser_verify_list_visible (partial items)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
    -
  • Apple
  • -
  • Banana
  • -
  • Cherry
  • -
  • Date
  • -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_list_visible', - arguments: { - element: 'Fruit list', - ref: 'e2', - items: ['Apple', 'Cherry'], - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\` -- list: - - listitem: "Apple" - - listitem: "Cherry" -\`);`), - }); -}); - -test('browser_verify_list_visible (item not found)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
    -
  • Apple
  • -
  • Banana
  • -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_list_visible', - arguments: { - element: 'Fruit list', - ref: 'e2', - items: ['Apple', 'Cherry'], - }, - })).toHaveResponse({ - isError: true, - result: 'Item "Cherry" not found', - }); -}); - -test('browser_verify_value (textbox)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- - -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'textbox', - element: 'Name textbox', - ref: 'e3', - value: 'John Doe', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('textbox', { name: 'Name' })).toHaveValue('John Doe');`), - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'textbox', - element: 'Email textbox', - ref: 'e4', - value: 'john@example.com', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('john@example.com');`), - }); -}); - -test('browser_verify_value (textbox wrong value)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'textbox', - element: 'Name textbox', - ref: 'e3', - value: 'Jane Smith', - }, - })).toHaveResponse({ - isError: true, - result: 'Expected value "Jane Smith", but got "John Doe"', - }); -}); - -test('browser_verify_value (checkbox checked)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- - -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'checkbox', - element: 'Subscribe checkbox', - ref: 'e3', - value: 'true', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('checkbox')).toBeChecked();`), - }); -}); - -test('browser_verify_value (checkbox unchecked)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- - -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'checkbox', - element: 'Subscribe checkbox', - ref: 'e3', - value: 'false', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('checkbox')).not.toBeChecked();`), - }); -}); - -test('browser_verify_value (checkbox wrong value)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- - -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'checkbox', - element: 'Subscribe checkbox', - ref: 'e3', - value: 'false', - }, - })).toHaveResponse({ - isError: true, - result: 'Expected value "false", but got "true"', - }); -}); - -test('browser_verify_value (radio checked)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- - - - -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'radio', - element: 'Color radio', - ref: 'e3', - value: 'true', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('radio', { name: 'Red' })).toBeChecked();`), - }); -}); - -test('browser_verify_value (slider)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- - -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'slider', - element: 'Volume slider', - ref: 'e3', - value: '75', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('slider')).toHaveValue('75');`), - }); -}); - -test('browser_verify_value (combobox)', async ({ client, server }) => { - server.setContent('/', ` - Test Page -
- -
- `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_verify_value', - arguments: { - type: 'combobox', - element: 'Country select', - ref: 'e3', - value: 'United States', - }, - })).toHaveResponse({ - result: 'Done', - code: expect.stringContaining(`await expect(page.getByRole('combobox')).toHaveValue('United States');`), - }); -}); diff --git a/tests/vscode.spec.ts b/tests/vscode.spec.ts deleted file mode 100644 index 7f1c3d2..0000000 --- a/tests/vscode.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_connect(vscode) works', async ({ startClient, playwright, browserName }) => { - const { client } = await startClient({ - args: ['--vscode'], - }); - - const server = await playwright[browserName].launchServer(); - - expect(await client.callTool({ - name: 'browser_connect', - arguments: { - connectionString: server.wsEndpoint(), - lib: require.resolve('playwright'), - } - })).toHaveResponse({ - result: 'Successfully connected.' - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,foo' - } - })).toHaveResponse({ - pageState: expect.stringContaining('foo'), - }); - - await server.close(); - - expect(await client.callTool({ - name: 'browser_snapshot', - arguments: {} - }), 'it actually used the server').toHaveResponse({ - isError: true, - result: expect.stringContaining('ECONNREFUSED') - }); -}); - -test('browser_connect(debugController) works', async ({ startClient }) => { - test.skip(!globalThis.WebSocket, 'WebSocket is not supported in this environment'); - - const { client } = await startClient({ - args: ['--vscode'], - }); - - expect(await client.callTool({ - name: 'browser_connect', - arguments: { - debugController: true, - } - })).toHaveResponse({ - result: 'No open browsers.' - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,foo' - } - })).toHaveResponse({ - pageState: expect.stringContaining('foo'), - }); - - const response = await client.callTool({ - name: 'browser_connect', - arguments: { - debugController: true, - } - }); - expect(response.content?.[0].text).toMatch(/Version: \d+\.\d+\.\d+/); - const url = new URL(response.content?.[0].text.match(/URL: (.*)/)?.[1]); - const messages: unknown[] = []; - const socket = new WebSocket(url); - socket.onmessage = event => { - messages.push(JSON.parse(event.data)); - }; - await new Promise((resolve, reject) => { - socket.onopen = resolve; - socket.onerror = reject; - }); - - socket.send(JSON.stringify({ - id: '1', - guid: 'DebugController', - method: 'setReportStateChanged', - params: { - enabled: true, - }, - metadata: {}, - })); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,bar' - } - })).toHaveResponse({ - pageState: expect.stringContaining('bar'), - }); - - await expect.poll(() => messages).toContainEqual(expect.objectContaining({ method: 'stateChanged' })); -}); diff --git a/tests/wait.spec.ts b/tests/wait.spec.ts deleted file mode 100644 index 0388faf..0000000 --- a/tests/wait.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('browser_wait_for(text)', async ({ client, server }) => { - server.setContent('/', ` - - - -
Text to disappear
- - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Click me', - ref: 'e2', - }, - }); - - expect(await client.callTool({ - name: 'browser_wait_for', - arguments: { text: 'Text to appear' }, - code: `await page.getByText("Text to appear").first().waitFor({ state: 'visible' });`, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), - }); -}); - -test('browser_wait_for(textGone)', async ({ client, server }) => { - server.setContent('/', ` - - - -
Text to disappear
- - `, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Click me', - ref: 'e2', - }, - }); - - expect(await client.callTool({ - name: 'browser_wait_for', - arguments: { textGone: 'Text to disappear' }, - code: `await page.getByText("Text to disappear").first().waitFor({ state: 'hidden' });`, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), - }); -}); - -test('browser_wait_for(time)', async ({ client, server }) => { - server.setContent('/', `
Hello World
`, 'text/html'); - - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.PREFIX }, - }); - - expect(await client.callTool({ - name: 'browser_wait_for', - arguments: { time: 1 }, - })).toHaveResponse({ - code: `await new Promise(f => setTimeout(f, 1 * 1000));`, - }); -}); diff --git a/tests/webdriver.spec.ts b/tests/webdriver.spec.ts deleted file mode 100644 index ef742d7..0000000 --- a/tests/webdriver.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('do not falsely advertise user agent as a test driver', async ({ client, server, mcpBrowser }) => { - test.skip(mcpBrowser === 'firefox'); - test.skip(mcpBrowser === 'webkit'); - server.route('/', (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(` - - - `); - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - })).toHaveResponse({ - pageState: expect.stringContaining(`webdriver: false`), - }); -}); diff --git a/tsconfig.all.json b/tsconfig.all.json deleted file mode 100644 index 8e0c687..0000000 --- a/tsconfig.all.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["**/*.ts", "**/*.tsx", "**/*.js"], -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index c9424f8..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2019", - "esModuleInterop": true, - "strict": true, - "module": "commonjs", - "rootDir": "src", - "outDir": "./lib", - "resolveJsonModule": true - }, - "include": [ - "src", - ], -} diff --git a/utils/update-readme.js b/update-readme.js similarity index 95% rename from utils/update-readme.js rename to update-readme.js index fe5b06a..0d086cc 100644 --- a/utils/update-readme.js +++ b/update-readme.js @@ -21,7 +21,7 @@ const path = require('path') const { zodToJsonSchema } = require('zod-to-json-schema') const { execSync } = require('child_process'); -const { allTools } = require('../lib/browser/tools.js'); +const { allTools } = require('playwright/lib/mcp/browser/tools'); const capabilities = { 'core': 'Core automation', @@ -34,10 +34,6 @@ const capabilities = { const toolsByCapability = Object.fromEntries(Object.entries(capabilities).map(([capability, title]) => [title, allTools.filter(tool => tool.capability === capability).sort((a, b) => a.schema.name.localeCompare(b.schema.name))])); -/** - * @param {import('../src/sdk/tool.js').ToolSchema} tool - * @returns {string[]} - */ function formatToolForReadme(tool) { const lines = /** @type {string[]} */ ([]); lines.push(``); @@ -135,7 +131,7 @@ async function updateOptions(content) { } async function updateReadme() { - const readmePath = path.join(path.dirname(__filename), '..', 'README.md'); + const readmePath = path.join(__dirname, 'README.md'); const readmeContent = await fs.promises.readFile(readmePath, 'utf-8'); const withTools = await updateTools(readmeContent); const withOptions = await updateOptions(withTools); diff --git a/utils/check-deps.js b/utils/check-deps.js deleted file mode 100644 index adbdde8..0000000 --- a/utils/check-deps.js +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env node -/** - * Copyright 2019 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// @ts-check - -const fs = require('fs'); -const ts = require('typescript'); -const path = require('path'); -const Module = require('module'); - -const builtins = new Set(Module.builtinModules); - -const depsCache = {}; -const packageRoot = path.resolve(__dirname, '..'); - -async function checkDeps() { - const deps = new Set(); - const src = path.join(packageRoot, 'src'); - - - let packageJSON; - try { - packageJSON = require(path.resolve(path.join(packageRoot, 'package.json'))); - } catch { - } - - const program = ts.createProgram({ - options: { - allowJs: true, - target: ts.ScriptTarget.ESNext, - strict: true, - }, - rootNames: listAllFiles(src), - }); - const sourceFiles = program.getSourceFiles(); - const errors = []; - sourceFiles.filter(x => !x.fileName.includes(path.sep + 'node_modules' + path.sep) && !x.fileName.includes(path.sep + 'bundles' + path.sep)).map(x => visit(x, x.fileName, x.getFullText())); - - if (errors.length) { - for (const error of errors) - console.log(error); - console.log(`--------------------------------------------------------`); - console.log(`Changing the project structure or adding new components?`); - console.log(`Update DEPS in ${packageRoot}`); - console.log(`--------------------------------------------------------`); - process.exit(1); - } - - if (packageJSON) { - for (const dep of deps) { - const resolved = require.resolve(dep, { paths: [packageRoot] }); - if (dep === resolved || !resolved.includes('node_modules')) - deps.delete(dep); - } - for (const dep of Object.keys(packageJSON.dependencies || {})) - deps.delete(dep); - - if (deps.size) { - console.log('Dependencies are not declared in package.json:'); - for (const dep of deps) - console.log(` ${dep}`); - process.exit(1); - } - } - - return packageJSON; - - function visit(node, fileName, text) { - if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { - if (node.importClause) { - if (node.importClause.isTypeOnly) - return; - if (node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { - if (node.importClause.namedBindings.elements.every(e => e.isTypeOnly)) - return; - } - } - const importName = node.moduleSpecifier.text; - let importPath; - if (importName.startsWith('.')) - importPath = path.resolve(path.dirname(fileName), importName); - - const mergedDeps = calculateDeps(fileName); - if (mergedDeps.includes('***')) - return; - if (importPath) { - if (!fs.existsSync(importPath)) { - if (fs.existsSync(importPath + '.ts')) - importPath = importPath + '.ts'; - else if (fs.existsSync(importPath + '.tsx')) - importPath = importPath + '.tsx'; - else if (fs.existsSync(importPath + '.d.ts')) - importPath = importPath + '.d.ts'; - } - - if (!allowImport(fileName, importPath, mergedDeps)) - errors.push(`Disallowed import ${path.relative(packageRoot, importPath)} in ${path.relative(packageRoot, fileName)}`); - return; - } - - const fullStart = node.getFullStart(); - const commentRanges = ts.getLeadingCommentRanges(text, fullStart); - for (const range of commentRanges || []) { - const comment = text.substring(range.pos, range.end); - if (comment.includes('@no-check-deps')) - return; - } - - let depName; - if (importName.startsWith('@')) - depName = importName.split('/').slice(0, 2).join('/'); - else - depName = importName.split('/')[0]; - deps.add(depName); - try { - require.resolve(depName, { paths: [packageRoot] }) - } catch (e) { - console.log(`Invalid dependency ${depName} in ${fileName}`); - process.exit(1); - } - - if (!allowExternalImport(importName, packageJSON)) - errors.push(`Disallowed external dependency ${importName} from ${path.relative(packageRoot, fileName)}`); - } - ts.forEachChild(node, x => visit(x, fileName, text)); - } - - function calculateDeps(from) { - const fromDirectory = path.dirname(from); - let depsDirectory = fromDirectory; - while (depsDirectory.startsWith(packageRoot) && !depsCache[depsDirectory] && !fs.existsSync(path.join(depsDirectory, 'DEPS.list'))) - depsDirectory = path.dirname(depsDirectory); - if (!depsDirectory.startsWith(packageRoot)) - return []; - - let deps = depsCache[depsDirectory]; - if (!deps) { - const depsListFile = path.join(depsDirectory, 'DEPS.list'); - deps = {}; - let group = []; - for (const line of fs.readFileSync(depsListFile, 'utf-8').split('\n').filter(Boolean).filter(l => !l.startsWith('#'))) { - const groupMatch = line.match(/\[(.*)\]/); - if (groupMatch) { - group = []; - deps[groupMatch[1]] = group; - continue; - } - if (line === '***') - group.push('***'); - else - group.push(path.resolve(depsDirectory, line)); - } - depsCache[depsDirectory] = deps; - } - - return [...(deps['*'] || []), ...(deps[path.relative(depsDirectory, from)] || [])] - } - - function allowImport(from, to, mergedDeps) { - const fromDirectory = path.dirname(from); - const toDirectory = isDirectory(to) ? to : path.dirname(to); - if (to === toDirectory) - to = path.join(to, 'index.ts'); - if (fromDirectory === toDirectory) - return true; - - for (const dep of mergedDeps) { - if (dep === '***') - return true; - if (to === dep || toDirectory === dep) - return true; - if (dep.endsWith('**')) { - const parent = dep.substring(0, dep.length - 2); - if (to.startsWith(parent)) - return true; - } - } - return false; - } - - function allowExternalImport(importName, packageJSON) { - // Only external imports are relevant. Files in src/web are bundled via webpack. - if (importName.startsWith('.') || (importName.startsWith('@') && !importName.startsWith('@playwright/'))) - return true; - if (!packageJSON) - return false; - const match = importName.match(/(@[\w-]+\/)?([^/]+)/); - const dependency = match[1] ? match[1] + match[2] : match[2]; - if (builtins.has(dependency)) - return true; - return !!(packageJSON.dependencies || {})[dependency]; - } -} - -function listAllFiles(dir) { - const dirs = fs.readdirSync(dir, { withFileTypes: true }); - const result = []; - dirs.forEach(d => { - const res = path.resolve(dir, d.name); - if (d.isDirectory()) - result.push(...listAllFiles(res)); - else - result.push(res); - }); - return result; -} - -checkDeps().catch(e => { - console.error(e && e.stack ? e.stack : e); - process.exit(1); -}); - -function isDirectory(dir) { - return fs.existsSync(dir) && fs.statSync(dir).isDirectory(); -} diff --git a/utils/copyright.js b/utils/copyright.js deleted file mode 100644 index c391f5c..0000000 --- a/utils/copyright.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ diff --git a/utils/generate-links.js b/utils/generate-links.js deleted file mode 100644 index a54fe9c..0000000 --- a/utils/generate-links.js +++ /dev/null @@ -1,6 +0,0 @@ -const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["@playwright/mcp@latest"] }); -const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`; -// Github markdown does not allow linking to `vscode:` directly, so you can use our redirect: -const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`; - -console.log(urlForGithub); \ No newline at end of file diff --git a/utils/set-version.js b/utils/set-version.js deleted file mode 100644 index ec0e2da..0000000 --- a/utils/set-version.js +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env node -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// @ts-check - -import fs from 'fs'; -import path from 'path'; -import child_process from 'child_process'; -import { argv } from 'process'; - -const readJSON = async (filePath) => JSON.parse(await fs.promises.readFile(filePath, 'utf8')); -const writeJSON = async (filePath, json) => { - await fs.promises.writeFile(filePath, JSON.stringify(json, null, 2) + '\n'); -} - -async function updatePackageJSON(dir, version) { - const packageJSONPath = path.join(dir, 'package.json'); - const packageJSON = await readJSON(packageJSONPath); - console.log(`Updating ${packageJSONPath} to version ${version}`); - packageJSON.version = version; - await writeJSON(packageJSONPath, packageJSON); - - // Run npm i to update package-lock.json - child_process.execSync('npm i', { - cwd: dir - }); -} - -async function updateExtensionManifest(dir, version) { - const manifestPath = path.join(dir, 'manifest.json'); - const manifest = await readJSON(manifestPath); - console.log(`Updating ${manifestPath} to version ${version}`); - manifest.version = version; - await writeJSON(manifestPath, manifest); -} - -async function setVersion(version) { - if (version.startsWith('v')) - throw new Error('version must not start with "v"'); - - const packageRoot = path.join(__dirname, '..'); - await updatePackageJSON(packageRoot, version) - await updatePackageJSON(path.join(packageRoot, 'extension'), version) - await updateExtensionManifest(path.join(packageRoot, 'extension'), version) -} - -if (argv.length !== 3) { - console.error('Usage: set-version '); - process.exit(1); -} - -setVersion(argv[2]); \ No newline at end of file