From 8c65417508fefed8d6ebfc32f6d63d30518bd045 Mon Sep 17 00:00:00 2001 From: Morgan Joyce Date: Sat, 12 Jul 2025 04:31:58 -0500 Subject: [PATCH 1/2] feat(mcp): add vendored Playwright MCP server for browser automation - Vendored Microsoft's Playwright MCP server (commit 59f1d67) - Added wrapper script and setup script for Node.js environment - Integrated with MCP configuration and setup-all script - Cleaned unnecessary files (tests, examples, dev configs) - Added attribution README with vendoring details This is a deliberate choice to vendor rather than use as dependency for stability, customization, and self-containment. Closes #766 --- mcp/mcp.json | 5 + mcp/playwright-mcp-wrapper.sh | 30 + mcp/servers/playwright-mcp/.gitignore | 8 + mcp/servers/playwright-mcp/.npmignore | 7 + mcp/servers/playwright-mcp/LICENSE | 202 + mcp/servers/playwright-mcp/README.md | 17 + mcp/servers/playwright-mcp/cli.js | 18 + mcp/servers/playwright-mcp/config.d.ts | 128 + mcp/servers/playwright-mcp/eslint.config.mjs | 205 + mcp/servers/playwright-mcp/index.d.ts | 28 + mcp/servers/playwright-mcp/index.js | 19 + mcp/servers/playwright-mcp/package-lock.json | 4347 +++++++++++++++++ mcp/servers/playwright-mcp/package.json | 70 + .../src/browserContextFactory.ts | 266 + .../playwright-mcp/src/browserServer.ts | 197 + mcp/servers/playwright-mcp/src/cdpRelay.ts | 349 ++ mcp/servers/playwright-mcp/src/config.ts | 276 ++ mcp/servers/playwright-mcp/src/connection.ts | 98 + mcp/servers/playwright-mcp/src/context.ts | 351 ++ mcp/servers/playwright-mcp/src/fileUtils.ts | 37 + mcp/servers/playwright-mcp/src/httpServer.ts | 232 + mcp/servers/playwright-mcp/src/index.ts | 46 + mcp/servers/playwright-mcp/src/javascript.ts | 53 + .../playwright-mcp/src/manualPromise.ts | 127 + mcp/servers/playwright-mcp/src/package.ts | 22 + .../playwright-mcp/src/pageSnapshot.ts | 55 + mcp/servers/playwright-mcp/src/program.ts | 88 + .../playwright-mcp/src/resources/resource.ts | 36 + mcp/servers/playwright-mcp/src/server.ts | 59 + mcp/servers/playwright-mcp/src/tab.ts | 120 + mcp/servers/playwright-mcp/src/tools.ts | 66 + .../playwright-mcp/src/tools/common.ts | 78 + .../playwright-mcp/src/tools/console.ts | 47 + .../playwright-mcp/src/tools/dialogs.ts | 62 + mcp/servers/playwright-mcp/src/tools/files.ts | 59 + .../playwright-mcp/src/tools/install.ts | 63 + .../playwright-mcp/src/tools/keyboard.ts | 54 + .../playwright-mcp/src/tools/navigate.ts | 104 + .../playwright-mcp/src/tools/network.ts | 59 + mcp/servers/playwright-mcp/src/tools/pdf.ts | 58 + .../playwright-mcp/src/tools/screenshot.ts | 90 + .../playwright-mcp/src/tools/snapshot.ts | 234 + mcp/servers/playwright-mcp/src/tools/tabs.ts | 134 + .../playwright-mcp/src/tools/testing.ts | 67 + mcp/servers/playwright-mcp/src/tools/tool.ts | 68 + mcp/servers/playwright-mcp/src/tools/utils.ts | 92 + .../playwright-mcp/src/tools/vision.ts | 213 + mcp/servers/playwright-mcp/src/tools/wait.ts | 70 + mcp/servers/playwright-mcp/src/transport.ts | 149 + mcp/servers/playwright-mcp/tsconfig.json | 15 + mcp/setup-all-mcp-servers.sh | 2 + mcp/setup-playwright-mcp.sh | 57 + 52 files changed, 9337 insertions(+) create mode 100755 mcp/playwright-mcp-wrapper.sh create mode 100644 mcp/servers/playwright-mcp/.gitignore create mode 100644 mcp/servers/playwright-mcp/.npmignore create mode 100644 mcp/servers/playwright-mcp/LICENSE create mode 100644 mcp/servers/playwright-mcp/README.md create mode 100755 mcp/servers/playwright-mcp/cli.js create mode 100644 mcp/servers/playwright-mcp/config.d.ts create mode 100644 mcp/servers/playwright-mcp/eslint.config.mjs create mode 100644 mcp/servers/playwright-mcp/index.d.ts create mode 100755 mcp/servers/playwright-mcp/index.js create mode 100644 mcp/servers/playwright-mcp/package-lock.json create mode 100644 mcp/servers/playwright-mcp/package.json create mode 100644 mcp/servers/playwright-mcp/src/browserContextFactory.ts create mode 100644 mcp/servers/playwright-mcp/src/browserServer.ts create mode 100644 mcp/servers/playwright-mcp/src/cdpRelay.ts create mode 100644 mcp/servers/playwright-mcp/src/config.ts create mode 100644 mcp/servers/playwright-mcp/src/connection.ts create mode 100644 mcp/servers/playwright-mcp/src/context.ts create mode 100644 mcp/servers/playwright-mcp/src/fileUtils.ts create mode 100644 mcp/servers/playwright-mcp/src/httpServer.ts create mode 100644 mcp/servers/playwright-mcp/src/index.ts create mode 100644 mcp/servers/playwright-mcp/src/javascript.ts create mode 100644 mcp/servers/playwright-mcp/src/manualPromise.ts create mode 100644 mcp/servers/playwright-mcp/src/package.ts create mode 100644 mcp/servers/playwright-mcp/src/pageSnapshot.ts create mode 100644 mcp/servers/playwright-mcp/src/program.ts create mode 100644 mcp/servers/playwright-mcp/src/resources/resource.ts create mode 100644 mcp/servers/playwright-mcp/src/server.ts create mode 100644 mcp/servers/playwright-mcp/src/tab.ts create mode 100644 mcp/servers/playwright-mcp/src/tools.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/common.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/console.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/dialogs.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/files.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/install.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/keyboard.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/navigate.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/network.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/pdf.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/screenshot.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/snapshot.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/tabs.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/testing.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/tool.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/utils.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/vision.ts create mode 100644 mcp/servers/playwright-mcp/src/tools/wait.ts create mode 100644 mcp/servers/playwright-mcp/src/transport.ts create mode 100644 mcp/servers/playwright-mcp/tsconfig.json create mode 100755 mcp/setup-playwright-mcp.sh diff --git a/mcp/mcp.json b/mcp/mcp.json index aab529f0..39da1ba0 100644 --- a/mcp/mcp.json +++ b/mcp/mcp.json @@ -48,6 +48,11 @@ "env": { "FASTMCP_LOG_LEVEL": "ERROR" } + }, + "playwright": { + "command": "playwright-mcp-wrapper.sh", + "args": [], + "env": {} } } } \ No newline at end of file diff --git a/mcp/playwright-mcp-wrapper.sh b/mcp/playwright-mcp-wrapper.sh new file mode 100755 index 00000000..cdff2b12 --- /dev/null +++ b/mcp/playwright-mcp-wrapper.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# ========================================================= +# PLAYWRIGHT MCP WRAPPER SCRIPT +# ========================================================= +# PURPOSE: Runtime wrapper that executes the Playwright MCP server +# This script is called by the MCP system during normal operation +# Enhanced with error logging to address MCP client logging limitations +# ========================================================= + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source MCP logging utilities +source "$SCRIPT_DIR/utils/mcp-logging.sh" + +# Path to the Playwright MCP server directory +SERVER_DIR="$SCRIPT_DIR/servers/playwright-mcp" + +# Check if server directory exists +if [[ ! -d "$SERVER_DIR" ]]; then + mcp_log_error "PLAYWRIGHT" "Server directory not found: $SERVER_DIR" "Run setup-playwright-mcp.sh to install the Playwright MCP server" + exit 1 +fi + +# Check if node command is available +mcp_check_command "PLAYWRIGHT" "node" "Install Node.js: brew install node" + +# Run the server using Node.js +mcp_exec_with_logging "PLAYWRIGHT" "node" "$SERVER_DIR/index.js" "$@" \ No newline at end of file diff --git a/mcp/servers/playwright-mcp/.gitignore b/mcp/servers/playwright-mcp/.gitignore new file mode 100644 index 00000000..1089cefa --- /dev/null +++ b/mcp/servers/playwright-mcp/.gitignore @@ -0,0 +1,8 @@ +lib/ +node_modules/ +test-results/ +playwright-report/ +.vscode/mcp.json + +.idea +.DS_Store diff --git a/mcp/servers/playwright-mcp/.npmignore b/mcp/servers/playwright-mcp/.npmignore new file mode 100644 index 00000000..a2846ba6 --- /dev/null +++ b/mcp/servers/playwright-mcp/.npmignore @@ -0,0 +1,7 @@ +**/* +README.md +LICENSE +!lib/**/*.js +!cli.js +!index.* +!config.d.ts diff --git a/mcp/servers/playwright-mcp/LICENSE b/mcp/servers/playwright-mcp/LICENSE new file mode 100644 index 00000000..df112373 --- /dev/null +++ b/mcp/servers/playwright-mcp/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + 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/mcp/servers/playwright-mcp/README.md b/mcp/servers/playwright-mcp/README.md new file mode 100644 index 00000000..8d951c9f --- /dev/null +++ b/mcp/servers/playwright-mcp/README.md @@ -0,0 +1,17 @@ +# Playwright MCP Server (Vendored) + +This is a vendored copy of Microsoft's Playwright MCP server. +Original source: https://github.com/microsoft/playwright-mcp + +Vendored on: 2025-01-12 +Original commit: 59f1d67 + +This copy is maintained independently and does not track upstream changes. + +## Usage + +This server provides browser automation capabilities through the MCP protocol. + +## License + +See LICENSE file for original Microsoft license terms. \ No newline at end of file diff --git a/mcp/servers/playwright-mcp/cli.js b/mcp/servers/playwright-mcp/cli.js new file mode 100755 index 00000000..bbda09e8 --- /dev/null +++ b/mcp/servers/playwright-mcp/cli.js @@ -0,0 +1,18 @@ +#!/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. + */ + +import './lib/program.js'; diff --git a/mcp/servers/playwright-mcp/config.d.ts b/mcp/servers/playwright-mcp/config.d.ts new file mode 100644 index 00000000..a9359187 --- /dev/null +++ b/mcp/servers/playwright-mcp/config.d.ts @@ -0,0 +1,128 @@ +/** + * 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 * as playwright from 'playwright'; + +export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install' | 'testing'; + +export type Config = { + /** + * The browser to use. + */ + browser?: { + /** + * Use browser agent (experimental). + */ + browserAgent?: string; + + /** + * The type of browser to use. + */ + browserName?: 'chromium' | 'firefox' | 'webkit'; + + /** + * Keep the browser profile in memory, do not save it to disk. + */ + isolated?: boolean; + + /** + * Path to a user data directory for browser profile persistence. + * Temporary directory is created by default. + */ + userDataDir?: string; + + /** + * Launch options passed to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context + * + * This is useful for settings options like `channel`, `headless`, `executablePath`, etc. + */ + launchOptions?: playwright.LaunchOptions; + + /** + * Context options for the browser context. + * + * This is useful for settings options like `viewport`. + */ + contextOptions?: playwright.BrowserContextOptions; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + + /** + * Remote endpoint to connect to an existing Playwright server. + */ + remoteEndpoint?: string; + }, + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'tabs': Tab management features. + * - 'pdf': PDF generation and manipulation. + * - 'history': Browser history access. + * - 'wait': Wait and timing utilities. + * - 'files': File upload/download support. + * - 'install': Browser installation utilities. + */ + capabilities?: ToolCapability[]; + + /** + * Run server that uses screenshots (Aria snapshots are used by default). + */ + vision?: boolean; + + /** + * Whether to save the Playwright trace of the session into the output directory. + */ + saveTrace?: boolean; + + /** + * The directory to save output files. + */ + outputDir?: string; + + network?: { + /** + * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + */ + allowedOrigins?: string[]; + + /** + * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + */ + blockedOrigins?: string[]; + }; + + /** + * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them. + */ + imageResponses?: 'allow' | 'omit' | 'auto'; +}; diff --git a/mcp/servers/playwright-mcp/eslint.config.mjs b/mcp/servers/playwright-mcp/eslint.config.mjs new file mode 100644 index 00000000..eda1efff --- /dev/null +++ b/mcp/servers/playwright-mcp/eslint.config.mjs @@ -0,0 +1,205 @@ +/** + * 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 typescriptEslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import notice from "eslint-plugin-notice"; +import path from "path"; +import { fileURLToPath } from "url"; +import stylistic from "@stylistic/eslint-plugin"; +import importRules from "eslint-plugin-import"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const plugins = { + "@stylistic": stylistic, + "@typescript-eslint": typescriptEslint, + notice, + import: importRules, +}; + +export const baseRules = { + "import/extensions": ["error", "ignorePackages", {ts: "always"}], + "@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(fileURLToPath(import.meta.url), "..", "tsconfig.all.json"), + } +}; + +export default [ + { + ignores: ["**/*.js"], + }, + { + files: ["**/*.ts", "**/*.tsx"], + plugins, + languageOptions, + rules: baseRules, + }, +]; diff --git a/mcp/servers/playwright-mcp/index.d.ts b/mcp/servers/playwright-mcp/index.d.ts new file mode 100644 index 00000000..9ea8bcc8 --- /dev/null +++ b/mcp/servers/playwright-mcp/index.d.ts @@ -0,0 +1,28 @@ +#!/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. + */ + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { Config } from './config.js'; +import type { BrowserContext } from 'playwright'; + +export type Connection = { + server: Server; + close(): Promise; +}; + +export declare function createConnection(config?: Config, contextGetter?: () => Promise): Promise; +export {}; diff --git a/mcp/servers/playwright-mcp/index.js b/mcp/servers/playwright-mcp/index.js new file mode 100755 index 00000000..69f7ae28 --- /dev/null +++ b/mcp/servers/playwright-mcp/index.js @@ -0,0 +1,19 @@ +#!/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. + */ + +import { createConnection } from './lib/index.js'; +export { createConnection }; diff --git a/mcp/servers/playwright-mcp/package-lock.json b/mcp/servers/playwright-mcp/package-lock.json new file mode 100644 index 00000000..04df3b3b --- /dev/null +++ b/mcp/servers/playwright-mcp/package-lock.json @@ -0,0 +1,4347 @@ +{ + "name": "@playwright/mcp", + "version": "0.0.29", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@playwright/mcp", + "version": "0.0.29", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.0", + "commander": "^13.1.0", + "debug": "^4.4.1", + "mime": "^4.0.7", + "playwright": "1.53.0", + "ws": "^8.18.1", + "zod-to-json-schema": "^3.24.4" + }, + "bin": { + "mcp-server-playwright": "cli.js" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.19.0", + "@playwright/test": "1.53.0", + "@stylistic/eslint-plugin": "^3.0.1", + "@types/chrome": "^0.0.315", + "@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", + "eslint": "^9.19.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-notice": "^1.0.0", + "typescript": "^5.8.2" + }, + "engines": { + "node": ">=18" + } + }, + "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.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "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.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "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.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "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.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.12.0", + "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.11.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", + "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "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.53.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", + "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", + "dev": true, + "dependencies": { + "playwright": "1.53.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "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/chrome": { + "version": "0.0.315", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.315.tgz", + "integrity": "sha512-Oy1dYWkr6BCmgwBtOngLByCHstQ3whltZg7/7lubgIZEYvKobDneqplgc6LKERNRBwckFviV4UU5AZZNUFrJ4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "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==", + "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.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "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", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "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", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-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", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/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.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@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.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.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.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "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.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/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "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.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/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", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", + "integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "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", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 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", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "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", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "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", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "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", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "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", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "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", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "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" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/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", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "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==", + "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" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", + "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", + "dependencies": { + "playwright-core": "1.53.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", + "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "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", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "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", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "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", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/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", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/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", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "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", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "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", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "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==", + "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==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/mcp/servers/playwright-mcp/package.json b/mcp/servers/playwright-mcp/package.json new file mode 100644 index 00000000..212db1c0 --- /dev/null +++ b/mcp/servers/playwright-mcp/package.json @@ -0,0 +1,70 @@ +{ + "name": "@playwright/mcp", + "version": "0.0.29", + "description": "Playwright Tools for MCP", + "type": "module", + "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", + "build:extension": "tsc --project extension", + "lint": "npm run update-readme && eslint . && tsc --noEmit", + "update-readme": "node utils/update-readme.js", + "watch": "tsc --watch", + "watch:extension": "tsc --watch --project extension", + "test": "playwright test", + "ctest": "playwright test --project=chrome", + "ftest": "playwright test --project=firefox", + "wtest": "playwright test --project=webkit", + "etest": "playwright test --project=chromium-extension", + "run-server": "node lib/browserServer.js", + "clean": "rm -rf lib && rm -rf extension/lib", + "npm-publish": "npm run clean && npm run build && npm run test && npm publish" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.0", + "commander": "^13.1.0", + "debug": "^4.4.1", + "mime": "^4.0.7", + "playwright": "1.53.0", + "ws": "^8.18.1", + "zod-to-json-schema": "^3.24.4" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.19.0", + "@playwright/test": "1.53.0", + "@stylistic/eslint-plugin": "^3.0.1", + "@types/chrome": "^0.0.315", + "@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", + "eslint": "^9.19.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-notice": "^1.0.0", + "typescript": "^5.8.2" + }, + "bin": { + "mcp-server-playwright": "cli.js" + } +} diff --git a/mcp/servers/playwright-mcp/src/browserContextFactory.ts b/mcp/servers/playwright-mcp/src/browserContextFactory.ts new file mode 100644 index 00000000..f14cd7d9 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/browserContextFactory.ts @@ -0,0 +1,266 @@ +/** + * 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 net from 'node:net'; +import path from 'node:path'; +import os from 'node:os'; + +import debug from 'debug'; +import * as playwright from 'playwright'; +import { userDataDir } from './fileUtils.js'; + +import type { FullConfig } from './config.js'; +import type { BrowserInfo, LaunchBrowserRequest } from './browserServer.js'; + +const testDebug = debug('pw:mcp:test'); + +export function contextFactory(browserConfig: FullConfig['browser']): BrowserContextFactory { + if (browserConfig.remoteEndpoint) + return new RemoteContextFactory(browserConfig); + if (browserConfig.cdpEndpoint) + return new CdpContextFactory(browserConfig); + if (browserConfig.isolated) + return new IsolatedContextFactory(browserConfig); + if (browserConfig.browserAgent) + return new BrowserServerContextFactory(browserConfig); + return new PersistentContextFactory(browserConfig); +} + +export interface BrowserContextFactory { + createContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }>; +} + +class BaseContextFactory implements BrowserContextFactory { + readonly browserConfig: FullConfig['browser']; + protected _browserPromise: Promise | undefined; + readonly name: string; + + constructor(name: string, browserConfig: FullConfig['browser']) { + this.name = name; + this.browserConfig = browserConfig; + } + + protected async _obtainBrowser(): Promise { + if (this._browserPromise) + return this._browserPromise; + testDebug(`obtain browser (${this.name})`); + this._browserPromise = this._doObtainBrowser(); + void this._browserPromise.then(browser => { + browser.on('disconnected', () => { + this._browserPromise = undefined; + }); + }).catch(() => { + this._browserPromise = undefined; + }); + return this._browserPromise; + } + + protected async _doObtainBrowser(): Promise { + throw new Error('Not implemented'); + } + + async createContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + testDebug(`create browser context (${this.name})`); + const browser = await this._obtainBrowser(); + 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.name})`); + if (browser.contexts().length === 1) + this._browserPromise = undefined; + await browserContext.close().catch(() => {}); + if (browser.contexts().length === 0) { + testDebug(`close browser (${this.name})`); + await browser.close().catch(() => {}); + } + } +} + +class IsolatedContextFactory extends BaseContextFactory { + constructor(browserConfig: FullConfig['browser']) { + super('isolated', browserConfig); + } + + protected override async _doObtainBrowser(): Promise { + await injectCdpPort(this.browserConfig); + const browserType = playwright[this.browserConfig.browserName]; + return browserType.launch({ + ...this.browserConfig.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.browserConfig.contextOptions); + } +} + +class CdpContextFactory extends BaseContextFactory { + constructor(browserConfig: FullConfig['browser']) { + super('cdp', browserConfig); + } + + protected override async _doObtainBrowser(): Promise { + return playwright.chromium.connectOverCDP(this.browserConfig.cdpEndpoint!); + } + + protected override async _doCreateContext(browser: playwright.Browser): Promise { + return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0]; + } +} + +class RemoteContextFactory extends BaseContextFactory { + constructor(browserConfig: FullConfig['browser']) { + super('remote', browserConfig); + } + + protected override async _doObtainBrowser(): Promise { + const url = new URL(this.browserConfig.remoteEndpoint!); + url.searchParams.set('browser', this.browserConfig.browserName); + if (this.browserConfig.launchOptions) + url.searchParams.set('launch-options', JSON.stringify(this.browserConfig.launchOptions)); + return playwright[this.browserConfig.browserName].connect(String(url)); + } + + protected override async _doCreateContext(browser: playwright.Browser): Promise { + return browser.newContext(); + } +} + +class PersistentContextFactory implements BrowserContextFactory { + readonly browserConfig: FullConfig['browser']; + private _userDataDirs = new Set(); + + constructor(browserConfig: FullConfig['browser']) { + this.browserConfig = browserConfig; + } + + async createContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + await injectCdpPort(this.browserConfig); + testDebug('create browser context (persistent)'); + const userDataDir = this.browserConfig.userDataDir ?? await this._createUserDataDir(); + + this._userDataDirs.add(userDataDir); + testDebug('lock user data dir', userDataDir); + + const browserType = playwright[this.browserConfig.browserName]; + for (let i = 0; i < 5; i++) { + try { + const browserContext = await browserType.launchPersistentContext(userDataDir, { + ...this.browserConfig.launchOptions, + ...this.browserConfig.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() { + let cacheDirectory: string; + if (process.platform === 'linux') + cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); + else if (process.platform === 'darwin') + cacheDirectory = path.join(os.homedir(), 'Library', 'Caches'); + else if (process.platform === 'win32') + cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); + else + throw new Error('Unsupported platform: ' + process.platform); + const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${this.browserConfig.launchOptions?.channel ?? this.browserConfig?.browserName}-profile`); + await fs.promises.mkdir(result, { recursive: true }); + return result; + } +} + +export class BrowserServerContextFactory extends BaseContextFactory { + constructor(browserConfig: FullConfig['browser']) { + super('persistent', browserConfig); + } + + protected override async _doObtainBrowser(): Promise { + const response = await fetch(new URL(`/json/launch`, this.browserConfig.browserAgent), { + method: 'POST', + body: JSON.stringify({ + browserType: this.browserConfig.browserName, + userDataDir: this.browserConfig.userDataDir ?? await this._createUserDataDir(), + launchOptions: this.browserConfig.launchOptions, + contextOptions: this.browserConfig.contextOptions, + } as LaunchBrowserRequest), + }); + const info = await response.json() as BrowserInfo; + if (info.error) + throw new Error(info.error); + return await playwright.chromium.connectOverCDP(`http://localhost:${info.cdpPort}/`); + } + + protected override async _doCreateContext(browser: playwright.Browser): Promise { + return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0]; + } + + private async _createUserDataDir() { + const dir = await userDataDir(this.browserConfig); + await fs.promises.mkdir(dir, { recursive: true }); + return dir; + } +} + +async function injectCdpPort(browserConfig: FullConfig['browser']) { + if (browserConfig.browserName === 'chromium') + (browserConfig.launchOptions as any).cdpPort = await findFreePort(); +} + +async function findFreePort() { + 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); + }); +} diff --git a/mcp/servers/playwright-mcp/src/browserServer.ts b/mcp/servers/playwright-mcp/src/browserServer.ts new file mode 100644 index 00000000..85c908d7 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/browserServer.ts @@ -0,0 +1,197 @@ +/** + * 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. + */ + +/* eslint-disable no-console */ + +import net from 'net'; + +import { program } from 'commander'; +import playwright from 'playwright'; + +import { HttpServer } from './httpServer.js'; +import { packageJSON } from './package.js'; + +import type http from 'http'; + +export type LaunchBrowserRequest = { + browserType: string; + userDataDir: string; + launchOptions: playwright.LaunchOptions; + contextOptions: playwright.BrowserContextOptions; +}; + +export type BrowserInfo = { + browserType: string; + userDataDir: string; + cdpPort: number; + launchOptions: playwright.LaunchOptions; + contextOptions: playwright.BrowserContextOptions; + error?: string; +}; + +type BrowserEntry = { + browser?: playwright.Browser; + info: BrowserInfo; +}; + +class BrowserServer { + private _server = new HttpServer(); + private _entries: BrowserEntry[] = []; + + constructor() { + this._setupExitHandler(); + } + + async start(port: number) { + await this._server.start({ port }); + this._server.routePath('/json/list', (req, res) => { + this._handleJsonList(res); + }); + this._server.routePath('/json/launch', async (req, res) => { + void this._handleLaunchBrowser(req, res).catch(e => console.error(e)); + }); + this._setEntries([]); + } + + private _handleJsonList(res: http.ServerResponse) { + const list = this._entries.map(browser => browser.info); + res.end(JSON.stringify(list)); + } + + private async _handleLaunchBrowser(req: http.IncomingMessage, res: http.ServerResponse) { + const request = await readBody(req); + let info = this._entries.map(entry => entry.info).find(info => info.userDataDir === request.userDataDir); + if (!info || info.error) + info = await this._newBrowser(request); + res.end(JSON.stringify(info)); + } + + private async _newBrowser(request: LaunchBrowserRequest): Promise { + const cdpPort = await findFreePort(); + (request.launchOptions as any).cdpPort = cdpPort; + const info: BrowserInfo = { + browserType: request.browserType, + userDataDir: request.userDataDir, + cdpPort, + launchOptions: request.launchOptions, + contextOptions: request.contextOptions, + }; + + const browserType = playwright[request.browserType as 'chromium' | 'firefox' | 'webkit']; + const { browser, error } = await browserType.launchPersistentContext(request.userDataDir, { + ...request.launchOptions, + ...request.contextOptions, + handleSIGINT: false, + handleSIGTERM: false, + }).then(context => { + return { browser: context.browser()!, error: undefined }; + }).catch(error => { + return { browser: undefined, error: error.message }; + }); + this._setEntries([...this._entries, { + browser, + info: { + browserType: request.browserType, + userDataDir: request.userDataDir, + cdpPort, + launchOptions: request.launchOptions, + contextOptions: request.contextOptions, + error, + }, + }]); + browser?.on('disconnected', () => { + this._setEntries(this._entries.filter(entry => entry.browser !== browser)); + }); + return info; + } + + private _updateReport() { + // Clear the current line and move cursor to top of screen + process.stdout.write('\x1b[2J\x1b[H'); + process.stdout.write(`Playwright Browser Server v${packageJSON.version}\n`); + process.stdout.write(`Listening on ${this._server.urlPrefix('human-readable')}\n\n`); + + if (this._entries.length === 0) { + process.stdout.write('No browsers currently running\n'); + return; + } + + process.stdout.write('Running browsers:\n'); + for (const entry of this._entries) { + const status = entry.browser ? 'running' : 'error'; + const statusColor = entry.browser ? '\x1b[32m' : '\x1b[31m'; // green for running, red for error + process.stdout.write(`${statusColor}${entry.info.browserType}\x1b[0m (${entry.info.userDataDir}) - ${statusColor}${status}\x1b[0m\n`); + if (entry.info.error) + process.stdout.write(` Error: ${entry.info.error}\n`); + } + + } + + private _setEntries(entries: BrowserEntry[]) { + this._entries = entries; + this._updateReport(); + } + + private _setupExitHandler() { + let isExiting = false; + const handleExit = async () => { + if (isExiting) + return; + isExiting = true; + setTimeout(() => process.exit(0), 15000); + for (const entry of this._entries) + await entry.browser?.close().catch(() => {}); + process.exit(0); + }; + + process.stdin.on('close', handleExit); + process.on('SIGINT', handleExit); + process.on('SIGTERM', handleExit); + } +} + +program + .name('browser-agent') + .option('-p, --port ', 'Port to listen on', '9224') + .action(async options => { + await main(options); + }); + +void program.parseAsync(process.argv); + +async function main(options: { port: string }) { + const server = new BrowserServer(); + await server.start(+options.port); +} + +function readBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => chunks.push(chunk)); + req.on('end', () => resolve(JSON.parse(Buffer.concat(chunks).toString()))); + }); +} + +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); + }); +} diff --git a/mcp/servers/playwright-mcp/src/cdpRelay.ts b/mcp/servers/playwright-mcp/src/cdpRelay.ts new file mode 100644 index 00000000..a606733f --- /dev/null +++ b/mcp/servers/playwright-mcp/src/cdpRelay.ts @@ -0,0 +1,349 @@ +/** + * 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. + */ + +/** + * Bridge Server - Standalone WebSocket server that bridges Playwright MCP and Chrome Extension + * + * Endpoints: + * - /cdp - Full CDP interface for Playwright MCP + * - /extension - Extension connection for chrome.debugger forwarding + */ + +/* eslint-disable no-console */ + +import { WebSocket, WebSocketServer } from 'ws'; +import http from 'node:http'; +import debug from 'debug'; +import { httpAddressToString } from './transport.js'; + +const debugLogger = debug('pw:mcp:relay'); + +const CDP_PATH = '/cdp'; +const EXTENSION_PATH = '/extension'; + +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 _wss: WebSocketServer; + private _playwrightSocket: WebSocket | null = null; + private _extensionConnection: ExtensionConnection | null = null; + private _connectionInfo: { + targetInfo: any; + // Page sessionId that should be used by this connection. + sessionId: string; + } | undefined; + + constructor(server: http.Server) { + this._wss = new WebSocketServer({ server }); + this._wss.on('connection', this._onConnection.bind(this)); + } + + stop(): void { + this._playwrightSocket?.close(); + this._extensionConnection?.close(); + } + + 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 === CDP_PATH) { + this._handlePlaywrightConnection(ws); + } else if (url.pathname === EXTENSION_PATH) { + this._handleExtensionConnection(ws); + } else { + debugLogger(`Invalid path: ${url.pathname}`); + ws.close(4004, 'Invalid path'); + } + } + + /** + * Handle Playwright MCP connection - provides full CDP interface + */ + private _handlePlaywrightConnection(ws: WebSocket): void { + if (this._playwrightSocket?.readyState === WebSocket.OPEN) { + debugLogger('Closing previous Playwright connection'); + this._playwrightSocket.close(1000, 'New connection established'); + } + this._playwrightSocket = ws; + debugLogger('Playwright MCP connected'); + ws.on('message', async data => { + try { + const message = JSON.parse(data.toString()); + await this._handlePlaywrightMessage(message); + } catch (error) { + debugLogger('Error parsing Playwright message:', error); + } + }); + ws.on('close', () => { + if (this._playwrightSocket === ws) { + void this._detachDebugger(); + this._playwrightSocket = null; + } + debugLogger('Playwright MCP disconnected'); + }); + ws.on('error', error => { + debugLogger('Playwright WebSocket error:', error); + }); + } + + private async _detachDebugger() { + this._connectionInfo = undefined; + await this._extensionConnection?.send('detachFromTab', {}); + } + + private _handleExtensionConnection(ws: WebSocket): void { + if (this._extensionConnection) + this._extensionConnection.close('New connection established'); + this._extensionConnection = new ExtensionConnection(ws); + this._extensionConnection.onclose = c => { + if (this._extensionConnection === c) + this._extensionConnection = null; + }; + this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this); + } + + private _handleExtensionMessage(method: string, params: any) { + switch (method) { + case 'forwardCDPEvent': + this._sendToPlaywright({ + sessionId: params.sessionId, + method: params.method, + params: params.params + }); + break; + case 'detachedFromTab': + debugLogger('← Debugger detached from tab:', params); + this._connectionInfo = undefined; + this._extensionConnection?.close(); + this._extensionConnection = null; + break; + } + } + + private async _handlePlaywrightMessage(message: CDPCommand): Promise { + debugLogger('← Playwright:', `${message.method} (id=${message.id})`); + if (!this._extensionConnection) { + debugLogger('Extension not connected, sending error to Playwright'); + this._sendToPlaywright({ + id: message.id, + error: { message: 'Extension not connected' } + }); + return; + } + if (await this._interceptCDPCommand(message)) + return; + await this._forwardToExtension(message); + } + + private async _interceptCDPCommand(message: CDPCommand): Promise { + switch (message.method) { + case 'Browser.getVersion': { + this._sendToPlaywright({ + id: message.id, + result: { + protocolVersion: '1.3', + product: 'Chrome/Extension-Bridge', + userAgent: 'CDP-Bridge-Server/1.0.0', + } + }); + return true; + } + case 'Browser.setDownloadBehavior': { + this._sendToPlaywright({ + id: message.id + }); + return true; + } + case 'Target.setAutoAttach': { + // Simulate auto-attach behavior with real target info + if (!message.sessionId) { + this._connectionInfo = await this._extensionConnection!.send('attachToTab'); + debugLogger('Simulating auto-attach for target:', message); + this._sendToPlaywright({ + method: 'Target.attachedToTarget', + params: { + sessionId: this._connectionInfo!.sessionId, + targetInfo: { + ...this._connectionInfo!.targetInfo, + attached: true, + }, + waitingForDebugger: false + } + }); + this._sendToPlaywright({ + id: message.id + }); + } else { + await this._forwardToExtension(message); + } + return true; + } + case 'Target.getTargetInfo': { + debugLogger('Target.getTargetInfo', message); + this._sendToPlaywright({ + id: message.id, + result: this._connectionInfo?.targetInfo + }); + return true; + } + } + return false; + } + + private async _forwardToExtension(message: CDPCommand): Promise { + try { + if (!this._extensionConnection) + throw new Error('Extension not connected'); + const { id, sessionId, method, params } = message; + const result = await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params }); + this._sendToPlaywright({ id, sessionId, result }); + } catch (e) { + debugLogger('Error in the extension:', e); + this._sendToPlaywright({ + id: message.id, + sessionId: message.sessionId, + error: { message: (e as Error).message } + }); + } + } + + private _sendToPlaywright(message: CDPResponse): void { + debugLogger('→ Playwright:', `${message.method ?? `response(id=${message.id})`}`); + this._playwrightSocket?.send(JSON.stringify(message)); + } +} + +export async function startCDPRelayServer(httpServer: http.Server) { + const wsAddress = httpAddressToString(httpServer.address()).replace(/^http/, 'ws'); + const cdpRelayServer = new CDPRelayServer(httpServer); + process.on('exit', () => cdpRelayServer.stop()); + console.error(`CDP relay server started on ${wsAddress}${EXTENSION_PATH} - Connect to it using the browser extension.`); + const cdpEndpoint = `${wsAddress}${CDP_PATH}`; + return cdpEndpoint; +} + +// CLI usage +if (import.meta.url === `file://${process.argv[1]}`) { + const port = parseInt(process.argv[2], 10) || 9223; + const httpServer = http.createServer(); + await new Promise(resolve => httpServer.listen(port, resolve)); + const server = new CDPRelayServer(httpServer); + + console.error(`CDP Bridge Server listening on ws://localhost:${port}`); + console.error(`- Playwright MCP: ws://localhost:${port}${CDP_PATH}`); + console.error(`- Extension: ws://localhost:${port}${EXTENSION_PATH}`); + + process.on('SIGINT', () => { + debugLogger('\nShutting down bridge server...'); + server.stop(); + process.exit(0); + }); +} + +class ExtensionConnection { + private readonly _ws: WebSocket; + private readonly _callbacks = new Map void, reject: (e: Error) => void }>(); + private _lastId = 0; + + onmessage?: (method: string, params: any) => void; + onclose?: (self: ExtensionConnection) => 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: string, params?: any, sessionId?: string): Promise { + if (this._ws.readyState !== WebSocket.OPEN) + throw new Error('WebSocket closed'); + const id = ++this._lastId; + this._ws.send(JSON.stringify({ id, method, params, sessionId })); + return new Promise((resolve, reject) => { + this._callbacks.set(id, { resolve, reject }); + }); + } + + close(message?: string) { + debugLogger('closing extension connection:', message); + this._ws.close(1000, message ?? 'Connection closed'); + this.onclose?.(this); + } + + 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: any) { + if (object.id && this._callbacks.has(object.id)) { + const callback = this._callbacks.get(object.id)!; + this._callbacks.delete(object.id); + if (object.error) + callback.reject(new Error(object.error.message)); + else + callback.resolve(object.result); + } else if (object.id) { + debugLogger('← Extension: unexpected response', object); + } else { + this.onmessage?.(object.method, object.params); + } + } + + private _onClose(event: WebSocket.CloseEvent) { + debugLogger(` code=${event.code} reason=${event.reason}`); + this._dispose(); + } + + 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/mcp/servers/playwright-mcp/src/config.ts b/mcp/servers/playwright-mcp/src/config.ts new file mode 100644 index 00000000..1c4a6f83 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/config.ts @@ -0,0 +1,276 @@ +/** + * 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 as PublicConfig, ToolCapability } from '../config.js'; +import type { BrowserContextOptions, LaunchOptions } from 'playwright'; +import { sanitizeForFilePath } from './tools/utils.js'; + +type Config = PublicConfig & { + /** + * TODO: Move to PublicConfig once we are ready to release this feature. + * Run server that is able to connect to the 'Playwright MCP' Chrome extension. + */ + extension?: boolean; +}; + +export type CLIOptions = { + allowedOrigins?: string[]; + blockedOrigins?: string[]; + blockServiceWorkers?: boolean; + browser?: string; + browserAgent?: string; + caps?: string; + cdpEndpoint?: string; + config?: string; + device?: string; + executablePath?: string; + headless?: boolean; + host?: string; + ignoreHttpsErrors?: boolean; + isolated?: boolean; + imageResponses?: 'allow' | 'omit' | 'auto'; + sandbox: boolean; + outputDir?: string; + port?: number; + proxyBypass?: string; + proxyServer?: string; + saveTrace?: boolean; + storageState?: string; + userAgent?: string; + userDataDir?: string; + viewportSize?: string; + vision?: boolean; + extension?: boolean; +}; + +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: {}, + outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())), +}; + +type BrowserUserConfig = NonNullable; + +export type FullConfig = Config & { + browser: Omit & { + browserName: 'chromium' | 'firefox' | 'webkit'; + launchOptions: NonNullable; + contextOptions: NonNullable; + }, + network: NonNullable, + outputDir: string; + 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 cliOverrides = await configFromCLIOptions(cliOptions); + const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides); + // Derive artifact output directory from config.outputDir + if (result.saveTrace) + result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces'); + return result; +} + +export function validateConfig(config: Config) { + if (config.extension) { + if (config.browser?.browserName !== 'chromium') + throw new Error('Extension mode is only supported for Chromium browsers.'); + } +} + +export async function configFromCLIOptions(cliOptions: CLIOptions): Promise { + 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) + 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.'); + if (cliOptions.device && cliOptions.extension) + throw new Error('Device emulation is not supported with extension mode.'); + + // 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: { + browserAgent: cliOptions.browserAgent ?? process.env.PW_BROWSER_AGENT, + browserName, + isolated: cliOptions.isolated, + userDataDir: cliOptions.userDataDir, + launchOptions, + contextOptions, + cdpEndpoint: cliOptions.cdpEndpoint, + }, + server: { + port: cliOptions.port, + host: cliOptions.host, + }, + capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability), + vision: !!cliOptions.vision, + extension: !!cliOptions.extension, + network: { + allowedOrigins: cliOptions.allowedOrigins, + blockedOrigins: cliOptions.blockedOrigins, + }, + saveTrace: cliOptions.saveTrace, + outputDir: cliOptions.outputDir, + imageResponses: cliOptions.imageResponses, + }; + + return result; +} + +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, name: string): Promise { + await fs.promises.mkdir(config.outputDir, { recursive: true }); + const fileName = sanitizeForFilePath(name); + return path.join(config.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; +} diff --git a/mcp/servers/playwright-mcp/src/connection.ts b/mcp/servers/playwright-mcp/src/connection.ts new file mode 100644 index 00000000..eff554d7 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/connection.ts @@ -0,0 +1,98 @@ +/** + * 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 { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'; +import { CallToolRequestSchema, ListToolsRequestSchema, Tool as McpTool } from '@modelcontextprotocol/sdk/types.js'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { Context } from './context.js'; +import { snapshotTools, visionTools } from './tools.js'; +import { packageJSON } from './package.js'; + +import { FullConfig, validateConfig } from './config.js'; + +import type { BrowserContextFactory } from './browserContextFactory.js'; + +export function createConnection(config: FullConfig, browserContextFactory: BrowserContextFactory): Connection { + const allTools = config.vision ? visionTools : snapshotTools; + const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability)); + validateConfig(config); + const context = new Context(tools, config, browserContextFactory); + const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, { + capabilities: { + tools: {}, + } + }); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: tools.map(tool => ({ + name: tool.schema.name, + description: tool.schema.description, + inputSchema: zodToJsonSchema(tool.schema.inputSchema), + annotations: { + title: tool.schema.title, + readOnlyHint: tool.schema.type === 'readOnly', + destructiveHint: tool.schema.type === 'destructive', + openWorldHint: true, + }, + })) as McpTool[], + }; + }); + + server.setRequestHandler(CallToolRequestSchema, async request => { + const errorResult = (...messages: string[]) => ({ + content: [{ type: 'text', text: messages.join('\n') }], + isError: true, + }); + const tool = tools.find(tool => tool.schema.name === request.params.name); + if (!tool) + return errorResult(`Tool "${request.params.name}" not found`); + + + const modalStates = context.modalStates().map(state => state.type); + if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState)) + return errorResult(`The tool "${request.params.name}" can only be used when there is related modal state present.`, ...context.modalStatesMarkdown()); + if (!tool.clearsModalState && modalStates.length) + return errorResult(`Tool "${request.params.name}" does not handle the modal state.`, ...context.modalStatesMarkdown()); + + try { + return await context.run(tool, request.params.arguments); + } catch (error) { + return errorResult(String(error)); + } + }); + + return new Connection(server, context); +} + +export class Connection { + readonly server: McpServer; + readonly context: Context; + + constructor(server: McpServer, context: Context) { + this.server = server; + this.context = context; + this.server.oninitialized = () => { + this.context.clientVersion = this.server.getClientVersion(); + }; + } + + async close() { + await this.server.close(); + await this.context.close(); + } +} diff --git a/mcp/servers/playwright-mcp/src/context.ts b/mcp/servers/playwright-mcp/src/context.ts new file mode 100644 index 00000000..ecf66b95 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/context.ts @@ -0,0 +1,351 @@ +/** + * 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 { callOnPageNoTrace, waitForCompletion } from './tools/utils.js'; +import { ManualPromise } from './manualPromise.js'; +import { Tab } from './tab.js'; +import { outputFile } from './config.js'; + +import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; +import type { ModalState, Tool, ToolActionResult } from './tools/tool.js'; +import type { FullConfig } from './config.js'; +import type { BrowserContextFactory } from './browserContextFactory.js'; + +type PendingAction = { + dialogShown: ManualPromise; +}; + +const testDebug = debug('pw:mcp:test'); + +export class Context { + readonly tools: Tool[]; + readonly config: FullConfig; + private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; + private _browserContextFactory: BrowserContextFactory; + private _tabs: Tab[] = []; + private _currentTab: Tab | undefined; + private _modalStates: (ModalState & { tab: Tab })[] = []; + private _pendingAction: PendingAction | undefined; + private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = []; + clientVersion: { name: string; version: string; } | undefined; + + constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory) { + this.tools = tools; + this.config = config; + this._browserContextFactory = browserContextFactory; + testDebug('create context'); + } + + clientSupportsImages(): boolean { + if (this.config.imageResponses === 'allow') + return true; + if (this.config.imageResponses === 'omit') + return false; + return !this.clientVersion?.name.includes('cursor'); + } + + modalStates(): ModalState[] { + return this._modalStates; + } + + setModalState(modalState: ModalState, inTab: Tab) { + this._modalStates.push({ ...modalState, tab: inTab }); + } + + clearModalState(modalState: ModalState) { + this._modalStates = this._modalStates.filter(state => state !== modalState); + } + + modalStatesMarkdown(): string[] { + const result: string[] = ['### Modal state']; + if (this._modalStates.length === 0) + result.push('- There is no modal state present'); + for (const state of this._modalStates) { + const tool = this.tools.find(tool => tool.clearsModalState === state.type); + result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`); + } + return result; + } + + tabs(): Tab[] { + return this._tabs; + } + + currentTabOrDie(): Tab { + if (!this._currentTab) + throw new Error('No current snapshot available. Capture a snapshot or navigate to a new location 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) { + this._currentTab = this._tabs[index - 1]; + await this._currentTab.page.bringToFront(); + } + + async ensureTab(): Promise { + const { browserContext } = await this._ensureBrowserContext(); + if (!this._currentTab) + await browserContext.newPage(); + return this._currentTab!; + } + + async listTabsMarkdown(): Promise { + if (!this._tabs.length) + return '### No tabs open'; + const lines: string[] = ['### Open tabs']; + for (let i = 0; i < this._tabs.length; i++) { + const tab = this._tabs[i]; + const title = await tab.title(); + const url = tab.page.url(); + const current = tab === this._currentTab ? ' (current)' : ''; + lines.push(`- ${i + 1}:${current} [${title}] (${url})`); + } + return lines.join('\n'); + } + + async closeTab(index: number | undefined) { + const tab = index === undefined ? this._currentTab : this._tabs[index - 1]; + await tab?.page.close(); + return await this.listTabsMarkdown(); + } + + async run(tool: Tool, params: Record | undefined) { + // Tab management is done outside of the action() call. + const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {})); + const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult; + const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined; + + if (resultOverride) + return resultOverride; + + if (!this._currentTab) { + return { + content: [{ + type: 'text', + text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.', + }], + }; + } + + const tab = this.currentTabOrDie(); + // TODO: race against modal dialogs to resolve clicks. + let actionResult: { content?: (ImageContent | TextContent)[] } | undefined; + try { + if (waitForNetwork) + actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined; + else + actionResult = await racingAction?.() ?? undefined; + } finally { + if (captureSnapshot && !this._javaScriptBlocked()) + await tab.captureSnapshot(); + } + + const result: string[] = []; + result.push(`- Ran Playwright code: +\`\`\`js +${code.join('\n')} +\`\`\` +`); + + if (this.modalStates().length) { + result.push(...this.modalStatesMarkdown()); + return { + content: [{ + type: 'text', + text: result.join('\n'), + }], + }; + } + + if (this._downloads.length) { + result.push('', '### Downloads'); + for (const entry of this._downloads) { + if (entry.finished) + result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`); + else + result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`); + } + result.push(''); + } + + if (this.tabs().length > 1) + result.push(await this.listTabsMarkdown(), ''); + + if (this.tabs().length > 1) + result.push('### Current tab'); + + result.push( + `- Page URL: ${tab.page.url()}`, + `- Page Title: ${await tab.title()}` + ); + + if (captureSnapshot && tab.hasSnapshot()) + result.push(tab.snapshotOrDie().text()); + + const content = actionResult?.content ?? []; + + return { + content: [ + ...content, + { + type: 'text', + text: result.join('\n'), + } + ], + }; + } + + async waitForTimeout(time: number) { + if (!this._currentTab || this._javaScriptBlocked()) { + await new Promise(f => setTimeout(f, time)); + return; + } + + await callOnPageNoTrace(this._currentTab.page, page => { + return page.evaluate(() => new Promise(f => setTimeout(f, 1000))); + }); + } + + private async _raceAgainstModalDialogs(action: () => Promise): Promise { + this._pendingAction = { + dialogShown: new ManualPromise(), + }; + + let result: ToolActionResult | undefined; + try { + await Promise.race([ + action().then(r => result = r), + this._pendingAction.dialogShown, + ]); + } finally { + this._pendingAction = undefined; + } + return result; + } + + private _javaScriptBlocked(): boolean { + return this._modalStates.some(state => state.type === 'dialog'); + } + + dialogShown(tab: Tab, dialog: playwright.Dialog) { + this.setModalState({ + type: 'dialog', + description: `"${dialog.type()}" dialog with message "${dialog.message()}"`, + dialog, + }, tab); + this._pendingAction?.dialogShown.resolve(); + } + + async downloadStarted(tab: Tab, download: playwright.Download) { + const entry = { + download, + finished: false, + outputFile: await outputFile(this.config, download.suggestedFilename()) + }; + this._downloads.push(entry); + await download.saveAs(entry.outputFile); + entry.finished = true; + } + + 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) { + this._modalStates = this._modalStates.filter(state => state.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.close(); + } + + async close() { + 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(); + }); + } + + 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 }> { + // TODO: move to the browser context factory to make it based on isolation mode. + const result = await this._browserContextFactory.createContext(); + const { browserContext } = result; + await this._setupRequestInterception(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; + } +} diff --git a/mcp/servers/playwright-mcp/src/fileUtils.ts b/mcp/servers/playwright-mcp/src/fileUtils.ts new file mode 100644 index 00000000..4155b749 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/fileUtils.ts @@ -0,0 +1,37 @@ +/** + * 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 os from 'node:os'; +import path from 'node:path'; + +import type { FullConfig } from './config.js'; + +export function cacheDir() { + let cacheDirectory: string; + if (process.platform === 'linux') + cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); + else if (process.platform === 'darwin') + cacheDirectory = path.join(os.homedir(), 'Library', 'Caches'); + else if (process.platform === 'win32') + cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); + else + throw new Error('Unsupported platform: ' + process.platform); + return path.join(cacheDirectory, 'ms-playwright'); +} + +export async function userDataDir(browserConfig: FullConfig['browser']) { + return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`); +} diff --git a/mcp/servers/playwright-mcp/src/httpServer.ts b/mcp/servers/playwright-mcp/src/httpServer.ts new file mode 100644 index 00000000..9e67bef7 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/httpServer.ts @@ -0,0 +1,232 @@ +/** + * 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 http from 'http'; +import net from 'net'; + +import mime from 'mime'; + +import { ManualPromise } from './manualPromise.js'; + + +export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => void; + +export type Transport = { + sendEvent?: (method: string, params: any) => void; + close?: () => void; + onconnect: () => void; + dispatch: (method: string, params: any) => Promise; + onclose: () => void; +}; + +export class HttpServer { + private _server: http.Server; + private _urlPrefixPrecise: string = ''; + private _urlPrefixHumanReadable: string = ''; + private _port: number = 0; + private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = []; + + constructor() { + this._server = http.createServer(this._onRequest.bind(this)); + decorateServer(this._server); + } + + server() { + return this._server; + } + + routePrefix(prefix: string, handler: ServerRouteHandler) { + this._routes.push({ prefix, handler }); + } + + routePath(path: string, handler: ServerRouteHandler) { + this._routes.push({ exact: path, handler }); + } + + port(): number { + return this._port; + } + + private async _tryStart(port: number | undefined, host: string) { + const errorPromise = new ManualPromise(); + const errorListener = (error: Error) => errorPromise.reject(error); + this._server.on('error', errorListener); + + try { + this._server.listen(port, host); + await Promise.race([ + new Promise(cb => this._server!.once('listening', cb)), + errorPromise, + ]); + } finally { + this._server.removeListener('error', errorListener); + } + } + + async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise { + const host = options.host || 'localhost'; + if (options.preferredPort) { + try { + await this._tryStart(options.preferredPort, host); + } catch (e: any) { + if (!e || !e.message || !e.message.includes('EADDRINUSE')) + throw e; + await this._tryStart(undefined, host); + } + } else { + await this._tryStart(options.port, host); + } + + const address = this._server.address(); + if (typeof address === 'string') { + this._urlPrefixPrecise = address; + this._urlPrefixHumanReadable = address; + } else { + this._port = address!.port; + const resolvedHost = address!.family === 'IPv4' ? address!.address : `[${address!.address}]`; + this._urlPrefixPrecise = `http://${resolvedHost}:${address!.port}`; + this._urlPrefixHumanReadable = `http://${host}:${address!.port}`; + } + } + + async stop() { + await new Promise(cb => this._server!.close(cb)); + } + + urlPrefix(purpose: 'human-readable' | 'precise'): string { + return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise; + } + + serveFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean { + try { + for (const [name, value] of Object.entries(headers || {})) + response.setHeader(name, value); + if (request.headers.range) + this._serveRangeFile(request, response, absoluteFilePath); + else + this._serveFile(response, absoluteFilePath); + return true; + } catch (e) { + return false; + } + } + + _serveFile(response: http.ServerResponse, absoluteFilePath: string) { + const content = fs.readFileSync(absoluteFilePath); + response.statusCode = 200; + const contentType = mime.getType(path.extname(absoluteFilePath)) || 'application/octet-stream'; + response.setHeader('Content-Type', contentType); + response.setHeader('Content-Length', content.byteLength); + response.end(content); + } + + _serveRangeFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string) { + const range = request.headers.range; + if (!range || !range.startsWith('bytes=') || range.includes(', ') || [...range].filter(char => char === '-').length !== 1) { + response.statusCode = 400; + return response.end('Bad request'); + } + + // Parse the range header: https://datatracker.ietf.org/doc/html/rfc7233#section-2.1 + const [startStr, endStr] = range.replace(/bytes=/, '').split('-'); + + // Both start and end (when passing to fs.createReadStream) and the range header are inclusive and start counting at 0. + let start: number; + let end: number; + const size = fs.statSync(absoluteFilePath).size; + if (startStr !== '' && endStr === '') { + // No end specified: use the whole file + start = +startStr; + end = size - 1; + } else if (startStr === '' && endStr !== '') { + // No start specified: calculate start manually + start = size - +endStr; + end = size - 1; + } else { + start = +startStr; + end = +endStr; + } + + // Handle unavailable range request + if (Number.isNaN(start) || Number.isNaN(end) || start >= size || end >= size || start > end) { + // Return the 416 Range Not Satisfiable: https://datatracker.ietf.org/doc/html/rfc7233#section-4.4 + response.writeHead(416, { + 'Content-Range': `bytes */${size}` + }); + return response.end(); + } + + // Sending Partial Content: https://datatracker.ietf.org/doc/html/rfc7233#section-4.1 + response.writeHead(206, { + 'Content-Range': `bytes ${start}-${end}/${size}`, + 'Accept-Ranges': 'bytes', + 'Content-Length': end - start + 1, + 'Content-Type': mime.getType(path.extname(absoluteFilePath))!, + }); + + const readable = fs.createReadStream(absoluteFilePath, { start, end }); + readable.pipe(response); + } + + private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) { + if (request.method === 'OPTIONS') { + response.writeHead(200); + response.end(); + return; + } + + request.on('error', () => response.end()); + try { + if (!request.url) { + response.end(); + return; + } + const url = new URL('http://localhost' + request.url); + for (const route of this._routes) { + if (route.exact && url.pathname === route.exact) { + route.handler(request, response); + return; + } + if (route.prefix && url.pathname.startsWith(route.prefix)) { + route.handler(request, response); + return; + } + } + response.statusCode = 404; + response.end(); + } catch (e) { + response.end(); + } + } +} + +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/mcp/servers/playwright-mcp/src/index.ts b/mcp/servers/playwright-mcp/src/index.ts new file mode 100644 index 00000000..2f5f2f94 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/index.ts @@ -0,0 +1,46 @@ +/** + * 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 { createConnection as createConnectionImpl } from './connection.js'; +import type { Connection } from '../index.js'; +import { resolveConfig } from './config.js'; +import { contextFactory } from './browserContextFactory.js'; + +import type { Config } from '../config.js'; +import type { BrowserContext } from 'playwright'; +import type { BrowserContextFactory } from './browserContextFactory.js'; + +export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise): Promise { + const config = await resolveConfig(userConfig); + const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config.browser); + return createConnectionImpl(config, factory); +} + +class SimpleBrowserContextFactory implements BrowserContextFactory { + 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/mcp/servers/playwright-mcp/src/javascript.ts b/mcp/servers/playwright-mcp/src/javascript.ts new file mode 100644 index 00000000..a1fabbd9 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/javascript.ts @@ -0,0 +1,53 @@ +/** + * 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/mcp/servers/playwright-mcp/src/manualPromise.ts b/mcp/servers/playwright-mcp/src/manualPromise.ts new file mode 100644 index 00000000..a5034e05 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/manualPromise.ts @@ -0,0 +1,127 @@ +/** + * 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/mcp/servers/playwright-mcp/src/package.ts b/mcp/servers/playwright-mcp/src/package.ts new file mode 100644 index 00000000..a6c7019d --- /dev/null +++ b/mcp/servers/playwright-mcp/src/package.ts @@ -0,0 +1,22 @@ +/** + * 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 url from 'node:url'; +import path from 'node:path'; + +const __filename = url.fileURLToPath(import.meta.url); +export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8')); diff --git a/mcp/servers/playwright-mcp/src/pageSnapshot.ts b/mcp/servers/playwright-mcp/src/pageSnapshot.ts new file mode 100644 index 00000000..5f2f07d6 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/pageSnapshot.ts @@ -0,0 +1,55 @@ +/** + * 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 playwright from 'playwright'; +import { callOnPageNoTrace } from './tools/utils.js'; + +type PageEx = playwright.Page & { + _snapshotForAI: () => Promise; +}; + +export class PageSnapshot { + private _page: playwright.Page; + private _text!: string; + + constructor(page: playwright.Page) { + this._page = page; + } + + static async create(page: playwright.Page): Promise { + const snapshot = new PageSnapshot(page); + await snapshot._build(); + return snapshot; + } + + text(): string { + return this._text; + } + + private async _build() { + const snapshot = await callOnPageNoTrace(this._page, page => (page as PageEx)._snapshotForAI()); + this._text = [ + `- Page Snapshot`, + '```yaml', + snapshot, + '```', + ].join('\n'); + } + + refLocator(params: { element: string, ref: string }): playwright.Locator { + return this._page.locator(`aria-ref=${params.ref}`).describe(params.element); + } +} diff --git a/mcp/servers/playwright-mcp/src/program.ts b/mcp/servers/playwright-mcp/src/program.ts new file mode 100644 index 00000000..5a381d3a --- /dev/null +++ b/mcp/servers/playwright-mcp/src/program.ts @@ -0,0 +1,88 @@ +/** + * 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 { Option, program } from 'commander'; +// @ts-ignore +import { startTraceViewerServer } from 'playwright-core/lib/server'; + +import { startHttpServer, startHttpTransport, startStdioTransport } from './transport.js'; +import { resolveCLIConfig } from './config.js'; +import { Server } from './server.js'; +import { packageJSON } from './package.js'; +import { startCDPRelayServer } from './cdpRelay.js'; + +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('--browser-agent ', 'Use browser agent (experimental).') + .option('--caps ', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.') + .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('--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", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.') + .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-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"') + .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)') + .addOption(new Option('--extension', 'Allow connecting to a running browser instance (Edge/Chrome only). Requires the \'Playwright MCP\' browser extension to be installed.').hideHelp()) + .action(async options => { + const config = await resolveCLIConfig(options); + const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined; + if (config.extension) { + if (!httpServer) + throw new Error('--port parameter is required for extension mode'); + // Point CDP endpoint to the relay server. + config.browser.cdpEndpoint = await startCDPRelayServer(httpServer); + } + + const server = new Server(config); + server.setupExitWatchdog(); + + if (httpServer) + await startHttpTransport(httpServer, server); + else + await startStdioTransport(server); + + if (config.saveTrace) { + const server = await startTraceViewerServer(); + const urlPrefix = server.urlPrefix('human-readable'); + const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json'; + // eslint-disable-next-line no-console + console.error('\nTrace viewer listening on ' + url); + } + }); + +function semicolonSeparatedList(value: string): string[] { + return value.split(';').map(v => v.trim()); +} + +void program.parseAsync(process.argv); diff --git a/mcp/servers/playwright-mcp/src/resources/resource.ts b/mcp/servers/playwright-mcp/src/resources/resource.ts new file mode 100644 index 00000000..abe0e5ba --- /dev/null +++ b/mcp/servers/playwright-mcp/src/resources/resource.ts @@ -0,0 +1,36 @@ +/** + * 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 { Context } from '../context.js'; + +export type ResourceSchema = { + uri: string; + name: string; + description?: string; + mimeType?: string; +}; + +export type ResourceResult = { + uri: string; + mimeType?: string; + text?: string; + blob?: string; +}; + +export type Resource = { + schema: ResourceSchema; + read: (context: Context, uri: string) => Promise; +}; diff --git a/mcp/servers/playwright-mcp/src/server.ts b/mcp/servers/playwright-mcp/src/server.ts new file mode 100644 index 00000000..8c143e13 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/server.ts @@ -0,0 +1,59 @@ +/** + * 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 { createConnection } from './connection.js'; +import { contextFactory } from './browserContextFactory.js'; + +import type { FullConfig } from './config.js'; +import type { Connection } from './connection.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { BrowserContextFactory } from './browserContextFactory.js'; + +export class Server { + readonly config: FullConfig; + private _connectionList: Connection[] = []; + private _browserConfig: FullConfig['browser']; + private _contextFactory: BrowserContextFactory; + + constructor(config: FullConfig) { + this.config = config; + this._browserConfig = config.browser; + this._contextFactory = contextFactory(this._browserConfig); + } + + async createConnection(transport: Transport): Promise { + const connection = createConnection(this.config, this._contextFactory); + this._connectionList.push(connection); + await connection.server.connect(transport); + return connection; + } + + setupExitWatchdog() { + let isExiting = false; + const handleExit = async () => { + if (isExiting) + return; + isExiting = true; + setTimeout(() => process.exit(0), 15000); + await Promise.all(this._connectionList.map(connection => connection.close())); + process.exit(0); + }; + + process.stdin.on('close', handleExit); + process.on('SIGINT', handleExit); + process.on('SIGTERM', handleExit); + } +} diff --git a/mcp/servers/playwright-mcp/src/tab.ts b/mcp/servers/playwright-mcp/src/tab.ts new file mode 100644 index 00000000..d80313d6 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tab.ts @@ -0,0 +1,120 @@ +/** + * 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 playwright from 'playwright'; + +import { PageSnapshot } from './pageSnapshot.js'; + +import type { Context } from './context.js'; +import { callOnPageNoTrace } from './tools/utils.js'; + +export class Tab { + readonly context: Context; + readonly page: playwright.Page; + private _consoleMessages: playwright.ConsoleMessage[] = []; + private _requests: Map = new Map(); + private _snapshot: PageSnapshot | undefined; + private _onPageClose: (tab: Tab) => void; + + constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { + this.context = context; + this.page = page; + this._onPageClose = onPageClose; + page.on('console', event => this._consoleMessages.push(event)); + 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.context.setModalState({ + type: 'fileChooser', + description: 'File chooser', + fileChooser: chooser, + }, this); + }); + page.on('dialog', dialog => this.context.dialogShown(this, dialog)); + page.on('download', download => { + void this.context.downloadStarted(this, download); + }); + page.setDefaultNavigationTimeout(60000); + page.setDefaultTimeout(5000); + } + + private _clearCollectedArtifacts() { + this._consoleMessages.length = 0; + this._requests.clear(); + } + + private _onClose() { + this._clearCollectedArtifacts(); + this._onPageClose(this); + } + + async title(): Promise { + return await callOnPageNoTrace(this.page, page => page.title()); + } + + async waitForLoadState(state: 'load', options?: { timeout?: number }): Promise { + await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(() => {})); + } + + async navigate(url: string) { + this._clearCollectedArtifacts(); + + const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(() => {})); + 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, 1000)), + ]); + if (!download) + throw e; + } + + // Cap load event to 5 seconds, the page is operational at this point. + await this.waitForLoadState('load', { timeout: 5000 }); + } + + hasSnapshot(): boolean { + return !!this._snapshot; + } + + snapshotOrDie(): PageSnapshot { + if (!this._snapshot) + throw new Error('No snapshot available'); + return this._snapshot; + } + + consoleMessages(): playwright.ConsoleMessage[] { + return this._consoleMessages; + } + + requests(): Map { + return this._requests; + } + + async captureSnapshot() { + this._snapshot = await PageSnapshot.create(this.page); + } +} diff --git a/mcp/servers/playwright-mcp/src/tools.ts b/mcp/servers/playwright-mcp/src/tools.ts new file mode 100644 index 00000000..bd6db0f0 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools.ts @@ -0,0 +1,66 @@ +/** + * 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.js'; +import console from './tools/console.js'; +import dialogs from './tools/dialogs.js'; +import files from './tools/files.js'; +import install from './tools/install.js'; +import keyboard from './tools/keyboard.js'; +import navigate from './tools/navigate.js'; +import network from './tools/network.js'; +import pdf from './tools/pdf.js'; +import snapshot from './tools/snapshot.js'; +import tabs from './tools/tabs.js'; +import screenshot from './tools/screenshot.js'; +import testing from './tools/testing.js'; +import vision from './tools/vision.js'; +import wait from './tools/wait.js'; + +import type { Tool } from './tools/tool.js'; + +export const snapshotTools: Tool[] = [ + ...common(true), + ...console, + ...dialogs(true), + ...files(true), + ...install, + ...keyboard(true), + ...navigate(true), + ...network, + ...pdf, + ...screenshot, + ...snapshot, + ...tabs(true), + ...testing, + ...wait(true), +]; + +export const visionTools: Tool[] = [ + ...common(false), + ...console, + ...dialogs(false), + ...files(false), + ...install, + ...keyboard(false), + ...navigate(false), + ...network, + ...pdf, + ...tabs(false), + ...testing, + ...vision, + ...wait(false), +]; diff --git a/mcp/servers/playwright-mcp/src/tools/common.ts b/mcp/servers/playwright-mcp/src/tools/common.ts new file mode 100644 index 00000000..8a16c35a --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/common.ts @@ -0,0 +1,78 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const close = defineTool({ + capability: 'core', + + schema: { + name: 'browser_close', + title: 'Close browser', + description: 'Close the page', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async context => { + await context.close(); + return { + code: [`await page.close()`], + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +const resize: ToolFactory = captureSnapshot => defineTool({ + 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 (context, params) => { + const tab = context.currentTabOrDie(); + + const code = [ + `// Resize browser window to ${params.width}x${params.height}`, + `await page.setViewportSize({ width: ${params.width}, height: ${params.height} });` + ]; + + const action = async () => { + await tab.page.setViewportSize({ width: params.width, height: params.height }); + }; + + return { + code, + action, + captureSnapshot, + waitForNetwork: true + }; + }, +}); + +export default (captureSnapshot: boolean) => [ + close, + resize(captureSnapshot) +]; diff --git a/mcp/servers/playwright-mcp/src/tools/console.ts b/mcp/servers/playwright-mcp/src/tools/console.ts new file mode 100644 index 00000000..45bf3d78 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/console.ts @@ -0,0 +1,47 @@ +/** + * 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 { defineTool } from './tool.js'; + +const console = defineTool({ + capability: 'core', + schema: { + name: 'browser_console_messages', + title: 'Get console messages', + description: 'Returns all console messages', + inputSchema: z.object({}), + type: 'readOnly', + }, + handle: async context => { + const messages = context.currentTabOrDie().consoleMessages(); + const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n'); + return { + code: [`// `], + action: async () => { + return { + content: [{ type: 'text', text: log }] + }; + }, + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +export default [ + console, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/dialogs.ts b/mcp/servers/playwright-mcp/src/tools/dialogs.ts new file mode 100644 index 00000000..348e4614 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/dialogs.ts @@ -0,0 +1,62 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const handleDialog: ToolFactory = captureSnapshot => defineTool({ + 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 (context, params) => { + const dialogState = context.modalStates().find(state => state.type === 'dialog'); + if (!dialogState) + throw new Error('No dialog visible'); + + if (params.accept) + await dialogState.dialog.accept(params.promptText); + else + await dialogState.dialog.dismiss(); + + context.clearModalState(dialogState); + + const code = [ + `// `, + ]; + + return { + code, + captureSnapshot, + waitForNetwork: false, + }; + }, + + clearsModalState: 'dialog', +}); + +export default (captureSnapshot: boolean) => [ + handleDialog(captureSnapshot), +]; diff --git a/mcp/servers/playwright-mcp/src/tools/files.ts b/mcp/servers/playwright-mcp/src/tools/files.ts new file mode 100644 index 00000000..2dc7837f --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/files.ts @@ -0,0 +1,59 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const uploadFile: ToolFactory = captureSnapshot => defineTool({ + capability: 'files', + + 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 (context, params) => { + const modalState = context.modalStates().find(state => state.type === 'fileChooser'); + if (!modalState) + throw new Error('No file chooser visible'); + + const code = [ + `// { + await modalState.fileChooser.setFiles(params.paths); + context.clearModalState(modalState); + }; + + return { + code, + action, + captureSnapshot, + waitForNetwork: true, + }; + }, + clearsModalState: 'fileChooser', +}); + +export default (captureSnapshot: boolean) => [ + uploadFile(captureSnapshot), +]; diff --git a/mcp/servers/playwright-mcp/src/tools/install.ts b/mcp/servers/playwright-mcp/src/tools/install.ts new file mode 100644 index 00000000..d0d51455 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/install.ts @@ -0,0 +1,63 @@ +/** + * 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 'zod'; +import { defineTool } from './tool.js'; + +import { fileURLToPath } from 'node:url'; + +const install = defineTool({ + capability: '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 => { + const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome'; + const cliUrl = import.meta.resolve('playwright/package.json'); + const cliPath = path.join(fileURLToPath(cliUrl), '..', '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('')}`)); + }); + }); + return { + code: [`// Browser ${channel} installed`], + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +export default [ + install, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/keyboard.ts b/mcp/servers/playwright-mcp/src/tools/keyboard.ts new file mode 100644 index 00000000..521aab20 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/keyboard.ts @@ -0,0 +1,54 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const pressKey: ToolFactory = captureSnapshot => defineTool({ + 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 (context, params) => { + const tab = context.currentTabOrDie(); + + const code = [ + `// Press ${params.key}`, + `await page.keyboard.press('${params.key}');`, + ]; + + const action = () => tab.page.keyboard.press(params.key); + + return { + code, + action, + captureSnapshot, + waitForNetwork: true + }; + }, +}); + +export default (captureSnapshot: boolean) => [ + pressKey(captureSnapshot), +]; diff --git a/mcp/servers/playwright-mcp/src/tools/navigate.ts b/mcp/servers/playwright-mcp/src/tools/navigate.ts new file mode 100644 index 00000000..501576ed --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/navigate.ts @@ -0,0 +1,104 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const navigate: ToolFactory = captureSnapshot => 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) => { + const tab = await context.ensureTab(); + await tab.navigate(params.url); + + const code = [ + `// Navigate to ${params.url}`, + `await page.goto('${params.url}');`, + ]; + + return { + code, + captureSnapshot, + waitForNetwork: false, + }; + }, +}); + +const goBack: ToolFactory = captureSnapshot => defineTool({ + capability: 'history', + schema: { + name: 'browser_navigate_back', + title: 'Go back', + description: 'Go back to the previous page', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async context => { + const tab = await context.ensureTab(); + await tab.page.goBack(); + const code = [ + `// Navigate back`, + `await page.goBack();`, + ]; + + return { + code, + captureSnapshot, + waitForNetwork: false, + }; + }, +}); + +const goForward: ToolFactory = captureSnapshot => defineTool({ + capability: 'history', + schema: { + name: 'browser_navigate_forward', + title: 'Go forward', + description: 'Go forward to the next page', + inputSchema: z.object({}), + type: 'readOnly', + }, + handle: async context => { + const tab = context.currentTabOrDie(); + await tab.page.goForward(); + const code = [ + `// Navigate forward`, + `await page.goForward();`, + ]; + return { + code, + captureSnapshot, + waitForNetwork: false, + }; + }, +}); + +export default (captureSnapshot: boolean) => [ + navigate(captureSnapshot), + goBack(captureSnapshot), + goForward(captureSnapshot), +]; diff --git a/mcp/servers/playwright-mcp/src/tools/network.ts b/mcp/servers/playwright-mcp/src/tools/network.ts new file mode 100644 index 00000000..9e1946c9 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/network.ts @@ -0,0 +1,59 @@ +/** + * 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 { defineTool } from './tool.js'; + +import type * as playwright from 'playwright'; + +const requests = defineTool({ + 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 context => { + const requests = context.currentTabOrDie().requests(); + const log = [...requests.entries()].map(([request, response]) => renderRequest(request, response)).join('\n'); + return { + code: [`// `], + action: async () => { + return { + content: [{ type: 'text', text: log }] + }; + }, + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +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/mcp/servers/playwright-mcp/src/tools/pdf.ts b/mcp/servers/playwright-mcp/src/tools/pdf.ts new file mode 100644 index 00000000..c020f034 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/pdf.ts @@ -0,0 +1,58 @@ +/** + * 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 { defineTool } from './tool.js'; + +import * as javascript from '../javascript.js'; +import { outputFile } from '../config.js'; + +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 = defineTool({ + capability: 'pdf', + + schema: { + name: 'browser_pdf_save', + title: 'Save as PDF', + description: 'Save page as PDF', + inputSchema: pdfSchema, + type: 'readOnly', + }, + + handle: async (context, params) => { + const tab = context.currentTabOrDie(); + const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`); + + const code = [ + `// Save page as ${fileName}`, + `await page.pdf(${javascript.formatObject({ path: fileName })});`, + ]; + + return { + code, + action: async () => tab.page.pdf({ path: fileName }).then(() => {}), + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +export default [ + pdf, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/screenshot.ts b/mcp/servers/playwright-mcp/src/tools/screenshot.ts new file mode 100644 index 00000000..439d79ab --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/screenshot.ts @@ -0,0 +1,90 @@ +/** + * 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 { defineTool } from './tool.js'; +import * as javascript from '../javascript.js'; +import { outputFile } from '../config.js'; +import { generateLocator } from './utils.js'; + +import type * as playwright from 'playwright'; + +const screenshotSchema = z.object({ + raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'), + 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.'), +}).refine(data => { + return !!data.element === !!data.ref; +}, { + message: 'Both element and ref must be provided or neither.', + path: ['ref', 'element'] +}); + +const screenshot = defineTool({ + 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 (context, params) => { + const tab = context.currentTabOrDie(); + const snapshot = tab.snapshotOrDie(); + const fileType = params.raw ? 'png' : 'jpeg'; + const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`); + const options: playwright.PageScreenshotOptions = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName }; + const isElementScreenshot = params.element && params.ref; + + const code = [ + `// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`, + ]; + + const locator = params.ref ? snapshot.refLocator({ element: params.element || '', ref: params.ref }) : null; + + if (locator) + code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`); + else + code.push(`await page.screenshot(${javascript.formatObject(options)});`); + + const includeBase64 = context.clientSupportsImages(); + const action = async () => { + const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options); + return { + content: includeBase64 ? [{ + type: 'image' as 'image', + data: screenshot.toString('base64'), + mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg', + }] : [] + }; + }; + + return { + code, + action, + captureSnapshot: true, + waitForNetwork: false, + }; + } +}); + +export default [ + screenshot, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/snapshot.ts b/mcp/servers/playwright-mcp/src/tools/snapshot.ts new file mode 100644 index 00000000..7d1ef32d --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/snapshot.ts @@ -0,0 +1,234 @@ +/** + * 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 { defineTool } from './tool.js'; +import * as javascript from '../javascript.js'; +import { generateLocator } from './utils.js'; + +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 => { + await context.ensureTab(); + + return { + code: [`// `], + captureSnapshot: true, + waitForNetwork: false, + }; + }, +}); + +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'), +}); + +const click = defineTool({ + capability: 'core', + schema: { + name: 'browser_click', + title: 'Click', + description: 'Perform click on a web page', + inputSchema: clickSchema, + type: 'destructive', + }, + + handle: async (context, params) => { + const tab = context.currentTabOrDie(); + const locator = tab.snapshotOrDie().refLocator(params); + + const code: string[] = []; + if (params.doubleClick) { + code.push(`// Double click ${params.element}`); + code.push(`await page.${await generateLocator(locator)}.dblclick();`); + } else { + code.push(`// Click ${params.element}`); + code.push(`await page.${await generateLocator(locator)}.click();`); + } + + return { + code, + action: () => params.doubleClick ? locator.dblclick() : locator.click(), + captureSnapshot: true, + waitForNetwork: true, + }; + }, +}); + +const drag = defineTool({ + 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 (context, params) => { + const snapshot = context.currentTabOrDie().snapshotOrDie(); + const startLocator = snapshot.refLocator({ ref: params.startRef, element: params.startElement }); + const endLocator = snapshot.refLocator({ ref: params.endRef, element: params.endElement }); + + const code = [ + `// Drag ${params.startElement} to ${params.endElement}`, + `await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});` + ]; + + return { + code, + action: () => startLocator.dragTo(endLocator), + captureSnapshot: true, + waitForNetwork: true, + }; + }, +}); + +const hover = defineTool({ + capability: 'core', + schema: { + name: 'browser_hover', + title: 'Hover mouse', + description: 'Hover over element on page', + inputSchema: elementSchema, + type: 'readOnly', + }, + + handle: async (context, params) => { + const snapshot = context.currentTabOrDie().snapshotOrDie(); + const locator = snapshot.refLocator(params); + + const code = [ + `// Hover over ${params.element}`, + `await page.${await generateLocator(locator)}.hover();` + ]; + + return { + code, + action: () => locator.hover(), + captureSnapshot: true, + waitForNetwork: true, + }; + }, +}); + +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 = defineTool({ + capability: 'core', + schema: { + name: 'browser_type', + title: 'Type text', + description: 'Type text into editable element', + inputSchema: typeSchema, + type: 'destructive', + }, + + handle: async (context, params) => { + const snapshot = context.currentTabOrDie().snapshotOrDie(); + const locator = snapshot.refLocator(params); + + const code: string[] = []; + const steps: (() => Promise)[] = []; + + if (params.slowly) { + code.push(`// Press "${params.text}" sequentially into "${params.element}"`); + code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`); + steps.push(() => locator.pressSequentially(params.text)); + } else { + code.push(`// Fill "${params.text}" into "${params.element}"`); + code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`); + steps.push(() => locator.fill(params.text)); + } + + if (params.submit) { + code.push(`// Submit text`); + code.push(`await page.${await generateLocator(locator)}.press('Enter');`); + steps.push(() => locator.press('Enter')); + } + + return { + code, + action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()), + captureSnapshot: true, + waitForNetwork: true, + }; + }, +}); + +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 = defineTool({ + capability: 'core', + schema: { + name: 'browser_select_option', + title: 'Select option', + description: 'Select an option in a dropdown', + inputSchema: selectOptionSchema, + type: 'destructive', + }, + + handle: async (context, params) => { + const snapshot = context.currentTabOrDie().snapshotOrDie(); + const locator = snapshot.refLocator(params); + + const code = [ + `// Select options [${params.values.join(', ')}] in ${params.element}`, + `await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});` + ]; + + return { + code, + action: () => locator.selectOption(params.values).then(() => {}), + captureSnapshot: true, + waitForNetwork: true, + }; + }, +}); + +export default [ + snapshot, + click, + drag, + hover, + type, + selectOption, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/tabs.ts b/mcp/servers/playwright-mcp/src/tools/tabs.ts new file mode 100644 index 00000000..4133bf16 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/tabs.ts @@ -0,0 +1,134 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const listTabs = defineTool({ + capability: 'tabs', + + schema: { + name: 'browser_tab_list', + title: 'List tabs', + description: 'List browser tabs', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async context => { + await context.ensureTab(); + return { + code: [`// `], + captureSnapshot: false, + waitForNetwork: false, + resultOverride: { + content: [{ + type: 'text', + text: await context.listTabsMarkdown(), + }], + }, + }; + }, +}); + +const selectTab: ToolFactory = captureSnapshot => defineTool({ + capability: 'tabs', + + schema: { + name: 'browser_tab_select', + title: 'Select a tab', + description: 'Select a tab by index', + inputSchema: z.object({ + index: z.number().describe('The index of the tab to select'), + }), + type: 'readOnly', + }, + + handle: async (context, params) => { + await context.selectTab(params.index); + const code = [ + `// `, + ]; + + return { + code, + captureSnapshot, + waitForNetwork: false + }; + }, +}); + +const newTab: ToolFactory = captureSnapshot => defineTool({ + capability: 'tabs', + + schema: { + name: 'browser_tab_new', + title: 'Open a new tab', + description: 'Open a new tab', + inputSchema: z.object({ + url: z.string().optional().describe('The URL to navigate to in the new tab. If not provided, the new tab will be blank.'), + }), + type: 'readOnly', + }, + + handle: async (context, params) => { + await context.newTab(); + if (params.url) + await context.currentTabOrDie().navigate(params.url); + + const code = [ + `// `, + ]; + return { + code, + captureSnapshot, + waitForNetwork: false + }; + }, +}); + +const closeTab: ToolFactory = captureSnapshot => defineTool({ + capability: 'tabs', + + schema: { + name: 'browser_tab_close', + title: 'Close a tab', + description: 'Close a tab', + inputSchema: z.object({ + index: z.number().optional().describe('The index of the tab to close. Closes current tab if not provided.'), + }), + type: 'destructive', + }, + + handle: async (context, params) => { + await context.closeTab(params.index); + const code = [ + `// `, + ]; + return { + code, + captureSnapshot, + waitForNetwork: false + }; + }, +}); + +export default (captureSnapshot: boolean) => [ + listTabs, + newTab(captureSnapshot), + selectTab(captureSnapshot), + closeTab(captureSnapshot), +]; diff --git a/mcp/servers/playwright-mcp/src/tools/testing.ts b/mcp/servers/playwright-mcp/src/tools/testing.ts new file mode 100644 index 00000000..9518d19b --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/testing.ts @@ -0,0 +1,67 @@ +/** + * 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 { defineTool } from './tool.js'; + +const generateTestSchema = z.object({ + name: z.string().describe('The name of the test'), + description: z.string().describe('The description of the test'), + steps: z.array(z.string()).describe('The steps of the test'), +}); + +const generateTest = defineTool({ + capability: 'testing', + + schema: { + name: 'browser_generate_playwright_test', + title: 'Generate a Playwright test', + description: 'Generate a Playwright test for given scenario', + inputSchema: generateTestSchema, + type: 'readOnly', + }, + + handle: async (context, params) => { + return { + resultOverride: { + content: [{ + type: 'text', + text: instructions(params), + }], + }, + code: [], + captureSnapshot: false, + waitForNetwork: false, + }; + }, +}); + +const instructions = (params: { name: string, description: string, steps: string[] }) => [ + `## Instructions`, + `- You are a playwright test generator.`, + `- You are given a scenario and you need to generate a playwright test for it.`, + '- DO NOT generate test code based on the scenario alone. DO run steps one by one using the tools provided instead.', + '- Only after all steps are completed, emit a Playwright TypeScript test that uses @playwright/test based on message history', + '- Save generated test file in the tests directory', + `Test name: ${params.name}`, + `Description: ${params.description}`, + `Steps:`, + ...params.steps.map((step, index) => `- ${index + 1}. ${step}`), +].join('\n'); + +export default [ + generateTest, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/tool.ts b/mcp/servers/playwright-mcp/src/tools/tool.ts new file mode 100644 index 00000000..4b88c898 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/tool.ts @@ -0,0 +1,68 @@ +/** + * 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 { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; +import type { z } from 'zod'; +import type { Context } from '../context.js'; +import type * as playwright from 'playwright'; +import type { ToolCapability } from '../../config.js'; + +export type ToolSchema = { + name: string; + title: string; + description: string; + inputSchema: Input; + type: 'readOnly' | 'destructive'; +}; + +type InputType = z.Schema; + +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 ToolActionResult = { content?: (ImageContent | TextContent)[] } | undefined | void; + +export type ToolResult = { + code: string[]; + action?: () => Promise; + captureSnapshot: boolean; + waitForNetwork: boolean; + resultOverride?: ToolActionResult; +}; + +export type Tool = { + capability: ToolCapability; + schema: ToolSchema; + clearsModalState?: ModalState['type']; + handle: (context: Context, params: z.output) => Promise; +}; + +export type ToolFactory = (snapshot: boolean) => Tool; + +export function defineTool(tool: Tool): Tool { + return tool; +} diff --git a/mcp/servers/playwright-mcp/src/tools/utils.ts b/mcp/servers/playwright-mcp/src/tools/utils.ts new file mode 100644 index 00000000..338c1891 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/utils.ts @@ -0,0 +1,92 @@ +/** + * 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 * as playwright from 'playwright'; +import type { Context } from '../context.js'; +import type { Tab } from '../tab.js'; + +export async function waitForCompletion(context: Context, 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 context.waitForTimeout(1000); + return result; + } finally { + dispose(); + } +} + +export 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)); +} + +export async function generateLocator(locator: playwright.Locator): Promise { + try { + return await (locator as any)._generateLocatorString(); + } catch (e) { + if (e instanceof Error && /locator._generateLocatorString: Timeout .* exceeded/.test(e.message)) + throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.'); + throw e; + } +} + +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/mcp/servers/playwright-mcp/src/tools/vision.ts b/mcp/servers/playwright-mcp/src/tools/vision.ts new file mode 100644 index 00000000..a3803116 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/vision.ts @@ -0,0 +1,213 @@ +/** + * 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 { defineTool } from './tool.js'; + +import * as javascript from '../javascript.js'; + +const elementSchema = z.object({ + element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), +}); + +const screenshot = defineTool({ + capability: 'core', + schema: { + name: 'browser_screen_capture', + title: 'Take a screenshot', + description: 'Take a screenshot of the current page', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async context => { + const tab = await context.ensureTab(); + const options = { type: 'jpeg' as 'jpeg', quality: 50, scale: 'css' as 'css' }; + + const code = [ + `// Take a screenshot of the current page`, + `await page.screenshot(${javascript.formatObject(options)});`, + ]; + + const action = () => tab.page.screenshot(options).then(buffer => { + return { + content: [{ type: 'image' as 'image', data: buffer.toString('base64'), mimeType: 'image/jpeg' }], + }; + }); + + return { + code, + action, + captureSnapshot: false, + waitForNetwork: false + }; + }, +}); + +const moveMouse = defineTool({ + capability: 'core', + schema: { + name: 'browser_screen_move_mouse', + 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 (context, params) => { + const tab = context.currentTabOrDie(); + const code = [ + `// Move mouse to (${params.x}, ${params.y})`, + `await page.mouse.move(${params.x}, ${params.y});`, + ]; + const action = () => tab.page.mouse.move(params.x, params.y); + return { + code, + action, + captureSnapshot: false, + waitForNetwork: false + }; + }, +}); + +const click = defineTool({ + capability: 'core', + schema: { + name: 'browser_screen_click', + title: 'Click', + description: 'Click left mouse button', + inputSchema: elementSchema.extend({ + x: z.number().describe('X coordinate'), + y: z.number().describe('Y coordinate'), + }), + type: 'destructive', + }, + + handle: async (context, params) => { + const tab = context.currentTabOrDie(); + const code = [ + `// Click mouse at coordinates (${params.x}, ${params.y})`, + `await page.mouse.move(${params.x}, ${params.y});`, + `await page.mouse.down();`, + `await page.mouse.up();`, + ]; + const action = async () => { + await tab.page.mouse.move(params.x, params.y); + await tab.page.mouse.down(); + await tab.page.mouse.up(); + }; + return { + code, + action, + captureSnapshot: false, + waitForNetwork: true, + }; + }, +}); + +const drag = defineTool({ + capability: 'core', + schema: { + name: 'browser_screen_drag', + title: 'Drag mouse', + description: 'Drag left mouse button', + 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 (context, params) => { + const tab = context.currentTabOrDie(); + + const code = [ + `// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`, + `await page.mouse.move(${params.startX}, ${params.startY});`, + `await page.mouse.down();`, + `await page.mouse.move(${params.endX}, ${params.endY});`, + `await page.mouse.up();`, + ]; + + const action = 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(); + }; + + return { + code, + action, + captureSnapshot: false, + waitForNetwork: true, + }; + }, +}); + +const type = defineTool({ + capability: 'core', + schema: { + name: 'browser_screen_type', + title: 'Type text', + description: 'Type text', + inputSchema: z.object({ + text: z.string().describe('Text to type into the element'), + submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'), + }), + type: 'destructive', + }, + + handle: async (context, params) => { + const tab = context.currentTabOrDie(); + + const code = [ + `// Type ${params.text}`, + `await page.keyboard.type('${params.text}');`, + ]; + + const action = async () => { + await tab.page.keyboard.type(params.text); + if (params.submit) + await tab.page.keyboard.press('Enter'); + }; + + if (params.submit) { + code.push(`// Submit text`); + code.push(`await page.keyboard.press('Enter');`); + } + + return { + code, + action, + captureSnapshot: false, + waitForNetwork: true, + }; + }, +}); + +export default [ + screenshot, + moveMouse, + click, + drag, + type, +]; diff --git a/mcp/servers/playwright-mcp/src/tools/wait.ts b/mcp/servers/playwright-mcp/src/tools/wait.ts new file mode 100644 index 00000000..fc8be827 --- /dev/null +++ b/mcp/servers/playwright-mcp/src/tools/wait.ts @@ -0,0 +1,70 @@ +/** + * 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 { defineTool, type ToolFactory } from './tool.js'; + +const wait: ToolFactory = captureSnapshot => defineTool({ + capability: 'wait', + + 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) => { + if (!params.text && !params.textGone && !params.time) + throw new Error('Either time, text or textGone must be provided'); + + const code: string[] = []; + + if (params.time) { + code.push(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`); + await new Promise(f => setTimeout(f, Math.min(10000, 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) { + code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`); + await goneLocator.waitFor({ state: 'hidden' }); + } + + if (locator) { + code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`); + await locator.waitFor({ state: 'visible' }); + } + + return { + code, + captureSnapshot, + waitForNetwork: false, + }; + }, +}); + +export default (captureSnapshot: boolean) => [ + wait(captureSnapshot), +]; diff --git a/mcp/servers/playwright-mcp/src/transport.ts b/mcp/servers/playwright-mcp/src/transport.ts new file mode 100644 index 00000000..2342fe9c --- /dev/null +++ b/mcp/servers/playwright-mcp/src/transport.ts @@ -0,0 +1,149 @@ +/** + * 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 http from 'node:http'; +import assert from 'node:assert'; +import crypto from 'node:crypto'; + +import debug from 'debug'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; + +import type { AddressInfo } from 'node:net'; +import type { Server } from './server.js'; + +export async function startStdioTransport(server: Server) { + await server.createConnection(new StdioServerTransport()); +} + +const testDebug = debug('pw:mcp:test'); + +async function handleSSE(server: Server, 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 SSEServerTransport('/sse', res); + sessions.set(transport.sessionId, transport); + testDebug(`create SSE session: ${transport.sessionId}`); + const connection = await server.createConnection(transport); + res.on('close', () => { + testDebug(`delete SSE session: ${transport.sessionId}`); + sessions.delete(transport.sessionId); + // eslint-disable-next-line no-console + void connection.close().catch(e => console.error(e)); + }); + return; + } + + res.statusCode = 405; + res.end('Method not allowed'); +} + +async function handleStreamable(server: Server, 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 StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + onsessioninitialized: sessionId => { + sessions.set(sessionId, transport); + } + }); + transport.onclose = () => { + if (transport.sessionId) + sessions.delete(transport.sessionId); + }; + await server.createConnection(transport); + await transport.handleRequest(req, res); + return; + } + + res.statusCode = 400; + res.end('Invalid request'); +} + +export async function startHttpServer(config: { host?: string, port?: number }): Promise { + const { host, port } = config; + const httpServer = http.createServer(); + await new Promise((resolve, reject) => { + httpServer.on('error', reject); + httpServer.listen(port, host, () => { + resolve(); + httpServer.removeListener('error', reject); + }); + }); + return httpServer; +} + +export function startHttpTransport(httpServer: http.Server, mcpServer: Server) { + 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('/mcp')) + await handleStreamable(mcpServer, req, res, streamableSessions); + else + await handleSSE(mcpServer, req, res, url, sseSessions); + }); + const url = httpAddressToString(httpServer.address()); + const message = [ + `Listening on ${url}`, + 'Put this in your client config:', + JSON.stringify({ + 'mcpServers': { + 'playwright': { + 'url': `${url}/sse` + } + } + }, undefined, 2), + 'If your client supports streamable HTTP, you can use the /mcp endpoint instead.', + ].join('\n'); + // eslint-disable-next-line no-console + console.error(message); +} + +export function httpAddressToString(address: string | 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}`; +} diff --git a/mcp/servers/playwright-mcp/tsconfig.json b/mcp/servers/playwright-mcp/tsconfig.json new file mode 100644 index 00000000..114ce8b8 --- /dev/null +++ b/mcp/servers/playwright-mcp/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "moduleResolution": "nodenext", + "strict": true, + "module": "NodeNext", + "rootDir": "src", + "outDir": "./lib", + "resolveJsonModule": true + }, + "include": [ + "src", + ], +} diff --git a/mcp/setup-all-mcp-servers.sh b/mcp/setup-all-mcp-servers.sh index 785af037..5123523e 100755 --- a/mcp/setup-all-mcp-servers.sh +++ b/mcp/setup-all-mcp-servers.sh @@ -41,6 +41,7 @@ setup_scripts=( "setup-gitlab-mcp.sh" "setup-brave-search-mcp.sh" "setup-gdrive-mcp.sh" + "setup-playwright-mcp.sh" ) failed_setups=() @@ -111,6 +112,7 @@ binaries=( "servers/git-mcp-server/.venv/bin/python" "servers/filesystem-mcp-server/node_modules/.bin/filesystem-server" "servers/gitlab-mcp-server/.venv/bin/python" + "servers/playwright-mcp/index.js" ) for binary in "${binaries[@]}"; do diff --git a/mcp/setup-playwright-mcp.sh b/mcp/setup-playwright-mcp.sh new file mode 100755 index 00000000..44b8895e --- /dev/null +++ b/mcp/setup-playwright-mcp.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# ========================================================= +# PLAYWRIGHT MCP SERVER SETUP SCRIPT +# ========================================================= +# PURPOSE: Sets up the vendored Playwright MCP server +# This is a vendored copy maintained independently +# ========================================================= + +# Get the directory where this setup script is located +CURRENT_SCRIPT_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$CURRENT_SCRIPT_DIRECTORY/utils/mcp-setup-utils.sh" + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed. Please install Node.js first." + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "Error: npm is not installed. Please install npm first." + exit 1 +fi + +# Check if playwright-mcp directory exists +if [ ! -d "$CURRENT_SCRIPT_DIRECTORY/servers/playwright-mcp" ]; then + echo "Error: playwright-mcp directory not found at $CURRENT_SCRIPT_DIRECTORY/servers/playwright-mcp" + echo "This should be part of the dotfiles repository now." + exit 1 +else + echo "Found playwright-mcp directory in dotfiles..." +fi + +# Set up the Node.js environment +echo "Setting up Node.js environment for Playwright MCP server..." +cd "$CURRENT_SCRIPT_DIRECTORY/servers/playwright-mcp" + +# Install dependencies +if [ -f "package.json" ]; then + echo "Installing Node.js dependencies from package.json..." + npm install +else + echo "Error: No package.json found. Installation cannot proceed." + exit 1 +fi + +# Make the main script executable +if [ -f "index.js" ]; then + chmod +x "index.js" + echo "Made index.js executable" +else + echo "Warning: Could not find index.js" +fi + +echo "Playwright MCP server setup complete!" +echo "The wrapper script will now use the vendored version." \ No newline at end of file From 62507724b28126333253f44d28b9ab1b45b7875a Mon Sep 17 00:00:00 2001 From: Morgan Joyce Date: Fri, 18 Jul 2025 09:46:55 -0500 Subject: [PATCH 2/2] fix(mcp): update git mcp server implementation --- .../git-mcp-server/src/mcp_server_git/server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mcp/servers/git-mcp-server/src/mcp_server_git/server.py b/mcp/servers/git-mcp-server/src/mcp_server_git/server.py index d3624ce0..e3017352 100644 --- a/mcp/servers/git-mcp-server/src/mcp_server_git/server.py +++ b/mcp/servers/git-mcp-server/src/mcp_server_git/server.py @@ -393,6 +393,19 @@ def git_add(repo: git.Repo, files: list[str]) -> str: if not file_path.startswith(repo_path): raise ValueError(f"Invalid file path: {file}") + # Check for company-notes patterns (security: prevent accidental leaks) + import fnmatch + blocked_patterns = [] + for file in files: + # Normalize path separators + normalized_file = file.replace('\\', '/') + # Check if file matches *-notes/* pattern (e.g., company-notes/, flywire-notes/, etc.) + if fnmatch.fnmatch(normalized_file, "*-notes/*") or fnmatch.fnmatch(normalized_file, "*-notes"): + blocked_patterns.append(file) + + if blocked_patterns: + raise ValueError(f"Cannot add *-notes/ files (no-leaks principle): {', '.join(blocked_patterns)}") + # Find missing files using list comprehension missing_files = [ file for file in files